Measurement of execution time of built-in functions for Spreadsheet - google-apps-script

Are there methods to measure the execution time when built-in functions completed for Spreadsheet? When I use several built-in functions (For example, IMPORTHTML and IMPORTXML), if I know the average execution-time, it is easy for me to use and design data sheet.
I measure it of custom functions using this script.
function myFunction() {
var start = new Date();
// do something
var end = new Date();
var executiontime = end - start;
}
Thank you so much for your time and advices.

Unfortunately, there are not measurement tools for retrieving the execution time of built-in functions. This has already been commented by #Rubén. So I thought of about the workarounds. How about the following workaround?
Flow :
Import a value to a cell. The value is anything good, because this is used as a trigger. Please do this by yourself.
Custom functions cannot use setValue(). So I used onEdit().
func1() imports a formula that you want to measure the execution time by the script launched by the trigger.
At func2(), after set the formula, the measurement is started. The confirmation when built-in function was completed is carried out using loop.
By measuring the cost per one call for getValue(), it was found that that was about 0.0003 s. So I thought that this can be used.
The result of measurement can be seen at Stackdriver as milliseconds.
Sample script :
function func1(range, formula){
range.setFormula(formula);
}
function func2(range){
var d = range.getValue();
while (r == d) {
var r = range.getValue();
}
}
function onEdit(){
var formula = '### Built-in function ###'; // Please set the built-in function you want to measure the execution time.
var label = "Execution time for built-in functions.";
var ss = SpreadsheetApp.getActiveSheet();
var cell = ss.getActiveCell();
var range = ss.getRange(cell.getRow(), cell.getColumn());
func1(range, formula);
console.time(label);
func2(range);
console.timeEnd(label);
}
Note :
When built-in functions with very long time is measured, an error may occur at getValue().
In my environment, the built-in function for 10 seconds worked fine.
Updated at November 11, 2020:
As the additional information, I would like to add one more sample script for measuring the execution time of when built-in functions completed for Spreadsheet and the result using the script.
This is a simple sample script for measuring the process cost of functions on a cell. At first, in order to confirm whether this script can be used for measuring the process cost of the function put in a cell, a custom function was used. Because when the custom function is used, the process time of the script can be known by using Utilities.sleep(time).
Sample script:
When you test this script, please copy and paste the following script to the container-bound script of Google Spreadsheet. When you run the function of main(), the process cost of =SAMPLE(5000) can be obtained.
// This is a sample custom formula. This is used for testing.
function SAMPLE(time) {
Utilities.sleep(time);
return "ok";
}
// This is a script for measuring the process cost.
function main() {
const obj = { formula: `=SAMPLE(5000)`, returnValue: "ok" }; // Set formula and response value.
const label = "Execution time";
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
const range = sheet.getRange("A1");
range.clear();
// start --- measure
console.time(label);
range.setFormula(obj.formula);
SpreadsheetApp.flush();
while (range.getDisplayValue() != obj.returnValue) {}
console.timeEnd(label);
// end --- measure
range.clear();
}
In this sample, when =SAMPLE(5000) is put to a cell, the value of ok is shown in the cell after 5 seconds. main() measures the process time for this.
This sample script checks the output value from the function. So please set returnValue. Please be careful this.
At the formula of Spreadsheet, when the formula is put to the cell, the measurement of process time is started. So in this sample, I included setFormula and flush to the measured process cost.
Experimental result:
As an experiment, it shows the change of process time with increasing the sleep time of the custom function as above image. This result indicates that the process time is linearly increased with the increase in the sleep time. It was found that the process time was at least, more than the sleep time, and the process time was large in about 0.5 s for each sleep time as the offset. It is considered that this offset includes the process costs of setFormula, flush, getDisplayValue and the while loop. But, from this image, it is considered that when the process costs of various formulas are measured using above script, those can be compared by the relative comparison. And from this result, it is considered that above script can be used for measuring the execution time of the function in a cell of Spreadsheet.
Reference:
Benchmark: Measuring Process Costs for Formulas in Cells on Google Spreadsheet using Google Apps Script

Google Sheets doesn't include a built-in tool to measure the recalculation time.
One alternative is to use the Chrome Developers Tools Timeline but bear in mind that functions like IMPORTHTML and IMPORTXML are not recalculated every time that the spreadsheet does (reference Set a spreadsheet’s location and calculation settings).
Related Q&A
SO
Google spreadsheet constantly recalculates functions
Web Applications
What's causing / how to get rid of long web calls in Google Spreadsheets
Where does the calculation take place?

One can make a Named Function (just to have it easily reusable) to measure formula execution using only formulas, no scripts needed.
BENCHMARK()
BENCHMARK() (returns execution time in seconds):
=N(
REDUCE(
0,
{0; 1; 0},
LAMBDA(
acc,
cur,
IF(
cur = 0,
LAMBDA(x, x)(NOW()) - acc,
acc + 0 * ROWS(func())
)
)
) * 24 * 60 * 60
)
Its single parameter is func - a function to run and time its execution. You just get a working formula, enclose it in LAMBDA() and pass to BENCHMARK(). Examples are at the bottom.
BENCHMARKN()
BENCHMARKN():
=N(
REDUCE(
0,
{0; SEQUENCE(number); 0},
LAMBDA(
acc,
cur,
IF(
cur = 0,
LAMBDA(x, x)(NOW()) - acc,
acc + 0 * ROWS(func())
)
)
) * 24 * 60 * 60 / number
)
There's another parameter number: func will be executed in a loop number times. Then total execution time will be divided by number to get an average execution time.
UPD: enclosing NOW() in LAMBDA(x, x)(NOW()) freezes the value and prevents recalculation caused by any change in the sheet. Got it from here.
Examples
Say, we want to sum all squares of natural numbers from 1 to 1000000. So we have our formula:
=SUM(ARRAYFORMULA(SEQUENCE(1000000)))
We time it like so:
=BENCHMARK(LAMBDA(SUM(ARRAYFORMULA(SEQUENCE(1000000)^2))))
And in a 10-step loop like so:
=BENCHMARKN(LAMBDA(SUM(ARRAYFORMULA(SEQUENCE(1000000)^2))), 10)

Here's my option to caclulate the difference between ArrayFormulas:
Create a template with sample data
Set the script: change array with formulas, template Spreadsheet Id, cell address to set formulas, cell address to iterate values.
This function will test 2 or more formulas:
Copy sample file to isolate the influence of previous iterations.
Set formula and calculate the time to calculate it the first time
Edit source data in a loop and calculate time needed for the formula to recalculate.
Sample output, plotted in Sheets:
↑ formulas for the tests are taken from this question.
The y-axis is the processing time, and bars are the iterations of the script.
the low bars are the the times to insert the formula,
and higher bars are the times to re-calculate the formula when the value changed.
The Code:
function test_formulas_speed() {
/** Options ↓ ********************************************************************************************* */
// https://docs.google.com/spreadsheets/d/1vQu7hVr7FwH8H5N8JOlOGfvjlJgKtpfoM2DPPjgUaLo/template/preview
var testSpreadsheetId = '1vQu7hVr7FwH8H5N8JOlOGfvjlJgKtpfoM2DPPjgUaLo';
var formulas = [
'=INDEX(if(A2:A="",,LAMBDA(srt, SORT(SCAN(0,MAP(SEQUENCE(ROWS(A2:A)),LAMBDA(v,if(v=1,0,if(INDEX(srt,v,1)<>INDEX(srt,v-1,1),1,0)))),LAMBDA(ini,v,IF(v=1,1,ini+1))),index(srt,,2),1) ) (SORT({A2:A,SEQUENCE(ROWS(A2:A))}))))',
'=LAMBDA(a,INDEX(if(a="",,COUNTIFS(a,a,row(a),"<="&row(a)))))(A2:A)'
];
// value from the first cell of arrayformula ↓
var returnValue = 1;
// range to insert the formula
var rA1 = 'B2';
var sheetName = 'Sheet1';
var iterations = 5;
var testChangeRaangeA1 = 'A5';
var sub_iterations = 5; // the number of times to change the value
/** Options ↑ ********************************************************************************************* */
var results = [];
var file = DriveApp.getFileById(testSpreadsheetId);
var results = []
var testOne_ = function(formula, indx) {
// prepare
var copy = file.makeCopy();
var id = copy.getId();
var ss = SpreadsheetApp.openById(id);
var s = ss.getSheetByName(sheetName);
var r = s.getRange(rA1);
var rCh = s.getRange(testChangeRaangeA1);
var addToResult = function(t) {
var result = new Date() - t;
if (results[indx]) {
results[indx].push(result);
} else {
results[indx] = [result];
}
}
// measure time
var t = new Date();
r.setFormula(formula);
SpreadsheetApp.flush();
// loop until expected value is returned
while (r.getDisplayValue() != returnValue) {}
addToResult(t);
for (var i = 0; i < sub_iterations; i++) {
t = new Date();
// clear the cells, because of the internal cache
rCh.clearContent();
rCh.setValue(i);
SpreadsheetApp.flush();
addToResult(t);
}
// clean up
copy.setTrashed(true);
return 0;
}
for (var i = 0; i < iterations; i++) {
formulas.forEach(testOne_);
}
console.log(results);
}
Ref:
This benchmark by #Tanaikech

To add another option you can do:
function myFunction() {
console.time('someFunction');
// do something
console.timeEnd('someFunction');
}
And then look at the Stackdriver logs for the Function Execution.
Example output would be:
Jul 3, 2020, 1:03:00 AM Debug someFunction: 80ms

Related

How to reduce the latency between two script calls in Google Apps Script

In a self-developed add-on for Google Sheets, the functionality has been added that a sound file will be played from a JavaScript audio player in the sidebar, depending on the selection in the table. For the code itself see here.
When a line is selected in the table the corresponding sound file is played in the sidebar. Every time the next line is selected it takes around 2 seconds before the script will start to run and load the sound file into the sidebar. As the basic idea of the script is to quickly listen through long lists of sound files, it is crucial to reduce the waiting time as fare as possible.
A reproducible example is accessible here; Add-ons > 'play audio' (Google account necessary). To reproduce the error, the sheet has to be opened two times (e.g. in two browsers).
In order to reduce the latency you might try to reduce interval on your poll function as suggested by Cooper on a comment to the question and to change the getRecord function.
poll
At this time the interval is 2 seconds. Please bear in mind that reducing the interval too much might cause an error and also might have an important impact on the consume of the daily usage quotas. See https://developers.google.com/apps-script/guides/services/quotas
getRecord
Every time it runs it make multiple calls to Google Apps Script which are slow so you should look for a way to reduce the number of Google Apps Script calls. In order to do this you could store the spreadsheet table data in the client side code and only read it again if the data was changed.
NOTE: The Properties Service has a 50,000 daily usage quota for consumer accounts.
One way to quickly implement the above is to limit the getRecord function to read the current cell and add a button to reload the data from the table.
Function taken from the script bounded to the demo spreadsheet linked in the question.
function getRecord() {
var scriptProperties = PropertiesService.getScriptProperties();
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getDataRange().getValues();
var headers = data[0];
var rowNum = sheet.getActiveCell().getRow(); // Get currently selected row
var oldRowNum = scriptProperties.getProperty("selectedRow"); // Get previously selected row
if(rowNum == oldRowNum) { // Check if the was a row selection change
// Function returns the string "unchanged"
return "unchanged";
}
scriptProperties.setProperty("selectedRow", rowNum); // Update row index
if (rowNum > data.length) return [];
var record = [];
for (var col=0;col<headers.length;col++) {
var cellval = data[rowNum-1][col];
if (typeof cellval == "object") {
cellval = Utilities.formatDate(cellval, Session.getScriptTimeZone() , "M/d/yyyy");
}
record.push({ heading: headers[col],cellval:cellval });
}
return record;
}
Related
Problems when using a Google spreadsheet add-on by multiple users

Range#sort fails to sort based on new formula's values

I am extracting data from several Google Sheets into one main sheet using Google Apps Script. To convert dates to week-numbers I use the sheet function ISOWEEKNUM() for column 1.
I want to sort by week-numbers and because of headers I use Range#sort.
The problem I am having is that no sorting is performed when I test run getData(), the Range#sort call has no effect. If I first run getData() and then manually run my sortRange() function it works.
Is there something about writing cells with ISOWEEKNUM() in them and then try to sort directly in the same script execution? What can be done solve this without the user having to sort or start more scripts manually?
function getData()
{
var thisSpreadSheet = SpreadsheetApp.getActiveSpreadsheet();
var dataSheet = thisSpreadSheet.getSheetByName('Data');
var deliverySheets = listDeliverySheets();
var outputWeekAndTotal = [];
var outputCratesPerStore = [];
var i;
for(i = 0; i < deliverySheets.length; i++)
{
outputWeekAndTotal.push(["=ISOWEEKNUM(\""+deliverySheets[i].getRange("A2").getDisplayValue()+"\")", deliverySheets[i].getRange("L12").getValue()]);
outputCratesPerStore.push(deliverySheets[i].getRange("L5:L9").getValues());
}
dataSheet.getRange(2, 1, outputWeekAndTotal.length, outputWeekAndTotal[0].length)
.setValues(outputWeekAndTotal);
dataSheet.getRange(2, 3, outputCratesPerStore.length, outputCratesPerStore[0].length)
.setValues(outputCratesPerStore);
sortRange();
}
function sortRange()
{
var thisSpreadSheet = SpreadsheetApp.getActiveSpreadsheet();
var rangeToSort = thisSpreadSheet.getSheetByName('Data').getRange(2, 1, 7, 7); /*Constants used temporarily*/
rangeToSort.sort({column: 1, ascending: true});
}
The fundamental issue is that Google is free to (and does) optimize their interpretation of your code to avoid abuse of their servers. When you call functions that operate on different objects, Google doesn't always determine that the order is important, and thus may invoke certain API operations out of (your desired) order. You can help Google by chaining methods on the exact same object, but this is not always sufficient. Operations that cause side effects / asynchronous changes, such as writing a formula, or operate over different APIs - such as calling the Drive API/Service after using Form, Docs, or Sheets Service methods - may not be performed in order even if "chained."
To fix this, you must forcibly flush the write cache buffer. For the Spreadsheet Service, this is done via a call to SpreadsheetApp#flush.
...
dataSheet.getRange(...).setValues(...);
SpreadsheetApp.flush();
sortRange();
}
Flushing the write cache will force the written formulas to be calculated prior to executing the next script lines, making their values available to the sorting method.

Google sheet not updating custom function return value

I am very new to Google Apps Script (as well as JavaScript, for that matter), but I have been trying to tinker with it for fun.
I have tried writing a script to fetch API price data in Google Sheets, but am finding that the returned value is not updating when re-evaluating the script in the same cell.
Below is a script to fetch bitcoin price data from Coinbase's API. The script parses the JSON response of the request, as is described here.
function getBTCPrice() {
var url = "https://api.coinbase.com/v2/prices/BTC-USD/spot";
var response = UrlFetchApp.fetch(url);
var jsonSpotPrice = response.getContentText();
var parseSpotPrice = JSON.parse(jsonSpotPrice);
var price = "$" + parseSpotPrice.data.amount;
return price
}
Now, if I type =getBTCPrice() in some cell, and then re-evaluate a few moments later, I get the same price; however, if I evaluate the script in a different cell, I get a different result.
I've read some stuff about Google caching values in cells, so that perhaps the script isn't evaluated because the value of the cell has not changed. Is this the case here? If so, is there a workaround?
Any help is greatly appreciated!
I finally figured it out! Instead of trying to call the custom function from an actual sheet cell (which apparently stores cached values), the trick is to call the function within a script.
Using my above script:
function getBTCPrice(url) {
var response = UrlFetchApp.fetch(url);
var jsonSpotPrice = response.getContentText();
var parseSpotPrice = JSON.parse(jsonSpotPrice);
var price = "$" + parseSpotPrice.data.amount;
return price;
}
You can then call this function from another script. Specifically, I was looking to assign the updated price to a cell. Below is an example, which assigns the price to the active spreadsheet, in cell A1:
function updatePrice(){
var a = getBTCPrice("https://api.coinbase.com/v2/prices/BTC-USD/spot");
SpreadsheetApp.getActiveSpreadsheet().getRange('A1').setValue(a);
}
You can then proceed to set an appropriate time trigger. And that's all there is to it!
Have a look at this answer on Refresh data retrieved by a custom function in google spreadsheet.
As the answerer says, the trick is to
My solution was to add another parameter to my script, which I don't even use. Now, when you call the function with a parameter that is different than previous calls, it will have to rerun the script because the result for these parameters will not be in the cache.
Vik
In addition of Vikramaditya Gaonkar answer, you can use a installable trigger to get a refresh result each minute.
function getBTCPrice(input) {
url = "https://api.coinbase.com/v2/prices/BTC-USD/spot";
response = UrlFetchApp.fetch(url);
var jsonSpotPrice = response.getContentText();
var parseSpotPrice = JSON.parse(jsonSpotPrice);
var price = "$" + parseSpotPrice.data.amount;
return price
}
function up(){
SpreadsheetApp.getActiveSheet().getRange('A1').setValue(Math.random());
}
The parameter of getBTCPrice function is, in my case, cell A1 which is randomize each minute. For this, I create a installable trigger on up function
function up, time-driven, minute timer, every minute
I was also trying to make my custom function update, after searching I came up with the following function:
function updateFormulas() {
range = SpreadsheetApp.getActiveSpreadsheet().getDataRange();
formulas = range.getFormulas();
range.clear();
SpreadsheetApp.flush();
range.setValues(formulas);
}
The function above update all formulas of the spreadsheet. In my experience to make a custom function update I had to change its value, so I get all the data of the sheet, then I get the formulas and store them into a variable, then I clear their values and apply this change with "flush", finally I update the values I have just cleared with the formulas I have stored.
I created this function and in my case I have set the trigger for 1 minute to execute it, every minute all functions of the table are updated.
I hope this helps you.

Clock Trigger Builder Not calling function when scheduled - Google sheets app Script

I am using the app script provided by Google to access their prediction API through sheets. I am trying to predict thousands of rows at once, however, after 6 minutes the maximum execution time is reached at the code stops.
I implemented a solution that I found using clock trigger builder. Once I run the function it goes for 5 mins, then it stops sets a trigger to recall the function within 2 mins.
The major problem is that the function is not called when scheduled. I see it in the current triggers list, but it never gets called again. Can you please explain why this is occurring.
My intention is to predict as many lines as possible in 5 min then stop set a trigger to call the predict function again within a few minutes start where it left off and continue until ever element has been predicted.
I also need to know how would I store then values in cache so that it would know all the information that it needs when the function is called again.
//This is the function that is used to predict a selection of data
function predict() {
try {
clearOutput();
var startTime= (new Date()).getTime();
var sheet = SpreadsheetApp.getActiveSheet();
var selection = sheet.getActiveSelection();
var instances = selection.getValues();
var project_number = getProjectNumber();
var model_name = getModelName();
var startRow = stRow;
var MAX_RUNNING_TIME = 300000;
var REASONABLE_TIME_TO_WAIT = 60000;
for (var i = startRow; i < instances.length; ++i) {
var currTime = (new Date()).getTime();
if(currTime - startTime >= MAX_RUNNING_TIME) {
var builder = ScriptApp.newTrigger('predict').timeBased().after(REASONABLE_TIME_TO_WAIT);
builder.create();
break;
} else {
var result = predictSingleRow(project_number, model_name, instances[i]);
selection.getCell(i + 1, 1).setValue(result);
}
}
} catch(e) {
Browser.msgBox('ERROR:' + e, Browser.Buttons.OK);
}
}
Few things as to why your code is not functioning as intended:
1) Since you mentioned,"I see it in the current triggers list, but it never gets called again" and looking at your code, I am unsure whether you intended to call the function again after it's execution has completed. If you do, this is because your for loop runs for a while until the length of the instances is obtained. Nothing in the script suggests that the function needs to be run again once it has finished iterating through instances. Refer to this link to see how to Manage Trigger Programmatically.
2) var builder = ScriptApp.newTrigger('predict').timeBased().after(REASONABLE_TIME_TO_WAIT);
This line of your code falls under an if condition which stops the execution for 1 minute (value is 60000). Hence, adding 1 minute to the time since execution started. Nowhere are you resetting the startTime counter to the time after the waiting time since once the value of currTime - startTime has exceeded MAX_RUNNING_TIME, the function will keep calling the if loop for all iterations of the for loop after that. Simply put, if startTime was 9:35 and currTime was 9:40, after waiting for 1 minute the currTime is 9:41 which is still more than the MAX_RUNNING_TIME(5 minutes) because value of startTime still remains 9:35. Resetting it to 9:41 at this point should resolve your problem.
3) Loosing the break in the if loop would probably help fix that as well.
EDIT:
Add a function as shown in the link I mentioned above:
function callTrigger(){
ScriptApp.newTrigger('predict')
.timeBased()
.everyMinutes(30)
.create();
}
Run the function callTrigger once from your editor and you should be good to go. Remember, for minutes you can only pass values 1,5,15 or 30.

Google Spreadsheets: Script to check for completion of ImportHTML

I am trying to scrape data of a website once day automatically. In Google Spreadsheets, i use the =ImportHTML() function to import data tables, and then I extract the relevant data with a =query(). These functions take between 10 and 30 seconds to complete calculation, every time I open the spreadsheet.
I use a scheduled Google Apps Script, to copy the data into a different sheet (where it is stored, so i can run statistics) every day.
My problem is that I am having trouble to make the script wait for the calculations to be finished, before the data is copied. The Result is that my script just copies the error Message "N/A".
I tried just adding a Utilities.sleep(60000);, but it didn't work.
Is it possible to create a loop, that checks for the calculation to finish? I tried this without success:
function checkForError() {
var spreadsheet = SpreadsheetApp.getActive();
var source = spreadsheet.getRange ("Today!A1");
if (source = "N/A") {
Utilities.sleep(6000);
checkForError();
} else {
moveValuesOnly();
}
}
Locks are for this. Look up lock services in the docs. Use a public lock.
Here's how I used Zig's suggestion (combined with my own check loop) to solve my similar problem:
// Get lock for public shared resourse
var lock = LockService.getPublicLock();
// Wait for up to 120 seconds for other processes to finish.
lock.waitLock(120000);
// Load my values below
// something like sheet.getRange("A1").setFormula('= etc...
// Now force script to wait until cell D55 set to false (0) before
// performing copy / pastes
var current = SpreadsheetApp.setActiveSheet(sheet.getSheets()[1]);
var ready = 1;
var count = 0;
while (true) {
// break out of function if D55 value has changed to zero or counter
// has hit 250
if (count >= 250) break;
// otherwise keep counting...
ready = current.getRange("D55").getValue();
if (ready == 0) {count = 400;}
Utilities.sleep(100);
++count;
}
// wait for spreadsheet to finish... sigh...
Utilities.sleep(200);
// Do my copy and pastes stuff here
// for example sheet.getRange("a1:b1").copyTo(sheet.getRange("a3"), {contentsOnly:true});
// Key cells are updated so release the lock so that other processes can continue.
lock.releaseLock();
// end script
return;
}
This has worked fantastic for me, stopped Google's sporadic service from ruining my work!
Thanks goes to Zig's suggestion!