Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Any Science Behind Sheet Performance?

1644681626
John D.
Pro
Sheet Author
TL;DR Version Seems too many repeating sections is the most significant factor in poor sheet performance.  Is there any deeper understanding of why this is?  Does the volume of values/variables assigned to a repeating section have an impact?  Is it just a numbers game that sheets can only handle n repeating sections before there is noticeable lag in the sheet? Full Context Version With my custom sheet I noticed that a new sheet with few changes was highly performant, but after adding so many repeating sections performance really took a dive.  I went to the forums and read this to be the most likely case as the things which make JavaScript lag were really in getAttrs().  So I did a little performance testing of my own and found this to indeed be the case with an average of 500 ms to fetch 5 or 500 values.  The setAttrs() function seemed pretty lean, and getSectionIDs() came in second next to getAttrs() for causing lag at around 100 ms for the first call and 50 ms for each consecutive call (I do 4 calls). I considered leveraging async/await to conditionally call getSectionIDs() since I only need that data for a few scenarios, but while my POC did produce better run times I did not experience better performance.  Once I created one too many repeating sections, the performance just dragged irrespective of run time. Seems the only way to really make my sheet performant is to sort out the black magic behind repeating sections and try my best to limit use of them.  I can post recommendations for limiting repeating sections, but my sheet relies heavily on them.  Are there any best practices with using repeating sections I'm missing?
1644682687

Edited 1644682864
GiGs
Pro
Sheet Author
API Scripter
Your findings are interesting. Previous testing by Scott suggests setAttrs is a considerably bigger offender than getAttrs. If you're seeing poor performance due to getAtrs/setAttrs/getSectionIDs, the first thing to do would be to look at your sheet worker design, and see if you can reduce the number of getAttrs/setAttrs/getSectionIDs calls.  Look for cascade loops - where changing one attribute causes multiple sheet workers to fire, each of which causes other sheet workers to fire, and so on. If you have any of those, eliminate the cascades and combine the sheet workers where you can. Scott's favoured method I believe is to basically convert the sheet into a single sheet worker, doing one massive getAttrs and one setAttrs, and performing all calculations within that sheet. I don't go quite that far (though I almost did in the Torg Eternity sheet, as an experiment), but most sheets I've found do have areas where you can optimise the get/set path if you need to. It can be a bit of work, but if your sheet is lagging notably, it's necessary. Personally I've only found it to be necessary on sheets where those cascade loops have gone too far, or you have thousands of attributes, or more likely, both. I havent noticed an issue with repeating Sections-  though you said you have 4 repsection calls. Are they in the same worker? Repeating Sections do make it very easy to add lots of extra attributes, which might be a source of lag.
1644683612

Edited 1644683707
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Hmm, those results are the opposite of what I found in my tests, although I admittedly haven't tested their speed in a couple years. In my tests, setAttrs was the slowest sheetworker operation taking somewhere in the hundreds of milliseconds (depends on server conditions and your internet connection), while the get sheetworkers (getAttrs, getSectionIDs) are faster at somewhere in the 10's of milliseconds to process a request. All of that said, the way to avoid performance issues from javascript is to not chain several setAttrs together by relying on the event listeners to propagate changes throughout a sheet as this will quickly cause noticeable lag (especially if you have repeating section changes propagating this way). So, instead of writing code like this: <input name="attr_one" type="number" value="1"> <input name="attr_two" type="number" value="2" readonly> <input name="attr_three" type="number" value="6" readonly> <input name="attr_four" type="number" value="24" readonly> <script type="text/worker"> const calcTwo = function(event){ let val = +event.newValue || 0; const setObj = {}; setObj.two = val * 2 setAttrs(setObj); }; const calcThree = function(event){ let val = +event.newValue || 0; const setObj = {}; setObj.three = val * 3 setAttrs(setObj); }; const calcFour = function(event){ let val = +event.newValue || 0; const setObj = {}; setObj.four = val * 4 setAttrs(setObj); }; on('change:one',calcTwo); on('change:two',calcThree); on('change:three',calcFour); </script> Do all those actions with a single getAttrs and a single setAttrs: <input name="attr_one" type="number" value="1"> <input name="attr_two" type="number" value="2" readonly> <input name="attr_three" type="number" value="6" readonly> <input name="attr_four" type="number" value="24" readonly> <script type="text/worker"> const calcTwo = function(val){ return val * 2; }; const calcThree = function(val){ return val * 3; }; const calcFour = function(val){ return val * 4; }; const oneChanged = function(event){ const setObj = {}; let val = +event.newValue || 0; setObj.two = calcTwo(val); setObj.three = calcThree(setObj.two); setObj.four = calcFour(setObj.three); setAttrs(setObj,{silent:true});//I always set silently to help ensure that I don't have any cascades fire. }; on('change:one',oneChanged); </script> Now, that demo is an incredibly simple proof of concept and should actually be written with a lot more iterating of each calculation instead of hardcoding them in, but should get the point across for this discussion. If you're interested in a sheet framework that makes setting up this one getAttrs / one setAttrs pattern easy, you might want to follow my weekly forum post on building sheets, A Sheet Author's Journey  where I use my K-scaffold to create a sheet from scratch. The post coming this week will be discussing creation of repeating sections and how to hook up the javascript functions for the attributes that have been created so far. EDIT: Sniped by GiGs! But, yes this is a discussion he and I get into pretty much every time this comes up. I probably do go to far for most things, but it makes my coding simpler for me.
1644691727
GiGs
Pro
Sheet Author
API Scripter
Scott C. said: EDIT: Sniped by GiGs! But, yes this is a discussion he and I get into pretty much every time this comes up. I probably do go to far for most things, but it makes my coding simpler for me. I hope my post didnt come over as criticism. When you have a laggy sheet, your approach is the best way to deal with it.
1644693230
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Haha, not at all!
1644712607

Edited 1644712641
John D.
Pro
Sheet Author
Thanks, GiGs and Scott C! You guys are always expedient and thorough with your responses.  I really appreciate it! About 2, not quite 3 years ago I was working on BESM4e beta 2 sheet, and posted a question on performance issues.  You both responded then, too, and with similar advice.  All the things you'd said I should do I wasn't, and the things I shouldn't do I was.  I spent about the following 2 years in redesign and recoding from the ground up.  I learned a lot from that. The structure I came up with goes like this: <script start> global_variables functions() on(all_events, (eventInfo)=>{          <logic to determine if a repeating section was removed or a button was pressed, set variables accordingly>     eventInfo.sourceAttribute.replace(/^((.+?)_(.+?)_(.+?))_(.+)/, (match, source, sourceType, sectionName, rowID, sourceAttr) => {         var setObj = {};         getSectionIDs(ids1, {         getSectionIDs(ids2, {         getSectionIDs(ids3, {         getSectionIDs(ids4, {             getAttrs(all_the_data, values => {                 If(removed) {                     <clean up stuff>                     setAttrs(setObj,{silent:true});                     return;                 } else if(sectionType == 'repeating') {                     switch(sectionName) {                         case 'sn1':                                                          switch(sourceAttr) {                                 case 'sa1':                                     <do stuff>                                 break;                                 case 'sa2':                                     <do stuff>                                 break:                                 ...etc...                             }                         break;                         case 'sn2':                                                          switch(sourceAttr) {                                 case 'sa3':                                     <do stuff>                                 break;                                 case 'sa4':                                     <do stuff>                                 break;                                 ...etc...                             }                         }                         ...etc...                     }                                          setAttrs(setObj,{silent:true});                     return;                 } else { //non-repeating events                     <same structure as above, but for non-repeating events>                     setAttrs(setObj,{silent:true});                     return;                 }             });         });         });         });         });     }); }); </script end> This got me out of the cascading sheet worker hell, and eliminated a lot of redundant code.  All-in-all, the JavaScript seems to run efficiently, and with a minimal amount of repeating sections added the sheet is performant.  There's some threshold I've not tested for yet (which is really why I'm reaching out to the community now) where sheet performance goes out the window. The main repeating section that drives the whole sheet probably has more attributes/elements than the other repeating sections ... and the rest of the non-repeating sheet!  This is why I asked if it is know whether repeating section data/attributes/elements can cause lag, or just the presence of too many repeating sections (irrespective of how much data is attached to it).  I think  GiGs has answered that question, it's a matter of data/attributes overall and my repeating sections just add to the problem with more data/attributes. I've been playing around with this idea of pulling 98% of the UI and elements out of the main repeating section and placing them in a non-repeating area of the sheet, storing/managing most all the repeating section data in a JSON variable.  When a player puts focus on a repeating section, populate the external UI with the data of the repeating section from the JSON. In theory, this seems like it could sidestep the thousands of attributes issue and remain performant.  I have no idea if this will work but I think I can pull it off.  Converting existing sheets will be interesting. On the performance testing subject, my method was to leverage Date.now().  I set a base variable with Date.now() and then compared it to successive calls before and after functions to determine the time a function took to complete and the overall run time. Thoughts?
1644713210
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Ah, if you have a repeating section that is that big, then that's probably your problem. Your JSON method probably won't fix the issue because the sheet's attributes are always present once created, and repeating sections create (or at least reserve memory for) all the attributes in a section when a row is created. You'd need to actually delete all the rows and remake them each time, which sort of defeats the purpose of having all that data in the repeating section. I'd question why a repeating section is so large on your sheet. Do you have lots of attributes that hold intermediate steps of calculations? And do you have any autocalc inputs in these repeating sections?
1644718806

Edited 1644719434
John D.
Pro
Sheet Author
Thanks for another quick reply, Scott C! It took me all day to compose that response due to battling my children.  It's been a Lord of the Flies kind of day, and, while I've got family over to hide behind I'm going to take this moment to respond!  ;) All good questions.  Before I get into it I think I owe you a bit of context if you're not familiar with BESM.  Similar to GURPS or Champions (Hero Systems), BESM is a generic game system.  Just more simplified than GURPS/Champions (actually, I think it's based on Champions as the similarities are a bit coincidental...anyway.)  Also, before things get weird, BESM uses the term "attributes" to define an ability/power. In BESM, attributes define everything and anything a character can do.  An attribute is meant to be generic and then defined by customizations (i.e. enhancements and limiters) to make them more unique or nuanced.  And this is part of the answer to your question of why the main repeating section is so large, because it adds an attribute to the character sheet and the variations are extensive.  The other part of that answer is...because it's the way I saw the solution at the time and didn't see this performance thing coming.  I mean, it seems obvious now.  Hindsight. OK, so you make a good point about the repeating sections existing data.  Here's what I'm thinking for conversions/upgrades.  Create a new repeating section...yes, I know that's what got me into this mess but bear with me a moment...  Hide the offending repeating section.  Fetch all the repeating section data and write it to a JSON attribute.  Then create a new repeating row in the new section, which will only need like 3-4 attributes for me to do all the mapping between the repeating row and the external UI (i.e. attribute name (for display purposes), attribute selection, hidden variable for "new attribute"...and perhaps something else not important for this conversation).  Then delete the original repeating row in the offending repeating section. My idea with the JSON is that I can manage all the states of the attribute, like level, customizations...just about anything that doesn't need to be displayed on the repeating row.  There would be some element like a bullet or checkbox for the player to click for focus, and that would drive the event to populate the external UI with all the additional detail of the repeating row (like the level, customizations, etc.)  Any change to the attribute external UI would be mapped to the rowID and recorded in the JSON, and all other required functions would pull from the JSON file as well. Yeah?  Maybe? Question: if I remove the HTML for the offending repeating section with this proposed upgrade method instead of just hide it...would that be bad?  Otherwise, I need to figure out when to deprecate that HTML...no? Edit: also, no auto-calc in the repeating section.  I do use a couple, and fully intend to replace them with a sheet worker at some point...but they were so easy for doing some attribute cost accounting that I just went with it.  When a change to an attribute's level is made (which adjusts the attribute's cost) it updates a non-repeating value.  That feeds into two auto-calc inputs: total cost and "character points" remaining.
1644729538
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Ok, I see what you're working with.  So, answers to your questions: Question: if I remove the HTML for the offending repeating section with this proposed upgrade method instead of just hide it...would that be bad?  Otherwise, I need to figure out when to deprecate that HTML...no? Deleting the HTML is just fine, and what I would recommend, as long as you do have an update function that will port it all over. The data from that section will still be on the sheet after you delete the html. It only gets deleted if you use the removeRepeatingRow sheetworker function. And then I've got some questions about how BESM works. Can a character have any number of attributes? and the attributes don't necessarily need to be the same between characters? What sorts of customization are supported? Can you share the sheet code that you're working with at the moment? Because based on the information you've given, I'm not sure why the repeating sections would be causing performance issues (I've got a sheet in development that has 9 or 10 different repeating sections ranging from dead simple (about 3 attributes) to incredibly complex (something like 20 attributes per row), and it's working just fine with multiple rows added to each section. If you do decide to go with your JSON driven UI, I think it's probably a good idea although there's some potential pitfalls to watch out for, but that's mostly just making sure that the javascript is robust enough to not leave the user hanging. The Aaron and I have actually discussed a fully JSON driven sheet, so I'd be interested to see how this works. As for the auto-calc, that's good, just wanted to make sure we weren't dealing with like 10-20 auto-calc attributes per row which would really start to bog down the sheet.
1644774861

Edited 1644778602
John D.
Pro
Sheet Author
Good Morning Scott C! That's a lot better than, "you'll shoot your eye out, kid."  Which was the response I was expecting.  Can a character have any number of attributes? Yes, there are about 90 attributes to choose from, 30 standard customizations that can be applied to most all attributes, and 1-5 attribute specific customizations in some cases, but the weapon attribute has 50 customizations unto itself.  These attributes and customizations can generally be applied in any combination, and attributes can be applied more than once (such as a weapon attribute can be applied multiple times to defined a sword, a gun, swam missile battery, etc.) and the attributes don't necessarily need to be the same between characters? Correct.  Likely only on simple NPCs would this happen, like with henchmen or standard army soldiers, etc.  Generally in BESM, PCs and major NPCs are unique. What sorts of customization are supported? Generally customizations fall into one of two buckets, an enhancement  which provides some advantage or extension of the attribute.  Such as Range (by default an attribute applies to the character or by touching a target).  Or a customization could be a limiter , meaning that it applies some restriction or nerfs the attribute in some way.  Such as Concentration, a character can only defend (or not) when using the attribute as it requires intense focus. All of these customizations are represented by select inputs and all exist in each repeating row, I simply hide/show the relevant customizations depending on which attribute was chosen.  Rather than retrieving customization values individually, when a customization is configured, I add/remove it, respectively, to an array stored in a hidden input. To add more complexity to this, some attributes and customizations require additional configuration facilitated by a special input (text, select, or button array) to better define...something.  Such as the customization Ammo for Weapons.  When the Ammo limiter is selected, the initial configuration defines the number of reloads  (such as the number of magazines one can carry on their person for a firearm).  Then a special input is revealed to define the reload capacity (such as the number of cartridges a magazine can hold). Can you share the sheet code that you're working with at the moment? Certainly, it's on Github here .  If you'd rather I embed it in this post, I can try...but it's 14,000 lines of code.  Don't judge me. ;)
1645578514

Edited 1645590759
John D.
Pro
Sheet Author
UPDATE: I found that getAttrs() performance suffers more from the number of attributes fetched than the size of the attributes fetched.  Meaning, if I have 500 attributes whose total volume in size is 60kb, calling getAttrs() to fetch all 500 may average 200ms.  However, if I dump those 500 attributes into a single attribute as a JSON string, then my average getAttrs() fetch time decreases to 50ms. Note, these are approximate, averaged numbers based on my experience in testing.  I've not recorded actual times and volumes in proper scientific fashion.  However, my sheet performance since I've changed to a single JSON string have dramatically increased overall performance and playability.  Here's how I went about it. In my "on open" event I call all the data.  Everything.  So opening a sheet drags a bit, but this is a one-time operation.  Then I dump the single object with all the data into a new object, add in the arrays I need from getSectionIDs, and finally JSON.strigify the new object and write it to the sheet via setAttrs().  This approach seemed better and safer than trying to maintain a shadow database, as if there is any data corruption or synching issues then just closing and opening the sheet again refreshes all the legit data. When my single event listener fires (well, all events except open ;) ) I use getAttrs() to fetch my all-data-JSON-string and parse it into an object using the same name as the "values" object I used prior.  Because Roll20 uses a flat data structure I decided to do the same rather than reformatting my data into a hierarchical tree.  This allowed me to use the same code in my sheet, like v[`repeating_section_${rowID}_attribute`] without any additional changes.  This transition has been A LOT easier and quicker than I originally expected. Additionally, I breakout the sectionIDs into their own arrays that I built in the on open event from getSectionIDs.  Any new repeating sections created are pushed into their respective array. Before performing a setAttrs(), I merge the values object with another object used for capturing all sheet changes (overwriting the values object) along with the sectionIDs arrays, and JSON.stringify it before feeding it to setAttrs().  And...persistency! I'm not sure if going async/await would provide additional performance benefits, but that may be the next thing I POC once I'm done with a lot of long and sorely needed refactoring.  I mention this because there are still a few things I've noticed that I can't yet account for.  Such as, the getAttrs() fetch time seems to vary widely depending on the complexity of the script pieces that run following it.  I suspect this could be due to the way callbacks work and it would be an interesting exercise to sort out.  Also, I think CSS might be another point of optimization as my CSS file is about as organized as junkheap.  My hide/show/replace events all seem to drag, and I suspect this could be due to the parsing of my junky CSS file. Another factor I suspected but couldn't determine a way to test for was the availability of Roll20's "getAttrsAPI".  I felt like an early astronomer before telescopes.  A successive getAttrs() fetch time on the same operation could swing wildly by a few hundred ms.  This made me think that good response times were due to low volume of API calls, and poor response times due to high volume of API calls.  Like waiting in a queue to be processed. Here's some examples of my code for reference: function recordSheetChanges(eventInfo, values, setObj) {     const sheet_change_array = [<array of all non-actionable attributes>];     const recordChange = sheet_change_array.indexOf(eventInfo.sourceAttribute) !== -1 ? true : false;     if (recordChange) { setObj[`${eventInfo.sourceAttribute}`] = eventInfo.newValue; value = {...value, ...setObj}; setObj.sheetdb = JSON.stringify(value); setAttrs(setObj,{silent:true});     }     return recordChange; } function cleanupSheetdb(removedInfo, values) {     _.chain(removedInfo)     .keys()     .each( o => { delete values[o.toLowerCase()];     }); } function cleanupIDs(ids1, ids2, rowID) {     if (ids1.indexOf(rowID) !== -1) {         ids1.splice(ids1.indexOf(rowID), 1);     } else if (ids2.indexOf(rowID) !== -1) {         ids2.splice(ids2.indexOf(rowID), 1);     } } function registerEvents() {     const eventsToRegister = [ 'repeating_section1', 'repeating_section2', 'static_stat_base_str', 'static_stat_base_dex'     ];     const eventString = eventsToRegister.join(" change:");     return eventString; }; function registerActions() {     const buttonsToRegister = [ 'repeating_section1', 'repeating_section2', 'static_button_weapon_attack', 'static_button_save_wis'     ];     const buttonString = buttonsToRegister.join(" clicked:");     return buttonString; } function registerRemoved(){     const eventsToRegister = [ 'repeating_section1', 'repeating_section2', 'repeating_defect'     ];     const eventString = eventsToRegister.join(" remove:");     return eventString; } const events = `change:${registerEvents()} clicked:${registerActions()} remove:${registerRemoved()}`; on("sheet:opened", () => {     getSectionIDs('section1', ids1 => {     getSectionIDs('section2', ids2 => {         const attrs_array = buildAttrsArray(<build_array_of_the_attributes>);         getAttrs(attrs_array, (values) => {             const setObj = {};             const sheetdb = {};             sheetdb.ids1 = ids1;             sheetdb.ids2 = ids2;             _.each(attrs_array, o => {                 if(values[o] !== undefined) sheetdb[o] = values[o];             });             setObj.sheetdb = JSON.stringify(sheetdb);             setAttrs(setObj);         });     });     }); }); on(events, (eventInfo) => {     const setObj = {};     getAttrs('sheetdb', (v) => {         let values = JSON.parse(v.sheetdb);         if (recordSheetChanges(eventInfo, values, setObj)) return; //this function iterates through a global array of all non-actionable attributes to capture changes to sheet data.         let ids1 = values.ids1;         let ids2 = values.ids2;                  eventInfo.sourceAttribute.replace(/^((.+?)_(.+?)_(.+?))_(.+)/, (match, source, sourceType, sectionName, rowID, sourceAttr) => {             source = source.toLowerCase();             match = match.toLowerCase();             rowID = rowID.toLowerCase();             const isRemoved = eventInfo.triggerName.indexOf('remove') !== -1 ? true : false;             if (isRemoved) {                                  const removedInfo = eventInfo.removedInfo;                 cleanupSheetdb(eventInfo.removedInfo, values);                 cleanupIDs(ids1, ids2, rowID);             } else {                 <code for handling all sheet events>             }             values = {...values, ...setObj};             setObj.sheetdb = JSON.stringify(values);             setAttrs(setObj,{silent:true});         });     }); }); EDIT: added code for creating an all events string for a single event listener.
1645717763

Edited 1645720894
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Hmm, It does indeed look like something has changed in the backend. I setup a new speed test and found that both setAttrs and getAttrs are scaling at a rate of ~1ms/attribute, but getAttrs is wildly variable taking a minimum of 80ms to get 1000 variables, but as long as 2500ms  11000ms (went back and reviewed my speed data and the highest get time was shocking!) to get those same variables. I'm hoping that they get this fixed, or I'm going to need to rewrite the JS for several sheets. Some notes on your thoughts on how to optimize: In my "on open" event I call all the data.  Everything.  So opening a sheet drags a bit, but this is a one-time operation.  Then I dump the single object with all the data into a new object, add in the arrays I need from getSectionIDs, and finally JSON.strigify the new object and write it to the sheet via setAttrs().  This approach seemed better and safer than trying to maintain a shadow database, as if there is any data corruption or synching issues then just closing and opening the sheet again refreshes all the legit data. This is something that I may experiment with depending on how long this backend change goes on. I think you've hit most of the pain points with this sort of method. The only thing I would suggest testing is what happens when you have multiple users making changes at the same time on a sheet and what happens when you make changes to several sheets at the same time. Both of these scenarios are extremely unlikely with just users, but when you throw in the API's ability to manipulate sheets it becomes a near certainty that an API might try to do several successive manipulations to the same character or to several characters. I'd also look into ways to handle APIs that change attributes values, but don't trigger sheetworkers (there's several legacy scripts out there that operate this way) as that will cause a desync between your cache and the actual values. When my single event listener fires (well, all events except open ;) ) I use getAttrs() to fetch my all-data-JSON-string and parse it into an object using the same name as the "values" object I used prior.  Because Roll20 uses a flat data structure I decided to do the same rather than reformatting my data into a hierarchical tree.  This allowed me to use the same code in my sheet, like v[`repeating_section_${rowID}_attribute`] without any additional changes.  This transition has been A LOT easier and quicker than I originally expected. This is good to hear. It's the area that I was most worried about with changing to this method. Additionally, I breakout the sectionIDs into their own arrays that I built in the on open event from getSectionIDs.  Any new repeating sections created are pushed into their respective array. Before performing a setAttrs(), I merge the values object with another object used for capturing all sheet changes (overwriting the values object) along with the sectionIDs arrays, and JSON.stringify it before feeding it to setAttrs().  And...persistency! Other than the cache object, this is similar to what the K-scaffold does. I'll probably look into a way to incorporate this cache method into the K-scaffold. I'm not sure if going async/await would provide additional performance benefits, but that may be the next thing I POC once I'm done with a lot of long and sorely needed refactoring.  I mention this because there are still a few things I've noticed that I can't yet account for.  Such as, the getAttrs() fetch time seems to vary widely depending on the complexity of the script pieces that run following it.  I suspect this could be due to the way callbacks work and it would be an interesting exercise to sort out.  Unfortunately, async/await is not possible in character sheets (other than when using startRoll). There is a library of code called Roll20Async that we thought had solved the problem for several months, however it breaks the API sandbox so that any game using a sheet running on Roll20Async can't use API scripts that interact with attributes. Also, I think CSS might be another point of optimization as my CSS file is about as organized as junkheap.  My hide/show/replace events all seem to drag, and I suspect this could be due to the parsing of my junky CSS file. CSS doesn't care a great deal about organization fortunately, however if you have multiple rules that could apply to an item and are overwriting things several times, I suppose that could cause some performance issues. A bigger performance issue that I find is writing CSS declarations that are too broad. So something like: div .div-5 .span-6{/*some style*/} can really screw with the CSS efficiency because the CSS parser has to essentially check every div to see if it or any  of its children contain .div-5 and then has to check if .div-5 or any  of its children contain .span-6. If you've got a reasonably complex element structure, this can really slow down the rendering of the sheet, especially if the repeating sections are involved in the element tree that the CSS needs to inspect. Another factor I suspected but couldn't determine a way to test for was the availability of Roll20's "getAttrsAPI".  I felt like an early astronomer before telescopes.  A successive getAttrs() fetch time on the same operation could swing wildly by a few hundred ms.  This made me think that good response times were due to low volume of API calls, and poor response times due to high volume of API calls.  Like waiting in a queue to be processed. I suspect that this is the cause of the wild swings in getAttrs times, but it's odd that the setAttrs times are so much more consistent. For anyone interested, here's my sheet speed test code: <label style="display:inline;"> <span>Number of attributes</span> <input type='number' name='attr_num' value='10' style="width:10rem"> </label> <label style="display:inline;"> <span>Iterations</span> <input type='number' name='attr_iterations' value='100' style="width:10rem"> </label> <button type='action' name='act_trigger'>Test</button> <label style="display:inline;"> <span>Iterations Remaining</span> <input type='number' name='attr_iterations_remaining' value='0' style="width:10rem"> </label> <label style="display:inline;"> <span>Duration of Last Set Iteration (ms)</span> <input type='number' name='attr_last_set_duration' value='0' style="width:10rem"> </label> <label style="display:inline;"> <span>Duration of Last Get Iteration (ms)</span> <input type='number' name='attr_last_get_duration' value='0' style="width:10rem"> </label> <script type="text/worker"> const times = { /* general format is: gets:[{attrNum:20,duration:30},{attrNum:20,duration:40}],getAvgs:{'20':35},lastget:40 duration is in ms */ gets:[],getAvgs:{}, sets:[],setAvgs:{} }; let lastGetDuration = 0; const calcStats = function(type,start,end,attrNum){ let duration = end - start; times[`${type}s`].push({attrNum,duration}); let durationArr = times[`${type}s`] .reduce((memo,timeObj)=>{ if(timeObj.attrNum === attrNum){ memo.push(timeObj.duration); } return memo; },[]); times[`${type}Avgs`][`${attrNum}`] = durationArr .reduce((total,duration)=>{ return total + duration; }) / durationArr.length; return duration; }; const iterateTest = function(attrNum,iterations,origIterations,setDuration=0,getDuration=0){ origIterations = origIterations || iterations; iterations--; const getArr = []; const initiateObj = _.range(attrNum).reduce((m,n)=>{ m[`attribute_${n}`] = Math.random(); getArr.push(`attribute_${n}`); return m; },{iterations_remaining:iterations,last_get_duration:getDuration,last_set_duration:setDuration}); let setStart = Date.now(); setAttrs(initiateObj,{silent:true},()=>{ let setEnd = Date.now(); let setDuration = calcStats('set',setStart,setEnd,attrNum); let getStart = Date.now(); getAttrs(getArr,(attributes)=>{ let getEnd = Date.now(); let getDuration = calcStats('get',getStart,getEnd,attrNum); if(iterations){ iterateTest(attrNum,iterations,origIterations,setDuration,getDuration); }else{ outputResults(attrNum,setDuration,getDuration,origIterations); } }); }); }; const outputResults = function(attrNum,setDuration,getDuration,iterations){ console.log('Stat Summary'); console.table({ runs:iterations, get:{'Number of Attributes':attrNum,Duration:getDuration,'Average Duration (ms)':times.getAvgs[`${attrNum}`]}, set:{'Number of Attributes':attrNum,Duration:setDuration,'Average Duration (ms)':times.setAvgs[`${attrNum}`]}, }); console.log('Raw Stats'); console.table({runs:iterations,...times}); }; const runSpeedTest = function(){ console.log('Running sheet speed test'); getAttrs(['num','iterations'],(options)=>{ //Initiate the attributes to test let attrNum = +options.num || 1; let iterations = +options.iterations; iterateTest(attrNum,iterations); }); }; on('clicked:trigger',runSpeedTest); </script>
1645718917
GiGs
Pro
Sheet Author
API Scripter
Very interesting last couple of posts. Scott can you say more about this? Scott C. said: Unfortunately, async/await is not possible in character sheets (other than when using startRoll). There is a library of code called Roll20Async that we thought had solved the problem for several months, however it breaks the API sandbox so that any game using a sheet running on Roll20Async can't use API scripts that interact with attributes. I've been wary of using anything in sheets that dont follow official documentation, because there's no telling what bugs they may expose. But I'm interested in using them for personal sheets and had hoped to explore this. How does it mess up the API?
1645719175
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
The Roll20Async stuff relies on using the self object (which is an alias for window). Unfortunately, this doesn't exist in the API sandbox because the API sandbox doesn't run on a browser, so any API script that triggers a sheetworker will crash the API sandbox because the sheet code running on the sandbox can't find the self object and throws a referenceError. Just replacing self with some empty object won't fix the issue because we actually need to use the methods that are part of the self object in order to use Roll20Async, and we can't recreate those methods on our own object.
1645720403

Edited 1645720426
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
oh, and John, something else to consider with the cache method is how to handle situations where a user hasn't actually opened up the sheet, but an attribute change is made. This will happen quite easily with API scripts, which might do campaign wide manipulation of attribute values, or with custom roll parsing, which allows action buttons to be called from chat. Either one of these features can allow a player to interact with their sheet without ever needing to open the sheet or directly interact with the sheet itself.
1645720822
GiGs
Pro
Sheet Author
API Scripter
Scott C. said: The Roll20Async stuff relies on using the self object (which is an alias for window). Unfortunately, this doesn't exist in the API sandbox because the API sandbox doesn't run on a browser, so any API script that triggers a sheetworker will crash the API sandbox because the sheet code running on the sandbox can't find the self object and throws a referenceError. Just replacing self with some empty object won't fix the issue because we actually need to use the methods that are part of the self object in order to use Roll20Async, and we can't recreate those methods on our own object. Thanks, that makes sense.
1646787878
John D.
Pro
Sheet Author
Scott, thanks for your thorough reply!  Sorry it's taken me a while to reply as I've been fleshing this thing out and bug hunting.  When you brought up the notion of a net-new character sheet, while I hadn't tested at the time, it opened up a whole new swath of bugs to sort out!  :P In addition to the above sample code, I had to work out some logic to write the attribute change in eventInfo to the cache...but then I found several attribute changes that needed similar but special handling because while they would be written to the sheet they would not be updated in the cache.  The only way to get them into cache was to close and open the character sheet again.  Solved that mess. After much testing of existing and net-new sheets, I'm (mostly) confident of the changes and have submitted the PR to bake in the changes. Thank you for your insight and feedback as always, and appreciate you sharing your performance testing code as well.  I'm a "learn by doing" guy, and having tangible examples to play with accelerates my learning. I'd also like to note that even with this caching system in place, and the vastly improved performance it provides, there is still an upper limit to what the sheet seems able to support.  I think in most cases players won't run into it, but god-like characters will suffer performance hits. Now with this non-functional stuff out of the way, I'm back to feature development.  :D
1646790247
GiGs
Pro
Sheet Author
API Scripter
What sheet is this? Is it still the BESM4esheet?
1646858312
John D.
Pro
Sheet Author
GiGs, yes it is!  Now I'm getting all insecure with the thought someone might go looking at my code.  If you do, please forgive the mess.  I still need to go in and refactor some of the new changes, I just wanted to get it into a PR since I consider it playable (i.e. all known significant bugs resolved).  There's also a bunch of deprecated code commented out that I'll remove once I'm confident I don't need it anymore.  ;)