Skip to content

Instantly share code, notes, and snippets.

@brainysmurf
Last active November 5, 2020 07:38
Show Gist options
  • Save brainysmurf/afb107c7d0e3d8c1785b1320bdd469b4 to your computer and use it in GitHub Desktop.
Save brainysmurf/afb107c7d0e3d8c1785b1320bdd469b4 to your computer and use it in GitHub Desktop.
Concurrency (lack thereof) in Google Apps Scripts V8

Concurrency in Google AppsScripts (V8)

Steps to reproduce

  • Run useForLoop independently, and observe it takes about 7 seconds to complete
  • Run entrypoint which calls useForLoop three times, asynchronously
  • Observe that the second run takes at least three times as long as the first run to complete
  • Repeat the process above but in a browser, and observe the expected behaviour

Conclusion: Even though the documentation indicates that you can define async functions, and that you can make Promises, it ain't actually async.

Note

Even more proof is in the fact in observing what happens when you run in the browser. It executes entrypoint() immediately and "finishes" (although the calculations are still being made) and eventually outputs. But for the V8 engine, it indicates to the user that it is running and doesn't immediately return to the user.

Questions

  • Why is setTimeout undefined but Promises and AsyncFunction work as expected (minus the actual async)
  • Will Google have another implementation where they do work?

Related

Please see this gist to see the only way to do async on AppsScripts platform. (Note: It now requires you to create your own GCP project and link new project to that manually.)

/*
Wherein I set out to demonstrate that even though Google V8 engine syntax includes async and Promises
it is async in syntax only, and not in implementation.
The code below takes async function and runs them in parallel with `Promises.all`
But it is clear from the timing that it runs syncronously.
*/
/*
The promise returned in this function should resolve itself in 5 seconds, and running three of them
in parallel should also take 5 seconds.
Instead, an error is thrown as setTimeout is a `ReferenceError` and is undefined.
It is expected that on the first error thrown, it stops the other two (does not throw three errors).
(That setTimeout is a ReferenceError probably be the only clue we need that the stack is not async.)
*/
function useSetTimeout () {
const fiveSeconds = 5000;
return new Promise(resolve => setTimeout(resolve, fiveSeconds)); // ReferenceError: setTimeout not defined
}
async function calc(i, j) {
return i + j;
}
/*
The promise returned in this function should resolve itself in about 7 seconds, and running three of them
in parallel should also take about 7 seconds.
Instead, it takes 24 seconds (your mileage may vary) ... as if it had to run three times syncronously
(This indicates that it isn't being run in parallel like it should be.)
*/
function useForLoop () {
return new Promise( resolve => {
let total = 0;
for (let i = 0; i <= 10000; i++) {
for (let j = i; j > 0; j--) {
total += await calc(i + j);
}
}
resolve("completed");
} );
}
/*
This function does the work of attempting to run the async functions in paraellel.
*/
async function concurrency(func) {
let promises = [];
promises.push(func());
promises.push(func());
promises.push(func());
// Send an array of promises which should be executed
let results = await Promise.all(promises);
return results;
}
/*
This function implements timing, calls `concurrency`, which hands off to the intended asyncFunction
*/
function timeit (asyncFunction) {
const start = new Date().getTime();
concurrency(asyncFunction).then(result => {
const end = new Date().getTime();
const duration = (end - start) / 1000;
Logger.log(`function ${asyncFunction.name} took ${duration} seconds to complete`);
}).catch(error => {
Logger.log(error);
});
}
/*
Execute this function from the play button in the online editor
*/
function entrypoint () {
timeit(useSetTimeout);
timeit(useForLoop);
}
@Spencer-Easton
Copy link

OK I did some more testing and now coming to agree with you when you mix in App Script services. Take the following example:

var SS = SpreadsheetApp.openById('1y4nz3oWfPM-112Zz-AYYZM-hqIdXBUunFnuzACOAqBU');
var testSheet = SS.getSheetByName('Sheet1');

function writeToSheet(row, col) {
   testSheet.getRange(row,col).setValue(new Date().getTime())
  // SpreadsheetApp.flush() // uncomment this for super slow write mode
}

const wrapInPromise = (f,args) => new Promise((res, rej) => res(f(...args)));

function runAsync() {
    const work = new Array(50).fill(0).map((curr, i) => wrapInPromise(writeToSheet,[i+1,1]))
    return Promise.all(work)
}

function runSync() {
    for (let i = 1; i <= 50; i++) {
        writeToSheet(i,2);
    }
}


function runTogether() {
    runAsync().catch(console.error);
    runSync();
}

The Promise.All is being processed serially when you add an Apps Script service.. It's like there is a separate stack for App Script service calls. When ever a call to a service happens it get added to the stack and that is processed serially. So Promise.all just adds the call to the stack faster then adding is serially. No real time savings. It only makes sense if your Promise.all is doing something computationally expensive in addition to the Apps Script service call which if you look at my last comment is much faster.

@brainysmurf
Copy link
Author

Fascinating.

I see you added the comment to the issue tracker. Google themselves had seemed to acknowledge it.

Link to the issue tracker: https://issuetracker.google.com/issues/149937257

@brainysmurf
Copy link
Author

It'd be a lot easier to test and reason about if setTimeout was available…

@brainysmurf
Copy link
Author

I also timed it by looking at the execution logs rather than using Logger.log as I was wondering the same thing about calls to services; I left it in the published code for easier viewing by the end user, and I may have not noticed a difference in that case.

But did you not see the await calc() in my code? I did have an earlier version that didn't quite have correct async code. You may have followed a link to older version?

This would be correct async code:

async function calc2(i, j) {
    return i + j;
}

function useForLoop() {
    let total = 0;
    for (let i = 0; i <= 10000; i++) {
        for (let j = i; j > 0; j--) {
            total += await calc2(i, j);
        }
    }
    return total;
}

@Spencer-Easton
Copy link

Yea I was looking at the code block you have up top. The code above would work after you tag useForLoop as asyc. As of right now I am with you that using Promises has mixed value atm.

@ShepTeck
Copy link

ShepTeck commented Nov 5, 2020

So I tried this code and it still doesn't work

  async function calc2(i, j) {
    return i + j;
}

async function useForLoop() {
    let total = 0;
    for (let i = 0; i <= 10000; i++) {
        for (let j = i; j > 0; j--) {
            total += await calc2(i, j);
        }
    }
    
    alert("total is:  "+total); 
    return total;
}

In my example:

html file:

<!DOCTYPE html>
<html>
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="https://fonts.googleapis.com/css?family=Oxygen|Raleway&display=swap" rel="stylesheet">
    <base target="_top">
        
   
   <!--<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">-->
   <link rel="stylesheet" href="https://drive.google.com/uc?export=view&id=1iKnmVGSAh70SSwzLERkTdcYT85cpJeWH">
   
      
   <!-- <script src="https://code.jquery.com/jquery-1.12.4.js"></script> -->
   <script src="https://drive.google.com/uc?export=view&id=1iFtElfW5_mtWZpEDHdoE9zPYRaMmGO1y"></script>
  </head>
  <body>
  
  <br>
  <h3>This is the body</h3>
  
    
  </body>
  
  <script>
  
  async function calc2(i, j) {
    return i + j;
}

async function useForLoop() {
    let total = 0;
    for (let i = 0; i <= 10000; i++) {
        for (let j = i; j > 0; j--) {
            total += await calc2(i, j);
        }
    }
    
    alert("total is:  "+total); 
    return total;
}
  
  
  
  
  </script>
  
  
  
</html>



It is being called by a function from my .gs file:

function callForm()
{
    //Call the HTML file and set the width and height
    //var html = HtmlService.createHtmlOutputFromFile("tracker_form")
    var html = HtmlService.createHtmlOutputFromFile("test_async")
        .setWidth(450)
        .setHeight(300);

    //Display the dialog
    var dialog = SpreadsheetApp.getUi().showModalDialog(html, "Select the relevant draft");
}

There are no errors; but the alert never prints.

@brainysmurf
Copy link
Author

Not sure why you're testing client-side.
In any case, how does useForLoop get called on the client-side?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment