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

createObj('attribute') Taking too long and getting "Possible infinite loop detected, shutting down" error. Any way to speed it up or avoid the error?

1600937326

Edited 1600937488
Chris D.
Pro
Sheet Author
API Scripter
Compendium Curator
I could use some help figuring out how I can speed up the execution of my code, or at least stop the system from thinking my code is in an infinite loop.  I wrote the DupCharToken API, which will duplicate a character and a token multiple time. This is handy for GMs that don't like Mooks, they can just click a token and tell it duplicate the character sheet and token X number of times, giving lots of copies of the token, each of which has its very own character sheet.  The problem is that if you try to make too many of them, you get the " Possible infinite loop detected, shutting down. " error, and the sandbox needs to be restarted. When I first made the API, I tested it and it would copy up to 40 simple characters before the error would appear. However I am getting complaints that it is happening with fewer and fewer of number of duplicates requested. Today I tried to duplicate a PC in a large campaign that had hundreds of characters (including npc's and monsters), and it would not make even one copy of a PC. I traced it down, and it depends upon the number of attributes (including repeating section attributes) the character has.  Specifically, each instance of  createObj('attribute', sa); Takes 35 or more milliseconds to run. Thus copying/creating 100 attributes is going to take more than 3.5 seconds to run. And creating more than 1000 attributes is going to cause it to time out. This might be 5 characters that have 200 attributes each, or 1 character with more than 1000. Use of repeatingt sections means that even simple monsters usually have at least 200 attributes.  An interesting thing is that running BOTH  let newCObj = createObj('character', newC);                 and let newTObj = createObj('graphic', newT); completes in under 8 ms. So creating attributes seems to be taking more than 9 times longer than creating ether tokens or characters and I am not certain why.  Anyway, I am hoping that somebody can explain why create attribute is taking so much longer, and suggest a way I can work around this.  Basically in my code I get the token and character, and create duplicates. Then I get all the attributes and copy them using... _.each(findObjs({_type:'attribute', _characterid:oldCid}),(a)=>{ // Copy each attribute let sa = JSON.parse(JSON.stringify(a)); delete sa._id; delete sa._type; for( let i = 0; i < charArray.length; ++i ) { delete sa._characterid; sa._characterid = charArray[ i ].id; if( links.indexOf( sa.name ) > -1 ) { let newA = createObj('attribute', sa);          tokenArray[ i ].set( "bar" + (links.indexOf( sa.name ) + 1).toString() + "_link", newA.id); // Link the new token to the new attribute. } else createObj('attribute', sa); }; }); Any ideas?
1600947001

Edited 1600947476
The Aaron
Roll20 Production Team
API Scripter
The way you fix this is by creating a queue of operations, and then yielding control between the creation of each one.  Javascript effectively works on the concept of cooperative multitasking, your code must yield to let other code run or it will starve the other code and bog down the system.  Roll20's sandbox enforces this with a watchdog timer that verifies executed code has returned within some time limit (1-2 minutes, don't remember exactly), and terminates the sandbox with "Possible Infinite Loop Detected" when it hasn't. Moving to queued work embraces asynchronous operations, which is how Javascript can remain fast despite being single threaded.  Here's the above code using queues for creation.  Note that any code that comes after this and depends on it completing will likely start to fail because of the asynchronous changes. // create the queue we'll be processing asynchronously let queue = findObjs({_type:'attribute', _characterid:oldCid}); // create the function that will process the next element of the queue const burndown = () => { if(queue.length){ let a = queue.shift(); //////////////////////////////////////////////////////////// // the work body from above //////////////////////////////////////////////////////////// // if this is also taking too long, you'll need to make another queue // based on the charArray let sa = JSON.parse(JSON.stringify(a)); delete sa._id; delete sa._type; for( let i = 0; i < charArray.length; ++i ) { delete sa._characterid; sa._characterid = charArray[ i ].id; if( links.indexOf( sa.name ) > -1 ) { let newA = createObj('attribute', sa); tokenArray[ i ].set( "bar" + (links.indexOf( sa.name ) + 1).toString() + "_link", newA.id); // Link the new token to the new attribute. } else createObj('attribute', sa); }; //////////////////////////////////////////////////////////// // yield the execution thread until the next time slice setTimeout(burndown,0); } else { // You could call a function to do work after this queue is finished here } }; // start the execution by doing the first element burndown(); Edit: Here's what it looks like if you also split out each createObj() call to be in it's own time sliced queue: // create the queue we'll be processing asynchronously let queue = findObjs({_type:'attribute', _characterid:oldCid}); // create the function that will process the next element of the queue const burndown = () => { if(queue.length){ let a = queue.shift(); //////////////////////////////////////////////////////////// // the work body from above //////////////////////////////////////////////////////////// // if this is also taking too long, you'll need to make another queue // based on the charArray let sa = JSON.parse(JSON.stringify(a)); delete sa._id; delete sa._type; let charQueue = [...charArray]; let index = 0; const charBurndown = () => { if(charQueue.length){ let c = charQueue.shift(); sa._characterid = c.id; if( links.indexOf( sa.name ) > -1 ) { let newA = createObj('attribute', sa); tokenArray[ index++ ].set( "bar" + (links.indexOf( sa.name ) + 1).toString() + "_link", newA.id); // Link the new token to the new attribute. } else { createObj('attribute', sa); } // yield execution then process the next charQueue entry setTimeout(charBurndown,0); } else { // yield execution then process the next queue entry setTimeout(burndown,0); } //////////////////////////////////////////////////////////// }; // start processing the charQueue charBurndown(); } else { // You could call a function to do work after this queue is finished here } }; // start the execution by doing the first element burndown(); (note the index variable replacing the i variable from the loop)
1600958365
Chris D.
Pro
Sheet Author
API Scripter
Compendium Curator
Thanks! that was perfect! I am using the first one, since the 2nd one would only error out if somebody tried to make more than 1000 copies of a token, and frankly such a silly thing ought to error out.  Thanks again!
1600958830
The Aaron
Roll20 Production Team
API Scripter
no problem!  I didn't test that code, so caveat emptor... =D