How to reduce execution time of this Google Apps Script? - performance

I wrote a script that gets a rows data from a spreadsheet and loops through them, calling a function to send an SMS if the rows' data meets certain conditions (having a phone number and not having already been sent for example).
However after adding about 600 rows, the script execution time exceeds it's limit, that seems to be 5 minutes according to my research. I'm using JavaScript objects to read data and a for loop to iterate through the rows.
Can anyone tel me if it is possible to make it faster? I'm very new to programming but this seems such a light task for all this computing power that I can't understand why it takes so long
Thanks in advance!
Here's the code of the function I'm using:
// Will send SMS on the currently active sheet
function sendSms() {
// Use the send sms menu to trigger reconcile
var user = ScriptProperties.getProperty(PROPERTY_USER_RECONCILE);
if (user == null)
reconcileUser();
// The sheets
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Registo");
var settingsSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Settings");
// Fetch values for each row in the Range.
var startRow = 2;
var apiKey = settingsSheet.getRange("B2").getValue();
var apiSecret = settingsSheet.getRange("B3").getValue();
var prefix = settingsSheet.getRange("B4").getValue();
var numRows = sheet.getMaxRows() - 1;
var numCols = 16;
var statusColNum = 15; // IMPT: To keep track status in col 15
var dataRange = sheet.getRange(startRow, 1, numRows, numCols);
// Make sure there is API key and secret
if (apiKey == "" || apiSecret == "") {
Browser.msgBox("You MUST fill in your API key and secret in Settings sheet first!");
return;
}
// Create one JavaScript object per row of data.
var objects = getRowsData(sheet, dataRange);
var totalSent = 0;
for (var i = 0; i < objects.length; ++i) {
// Get a row object
var rowData = objects[i];
var ss = SpreadsheetApp.getActiveSpreadsheet();
var templateSheet = ss.getSheetByName("SMS Modelo");
var template = templateSheet.getRange("A1").getValue();
// jump loop iteration if conditions not satisied
if (rowData.resolv == "x" || rowData.contactoUtente == null || rowData.contactoUtente == "" || rowData.reserv == null || rowData.reserv == "" || rowData.cont == "x" || rowData.sms !== null) continue;
var message = fillInTemplateFromObject(template, rowData);
var senderName = "Farm Cunha"
var mobile = rowData.contactoUtente;
// Send via Nexmo API
var response = nexmoSendSms(apiKey, apiSecret,"+351" + mobile, message, senderName);
if (response.getResponseCode() == 200) {
var object = JSON.parse(response.getContentText());
if (object.messages[0]['status'] == "0") {
// Set to QUEUE status - We assumed SENT, as we don't handle delivery status.
//sheet.getRange(startRow + i, statusColNum).setValue(STATUS_QUEUE);
sheet.getRange(startRow + i, statusColNum).setValue(STATUS_SENT);
// Set the reference id
sheet.getRange(startRow + i, 19).setValue(object.messages[0]['message-id']);
// sheet.getRange(startRow + i, statusColNum+3).setValue(new Date()); linha pode ser activada para fazer timestamp do envio
totalSent++;
}
else {
// If status is not 0, then it is an error.
// Set status to the error text
sheet.getRange(startRow + i, statusColNum).setValue(object.messages[0]['error-text']);
}
}
else {
// Non 200 OK response
sheet.getRange(startRow + i, statusColNum).setValue("Error Response Code: " + response.getResponseCode);
}
SpreadsheetApp.flush();
// Need a wait. Need to throttle else will have "Route Busy" error.
Utilities.sleep(2000);
}
// Update total sent
var lastTotalSent = parseInt(ScriptProperties.getProperty(PROPERTY_SMS_SENT_FOR_RECONCILE));
if (isNaN(lastTotalSent)) lastTotalSent = 0;
ScriptProperties.setProperty(PROPERTY_SMS_SENT_FOR_RECONCILE, (lastTotalSent + totalSent).toString());
Logger.log("Last sent: " + lastTotalSent + " now sent: " + totalSent);
reconcileApp();
}

You have a few things in your loop that are too time consuming : spreadsheet readings and API calls + 2 seconds sleep !.
I would obviously advise you to take these out of the loop (specially the template sheet reading that is always the same!). A possible solution would be to check the conditions from the row objects and to save the valid entries in an array... THEN iterate in this array to call the API.
If this is still too long then proceed by small batches, saving the end position of the partial iteration in scriptproperties and using a timer trigger that will continue the process every 5 minutes until it is completed (and kill the trigger at the end).
There are a few example of this kind of "mechanics" on this forum, one recent example I suggested is here (it's more like a draft but the idea is there)

Ok, I've solved it by taking these 3 lines out of the loop as Serge (thanks) had told me to:
var ss = SpreadsheetApp.getActiveSpreadsheet();
var templateSheet = ss.getSheetByName("SMS Modelo");
var template = templateSheet.getRange("A1").getValue();
It's so simple that I don't know how I was not seeing that.
This simple change made the script much faster. For example, going through 600 rows would take more than 5 minutes. Now, more than 5000 rows only take seconds.

Related

Add a status to each proccesed row in for loop that has an if statement with Google App Script

I've got a for loop in App script that is looking only at rows that have data in two columns. I'd like to set a status on each row that is actually processed, but the statuses get added to the wrong rows. When I add to i it adds to the whole length of the array, so I guess I shouldn't be trying to process each row, what am I doing wrong?
function auditReport() {
var sheetname = "Sheet1"; // name of data sheet ex. Form Responses 1
var colstoworkon = 10; // how many cols are filled with data f.e. by a form
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.setActiveSheet(ss.getSheetByName(sheetname));
var sheet = ss.getSheetByName(sheetname);
var data = sheet.getRange(3,1,sheet.getLastRow()-1,colstoworkon).getValues(); // starting with row 2 and column 1 as our upper-left most column,
//This makes it loops continuously and checks all not done rows
for (var i in data) {
if(data[i][1] && data[i][2]){//if email or copy are undefined just skip
var setStatus = sheet.getRange(i,4).setValue("done")
} // end of if
} // End of Loop
} //End of email function
Modification points:
In your script, from getRange(3,1,sheet.getLastRow()-1,colstoworkon), in this case, it is required to be getRange(3,1,sheet.getLastRow()-2,colstoworkon).
In the case of for (var i in data) {, i is the string type.
When you want to use sheet.getRange(i,4).setValue("done"), it is required to be sheet.getRange(Number(i) + 3, 4).setValue("done").
I thought that this might be the reason of your issue of but the statuses get added to the wrong rows..
In the case of if (data[i][1] && data[i][2]) {, if the value is 0, data[i][1] && data[i][2] is false.
When these points are reflected to your script, it becomes as follows.
Modified script:
function auditReport() {
var sheetname = "Sheet1";
var colstoworkon = 10;
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.setActiveSheet(ss.getSheetByName(sheetname));
var sheet = ss.getSheetByName(sheetname);
var data = sheet.getRange(3, 1, sheet.getLastRow() - 2, colstoworkon).getDisplayValues();
for (var i in data) {
if (data[i][1] && data[i][2]) {
var setStatus = sheet.getRange(Number(i) + 3, 4).setValue("done");
}
}
}
Or, your script can be also modified as follows. In this modification, done is put using the range list. By this, the process cost can be reduced a little.
function auditReport() {
var sheetname = "Sheet1";
var colstoworkon = 10;
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.setActiveSheet(ss.getSheetByName(sheetname));
var sheet = ss.getSheetByName(sheetname);
var data = sheet.getRange(3, 1, sheet.getLastRow() - 2, colstoworkon).getDisplayValues();
var ranges = data.map(([,b,c], i) => b && c ? `D${i + 3}` : "").filter(String);
if (ranges.length == 0) return;
sheet.getRangeList(ranges).setValue("done");
}
References:
for...in
getRangeList(a1Notations)

GSheet Script: How to Optimize my Row and Sheet Iterator

Long story short, I have this bit of Google Script that clears content automatically in a GSheet. It is set on a trigger and it works...the code does what it's supposed to do. The issue is that it runs slow. It takes 2 to 3 minutes for the iterator to run. To help you scope the size of the task: there is 150 rows on each of the 8 sheets.
The objective of the code is to clear a number of rows on each sheet based on the value of the cell in the first column of a row.
So I would like to know if anyone has any insight or suggestion to improve the running time. I understand my method of using a for loop checks rows one by one, and that's a time-consuming task. I couldn't think of an alternate method with arrays or something?
Thanks all!
Here's the code:
function Reset_Button() {
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i = 1; i < sheets.length ; i++ ) {
var sheet = sheets[i];
sheet.getRange("C2").setValue(new Date());
var rangeData = sheet.getDataRange();
var lastRow = rangeData.getLastRow();
var searchRange = sheet.getRange(1,1, lastRow, 1);
for ( j = 1 ; j < lastRow ; j++){
var value = sheet.getRange(j,1).getValue()
if(value === 0){
sheet.getRange(j,2,1,5).clearContent()
}}}}
Typically you want to do as few writes to the spreadsheet as possible. Currently your code goes through each line and edits it if necessary. Instead get the entire data range you will be working with into one variable (let's say dRange and use .getValues() to get a 2d array of all the values into a second variable (let's say dValues). Then simply iterate over dValues, setting a blank "" in each you want to clear. Once you are done going over all values, just do a dRange.setValues(dValues) (that's why I said to keep the range in a separate variable). So as an example, the following will clear columns B through F if column A has a 0
function test(){
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i = 1; i <sheets.length; i++) {
sheets[i].getRange("C2").setValue(new Date());
var dRange = sheets[i].getDataRange();
var dValues = dRange.getValues();
for (var j = 1; j < dRange.getLastRow(); j++){
if (dValues[j][0] == 0) {
for (var c = 1; c < 6; c++) {
dValues[j][c] = ""
}
}
}
dRange.setValues(dValues);
}
}
For a single sheet of ~170 rows this takes a few seconds. One thing to note is that I wrote it based on your script, you set a date value in C2 however in your sript (and thus in the one I wrote based on yours) that falls within the range you are checking to be cleared, so double check your ranges

Why is this getting the same row twice in second loop?

The code below runs with no apparent errors, but despite I've looked for all possible causes I could think of, I couldn't find the reason why it gets the correct row iterated, marks it as processed ("Sim), and from the 2º iteration on, it gets the new row, but repeats the others already iterated over.
function formToData() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var ss = sheet.getSheetByName("Form Responses 1");
var targetSheet = sheet.getSheetByName("Cadastro de Cliente");
var StartRow = 2;
var RowRange = ss.getLastRow() - StartRow + 1;
var WholeRange = ss.getRange(StartRow, 1, RowRange, 30);
var AllValues = WholeRange.getValues();
var message = "";
for (var i = 0; i < AllValues.length; i++) {
var currentRow = AllValues[i];
//if row has been sent, then continue to next iteration
if (currentRow[0] != "" && currentRow[29] != "Sim") {
//set the row to look at
var setRow = parseInt(i) + StartRow;
var data = currentRow[0];
var dataFormatted = Utilities.formatDate(data, SpreadsheetApp.getActive().getSpreadsheetTimeZone(), "dd/MM/yyyy', às 'HH:mm") + "hs";
//set HTML template for information
message +=
"<p><b>Data: </b>" + dataFormatted + "</p>" +
"<p><b>Unidade: </b>" + currentRow[1] + "</p>"
//mark row as "sent"
ss.getRange(setRow, 30).setValue("Sim");
var values = targetSheet.getRange("A:A").getValues();
var maxIndex = values.reduce(function (maxIndex, row, index) {
return row[0] === "" ? maxIndex : index;
}, 0);
targetSheet.getRange(maxIndex + 2, 1, 1, 30)
.setNumberFormat("#")
.setValues([currentRow]);
var sendTo = "email";
var subject = "Cadastro de cliente novo";
if (message) {
MailApp.sendEmail({
to: sendTo,
subject: subject,
name: "Comercial - Emape",
htmlBody: message,
});
}
}
}//For loop closes
}
I'd appreaciate if you could help me find the flaw.
I tried to replicate this behavior, however, on a static sheet the script performs as expected:
Set unmarked rows in column 30 as 'Sim'.
Copy these rows to a separate sheet starting from the first empty row (or whichever row that Column A is empty).
Given the names of the functions and the sheets, this may be a Sheet generated by a Google Form. These sheets are dynamic, and there is a possibility that the contents may change while your script is running, especially if users are allowed to edit responses.
As a workaround, I suggest to lock the form before running the script:
Also, check the contents of the sheet and form for any possibility that the "Sim" mark might be overwritten by new or edited form data, maybe the sheet is inserting 30 columns instead of 29 or less.

Apps Script Exceeded MAXIMUM_RUNNING_TIME Workaround

First of all, I am continuing an old thread at this link that I am unable to comment on due to being a newbie.
I have a situation that an answer in that thread given by user Br. Sayan would really improve my Spreadsheet Google App Script. I am making calls to Google Url Shortener API, which puts quotas at 1 call per user per second. I have slowed my script down enough to accommodate this quota, but I then I run over the MAX_RUNNING_TIME for App Scripts execution due to the extended number of calls I need to make, so I need to break the loop when the execution time is exceeded and pick up where I left off.
Here is the code of his answer:
function runMe() {
var startTime= (new Date()).getTime();
//do some work here
var scriptProperties = PropertiesService.getScriptProperties();
var startRow= scriptProperties.getProperty('start_row');
for(var ii = startRow; ii <= size; ii++) {
var currTime = (new Date()).getTime();
if(currTime - startTime >= MAX_RUNNING_TIME) {
scriptProperties.setProperty("start_row", ii);
ScriptApp.newTrigger("runMe")
.timeBased()
.at(new Date(currTime+REASONABLE_TIME_TO_WAIT))
.create();
break;
} else {
doSomeWork();
}
}
//do some more work here
}
My Questions:
Is MAX_RUNNING_TIME a global variable with a value set by Apps Script that I can leave that reference as-is, or must I replace it with a value equalling the 6 minutes listed as the quota for run time on the Google API Console?
How can I place the bulk of my function within this script so that a loop that runs inside my function (say var i = 0; i < data.length; i++) will be synchronized with the loop in the portion given in the above code?
Clarification: when i is incremented up by 1, I need ii to increment by 1.
Does this happen automatically? Do I need one loop nested inside the other? Does the bulk of my function go in the first '//do some work here' or the second '//do some work here' or possibly even doSomeWork()?
#tehhowch agreed! However, HOW I need to adapt my code depends on where I need to put it in the above snippet.
Here is what I have so far:
'function short() {
var = startTime = (new Date()).getTime();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var run = 0;
var finc = 50;
var istart = run * finc;
var iLen = (run + 1) * finc;
var startRow = 2 + istart;
var endRow = startRow + finc;
var data = sheet.getSheetValues(startRow,2,endRow,1);
var shortUrl = new Array();
for (var i=istart; i < iLen; i++) {
Utilities.sleep(1100);
var url = UrlShortener.Url.insert({longUrl: data[i][0]});
shortUrl.push([url.id]);
Logger.log([url.id]);
}
var t = ss.setActiveSheet(ss.getSheets()[0]);
t.getRange(startRow,4,finc,data[0].length).clearContent();
t.getRange(startRow,4,finc,data[0].length).setValues(shortUrl);'
So if I update the code after each subsequent run to manually increase the variable 'run' by 1, and manually run the code again, this works.
I have also tried break it down into multiple functions by updating the i= and i < parts for each subsequent function, which also works, but requires much more manual work.
I have also tried, unsuccessfully, to use a prompt with a button press that continues the function, which would be better than the other attempts, but would still require a button press to resume the code after each run.
I want to automate the function as much as possible.

Google spreadsheet script very slow

I've got a script that i run each day. it's incredibly slow and i can't figure out what's slowing it down. Anyone have any suggestions? I originally wrote a function to do one sheet, then i added a function to loop and call it on every sheet.
function addAllLog() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var allSheets = ss.getSheets();
// build array of all sheets
for (var i in allSheets) {
if (i == 0) { continue;} // skip first sheet
var tmpName = allSheets[i].getName();
if (tmpName == "Print Sheet") break; //stop at this tab
var tmpName = ss.getSheetByName(allSheets[i].getName());
ss.setActiveSheet(ss.getSheetByName(allSheets[i].getName()));
addLog();
}
}
function addLog () {
var sheet = SpreadsheetApp.getActiveSheet();
var numLastRow = sheet.getLastRow();
var numThisRow = numLastRow+1;
today = new Date();
today.setDate(today.getDate()-1);
today = Utilities.formatDate(today,"EDT","MM/DD/YYYY");
if (sheet.getMaxRows() == numLastRow) sheet.insertRowAfter(numLastRow);
var range = sheet.getRange(numLastRow,2,1,16); // last row, column b, 1 row of 16 columns
var data = range.getValues();
range.copyTo(sheet.getRange(numLastRow+1, 2,1,16)); //you will need to define the size of the copied data see getRange()
sheet.getRange(numThisRow, 1).setValue(today);
sheet.getRange(numThisRow, 4).setValue('Log');
sheet.getRange(numThisRow, 5).setValue(''); //erase copied value if there was anything there
sheet.getRange(numThisRow, 7).setValue(''); //erase copied value if there was anything there
sheet.getRange(numThisRow, 6).setValue('=GoogleFinance(C' + numThisRow + ',"price")');
sheet.getRange(numThisRow, 6).setValues(sheet.getRange(numThisRow,6).getValues());
}
Any suggestions on speeding it up would be greatly appreciated!
Each time you set a value in a range directly over the Spreadsheet, it takes at least 800ms to do it, so the best way to face this problem is create an Object[][] with the values and set it with sheet.getRange(range).setValues(Object[][]), being sure that Object[][] has the same width-height of the range.
In this post, Best Practices | Apps Script, Google explains the difference.
Also, an interesting solution is make an Spreadsheet abstraction in memory. I make my own MemsheetApp, based in this approach and it works very nice. I'm receiving pull request if you want to improve it.
This version doesn't require that you activate each sheet so it may take a little less time.
function addAllLog() {
var ss=SpreadsheetApp.getActive();
var allSheets = ss.getSheets();
for (var i=1;i<allSheets.length;i++){
var shtname = allSheets[i].getName();
if (shtname == "Print Sheet"){
break; //stop at this tab
}else{
addLog(shtname);
}
}
}
function addLog (shtname) {
var sheet = SpreadsheetApp.getSheetByName(shtname);
var numLastRow = sheet.getLastRow();
var numThisRow = numLastRow+1;
today = new Date();
today.setDate(today.getDate()-1);
today = Utilities.formatDate(today,"EDT","MM/DD/YYYY");
if (sheet.getMaxRows() == numLastRow) sheet.insertRowAfter(numLastRow);
var range = sheet.getRange(numLastRow,2,1,16);
range.copyTo(sheet.getRange(numLastRow+1, 2,1,16));
sheet.getRange(numThisRow, 1).setValue(today);
sheet.getRange(numThisRow, 4).setValue('Log');
sheet.getRange(numThisRow, 5).setValue('');
sheet.getRange(numThisRow, 7).setValue('');
sheet.getRange(numThisRow, 6).setFormula('=GoogleFinance(C' + numThisRow + ',"price")');
}
It appears to be Google's fault. Even SpreadsheetApp.getSheetByName() is bottlenecking the script by a minute or so, in my tests.

Resources