Addon Series Part 4: 🏃🏼‍♀️Running Apps Script in Parallel from HTML

Max Makhrov
6 min readFeb 28, 2024

Hi, I’m Max from CoolTables.

You’ll see how to run Google Apps Script “parallel” executions from an HTML sidebar. You may need this to create the best add-on or sidebar ever. There are 3 pros to learning this technique:

  1. The speed
  2. The control over async task executions
  3. Svelte framework!

In short

Sample SpreadsheetGitHub Repo

👀Examples

Create a new queue instance. Add new tasks:

import { Q } from "@/Parallels/q";

const qq = new Q();
qq.addTasks(tasks);

Note the alias @ in the import statement. It stands for the './src' folder.

1/9. Run, pause, run

Run, pause, run again, and stop the execution:

qq.run();
qq.pause();
qq.stop();

Show the process using this HTML:

const subscription = qq.getStore();
// $: is a special Svelte syntax for reactive declarations
$: ({ total, done, running, paused, stopped } = $subscription.info);

↑ The variables total, done, running and friends will update constantly. This works thanks to Svelte's special reactive syntax: $: at the start of the line. Note also $-sign before “$subscription”. It is for getting the store contents.

2/9. See real-time task updates

See real-time updates of tasks, including details like elapsed time, runtime, and status, as the script runs:

  <Codebox
>{JSON.stringify(
$subscription.tasks.filter((task) => statuses[task.status]),
null,
2
)}
</Codebox>

3/9. Split the code into components

Run GAS tasks from different components. The execution limit will work for each new call, and you’ll never run out of it:

This sample includes a component with 2 children each running the same engine.

The “running” store holds the total number of running tasks:

<script>
// ...
import { running } from "@/Parallels/engine";
</script>


<p style="font-size: large;">
<strong>Total tasks running: </strong><code>{$running}</code>
</p>

4/9. Visualize the progress

The sample below shows a pixel image. We create an image of different colors from a 2-D HEX-color array.

You can imagine any possible visual, and the data changes can be monitored as easily as mentioning the variable name:

<PixelPainting bind:this={docpainting} data={paintingData} />

↑ this simplicity is possible due to Svelte reactivity and stores. I cannot imagine how hard it would be without Svelte.

5/9. Do not wait until the task timeout

This sample lets you set the maximum time for each function to execute:

In my tests, if you run 30 or more tasks “in parallel”, some Apps Script calls may occasionally timeout. Here’s how you can prevent too-long executions:

import { RunnerOptions } from "@/Parallels/options";
const options = new RunnerOptions();
options.setTimeout(2000);

6/9. Set stepped timeouts for your functions

Use multiple tries, and set a timeout for each try. The engine will try again until the tries end:

In this example, each next try has more maximum time to execute:

let msTimeouts = [1200, 1400, 2800];
// $: is a special Svelte syntax for reactive declarations
$: options.setTimeouts(msTimeouts);

↑ you can never let your user wait again for an unknown reason.

7/9. Push the limits

We have the limit of 30 executions as a constant. But if you are curious, you can go beyond the limit.

import Engine from "@/Parallels/engine";

let limit = 30;
// The limit will work globally, for all modules
$: Engine.setConcurrency(limit);

You may try different GAS functions and services with a limit of 50 concurrent tasks. Some of them will timeout, but the total script works faster than with a limit of 50. The final goal is to make the user’s task as fast as possible but accomplish all the tasks. That’s why I came up with the 30 calls, more on that later.

8/9. Set the priority

Set the new task with high priority:

Use options for setting the priority. Here is the code for the priority:

import { Q } from "@/Parallels/q";
import { RunnerOptions } from "@/Parallels/options";

const blueOptions = new RunnerOptions();
const blueQ = new Q();

// ...
blueOptions.setHighPriority();
blueQ.run(blueOptions);

You may do your regular add-on tasks while the user does not perform actions. It may be loading and saving data. Often you need to delay all regular tasks. If the user clicks some button, you’ll have an urgent task to run first.

9/9. Compare the Speed

Finally, here’s why you should do that at all. Watch the speed difference between concurrent and sequential calls:

Here we read data from Spreadsheets. In my tests, this gives ×10 speed up.

Imagine 30 concurrent App Script executions each calling an API in a batch of 20 URL fetches.

🛠️Installation

Copy the spreadsheet to try the copy of the sidebar without installation:

See the GitHub project to play with the source. It contains instructions on local installation.

🥳More samples

The first try

My first attempt was to create a painting based on the spreadsheet data:

Data setup…

Painting squares programmatically…

Displaying the progress in real-time…

Testing the Limit

According to docs, we have 10 calls:

The google.script.run API allows 10 concurrent calls to server functions. If you make an 11th call while 10 are still running, the server function will be delayed until one of the 10 spots is freed.

This sample shows 20 calls:

Reading and writing sample:

The speed of this GIF is ×6

Cache Service managed a whopping 42 successful parallel writes!

Counting the number of tasks running at the same time. Later I found out this number is 30:

Loading it more, with 150 tasks, but limit concurrency by 30. The results are cool:

I’ve loaded it with 1000 tasks. The result became unstable, and functions timed out or refused. So I decided to stop on concurrency of 30 and found the same expected results here (Tanaike to the rescue!) and in quotas.

Bingo! Google has another nice limit for us: 30 simultaneous executions per user.

The speed drops every 25–35 tasks:

Running 150 tasks, concurrency: 30. Each task writes data to a different cell of the same sheet.

…After endless tests. …After rewriting the engine from scratch 2 times. Finally, the work was done!

Photo by Ying Ge on Unsplash

--

--