Google apps script slow parsing - google-apps-script

I was trying to parse vmstat using Google Apps script so that everyone in the company could use this to create a graph of the data. You can find my code here but this code is really slow. Is there something I can do to make this better or isn't Google Apps Script suitable for this? The problem the ammount of rows that needs to be processed. Any suggestions are welcome.
function doGet(){
var file = DriveApp.getFileById(id)
var docContent = file.getAs('application/octet-stream').getDataAsString();
var data = Charts.newDataTable()
.addColumn(Charts.ColumnType.STRING, 'TIME')
.addColumn(Charts.ColumnType.NUMBER, 'Memory');
var lines = docContent.split("\n");
Logger.log(lines.length);
var i = 1;
lines.forEach(function(line) {
if ((line.indexOf('mem') < 0) && (line.indexOf('free') < 0)) {
var values = line.match(/\S+/g);
data.addRow(['5',parseInt(values[3])]);
Logger.log(i)
}
if (i == 20){
return;
}
i++;
});
for( var i=0;i< lines.length;i++){
data.addRow(['5',10]);
}
data.build();
var chart = Charts.newAreaChart()
.setDataTable(data)
.setStacked()
.setRange(0, 400)
.setTitle('Memory')
.build();
return UiApp.createApplication().add(chart);
}

This isn't a problem of code optimization (although the code isn't perfect), as much as division of work.
The accepted approach to web application performance optimization involves separating three concerns; presentation, business logic and data accessref. With the exception of the generation of the vmstat output, you've got all of that in one place, making the user wait while you locate a file on Google Drive (using two exhaustive searches, btw) and then parse it into a Charts DataTable, and finally generate HTML (via UiApp).
You may find that the accessibility of a Google Apps Script presentation is useful to your organization. (I know in my workplace that our IT folks clamp down on in-house web servers, for example.) If so, consider what you have as prototype, and refactor it to give better perceived performance.
Presentation: Move from UiApp + Charts to HtmlService + Google Visualization. This moves the generation of the chart into the web client, instead of keeping it in the server. This will give a faster page load, to start.
Business Logic: This will be the rules that map your data into the Visualization. Like the Charts Service that is built over it, GViz uses DataTables with column definitions and rows of data.
One option here is to repeat the column definition & data load you already have, except on the client in JavaScript. Doing that will be significantly faster than via Google Apps Script.
A second option, which is even faster, especially with large datasets, is to load the data from an array.
google.visualization.arrayToDataTable(...)
Either way, you need to get your data to the JavaScript function that will build your chart.
Data Access: (I assume) you're currently running a shell script in Linux that calls vmstat and pipes the output to a file in your local Google Drive folder. (Alternatively, the script may be using the Drive API to push the file to Google Drive.) This file is plain text.
The change I'd make here would be to produce csv output from vmstat, and use Google Apps Script to import the csv into a spreadsheet. Then, you can use Sheet.getSheetValues() to read all the data in one shot, in a server side function to be called from the client JavaScript.
This would not be as fast as a local server solution, but it's probably the best way to do this using the Google Apps Script environment.
Edit: See more about this in my blog post, Converting from UiApp + Chart Service to Html Service + Google Visualization API.

Related

Google Apps script: Spreadsheet to Chart to Image

I've created a function to take spreadsheet data, bring it to GAS, and then send it out to a webhook so I can use Zapier to build an image/pdf/etc. I used others samples and help on Google to figure out how to get the range, bring it over, etc and that's here:
const range = "Prior!H20:K23";
const [header, ...values] = SpreadsheetApp.getActiveSheet()
.getRange(range)
.getDisplayValues();
const table = Charts.newDataTable();
header.forEach((e) => table.addColumn(Charts.ColumnType.STRING, e));
values.forEach((e) => table.addRow(e));
const blob = Charts.newTableChart()
.setDataTable(table.build())
.setDimensions(500, 150)
.setOption("alternatingRowStyle", false)
.build()
.getBlob();
I then send the blob out, etc. The problem is that I'm losing all of the formatting from the spreadsheet, and since what I really want to do is almost just take a screenshot of the range in question, it's a problem. Specifically I'm losing text attributes, coloring, and the merging of cells.
Any ideas of how to do this, or, where in here I could modify/bring over the formatting?
There is no quick way to get a spreadsheet range with formatting through the SpreadsheetApp API.
See Tanaike's RichTextApp for one pretty simple method of getting formatted text content in a range of cells. The library that uses a temporary document to get the formatting.
To get a chart, complete with formatting, try .build().getAs('image/png') or 'image/svg' instead of .build().getBlob(). See Charts reference.
Alternatively, use Sheet.newChart() to get an EmbeddedTableChartBuilder instead of a TableChartBuilder.
You will need to extract formatting info from the sheet and feed it into the table builder
(Disclaimer, even by doing this you won't be able to do it with Apps Script, see below)
Especially if you use
.getDisplayValues();
Which will only get the values displayed from the sheet, no formatting information is taken from the sheet with this method.
You can use:
.getRichTextValues()
Though unfortunately a RichTextValue, is not recognized by the chart builder.
So the best thing is to break it up into a few steps, the first of which is using:
value.getTextStyle()
Which will then return a TextStyle object that has a few methods:
getFontFamily()
getFontSize()
getForegroundColor()
getForegroundColorObject()
isBold()
isItalic()
isStrikethrough()
isUnderline()
So now you can store the various formats you need to apply to your table.
However, the Apps Script Charts Service does not support this styling!
It seems that the Apps Script Chart Service does not support this type of styling. So maybe your only option is to open an HTML UI element via the UI service, or a Web app and then use the Google Charts service in the way demonstrated by the quickstart https://developers.google.com/chart/interactive/docs/quick_start.
You would need the setCell method.
This involves translating your styling to CSS, for example:
dataTable.setCell(22, 2, 15, 'Fifteen', {style: 'font-style:bold; font-size:22px;'});
// https://developers.google.com/chart/interactive/docs/reference#DataTable_setCell
File a feature request
You can always file a feature request for that by filling out this template:
https://issuetracker.google.com/issues/new?component=191640&template=823905
In the meantime though, you are probably best to get to grips with the JS library as it will always tend to be more complete than the Apps Script service that wraps it.
That, or build an HTML table without using Google Charts, then you could potentially use the HtmlService to pass it around in that format.
References
RichTextValue
DataTableBuilder
TableChartBuilder
UI service
Web apps
Charts
HtmlService

Google Apps Script - Creating and Saving Filters for Google Sheets

I feel like a bit of a chump, but I cannot work this out...
I have been given the job of producing a new master analysis sheet each month from a supplied XML file that combines with various columns of our (multiple) sheets. No problems, so far. I have got all of that working the way I want. :-)
My issue is that we also have about 6-8 filters saved with a specific sheet that allow our auditors to focus on specific areas (and as you can understand, our auditors want these to work EXACTLY as they specify).
I have tried using createFilter() but there doesn't appear any way to save multiple filters to that sheet (maybe I am missing something). No joy! :-(
I have tried recording a macro which I could then run to create the filters. No joy here either :-(
Do I have to tell these pesky auditors to create there own filters each month (they do know how, but it's beneath them), or is there a way I can script them up and get them off my back?
Unfortunately (as much as I would like to) I cannot share our sheets or scripts as we have significant IP embedded there.
I would really appreciate some guidance as to how you might approach this (if it is possible).
Kind regards
Ian
If you're indeed talking about the 'Create new filter view', I suggest making an template sheet. So instead of creating a new sheet every month, make one template spreadsheet and add all the filter views your auditors desire. Then copy that spreadsheet, and paste the new data in it.
The correct way to create a filter using Apps Script and the createFilter() is this one:
function setFilters() {
var ss = SpreadsheetApp.getActiveSheet();
var rangeFilter = ss.getRange("INPUT_YOUR_RANGE_HERE");
var filter = rangeFilter1.createFilter();
var filterCriteria = SpreadsheetApp.newFilterCriteria();
filterCriteria.ADD_YOUR_CRITERIA_HERE;
filter.setColumnFilterCriteria(columnPosition, filterCriteria.build());
}
As you can see, you must use build() in order to build the criteria for the filter you have created.
You can also use the Sheets advanced services and create the filters using the Sheets API, something similar to this:
var filterSettings = {
//YOUR FILTER SETTINGS
};
var request = [{
"setBasicFilter": {
"filter": filterSettings
}
}];
And as for calling the Sheets service and applying the above filter, you can use this:
Sheets.Spreadsheets.batchUpdate({'requests': request}, SPREADSHEET_ID);
Reference
Range Class Apps Script createFilter();
Filter Class Apps Script;
Apps Script Google Advanced Services.

Timeout errors with Google Slides API, and terrible write performance

I'm appending dozens of slides to presentations with a simple app script :
var presentation_to = SlidesApp.openById(presentation_to_id);
var presentation_from = SlidesApp.openById(presentation_from_id);
var slide = presentation_from.getSlideById(slide_id);
var newSlide = presentation_to.appendSlide(slide);
I have an app script API endpoint for that.
I started with one app script that would loop through all the slides IDs, but this had terrible performance and would timeout after 5 minutes. I've split my calls to the API app script to ask for one slide at a time, with parallelization (so I run several request to add a slide to the same presentation at once).
When the slides have big pictures in them, I still end up getting this :
Google::Apis::TransmissionError: execution expired
Is appendSlide() performance so bad what I want to do is not possible, or is there a way for me to make it work without having to wait 1h to generate one 50 slides presentation ?
PS : You'll find attached the logs of the script. Each line is meant to append ONE slide to a presentation (always the same destination). The execution times and error rates are just through the roof. Is performance simply limited by Google or is there a way to bypass this issue ?
Considerations
Using Apps Script SlideApp built-in Class you are basically creating an API call every time you run .appendSlide() method. This will cause big network overhead when your script inserts a lot of slides.
Generally this is solved by using batch requests via Advanced Google Services.
Unfortunately, there is no method to create a Page copy request that you could insert in a batch operation. If this is really important for your workflow you should consider filing a Feature Request
Reference
Slides API

Google Sheets Script Timeout Workaround

I'm building a script that uses a looping ImportHTML command to web scrape weather data based on zip code, and am currently running into an issue with the execution timing out every time the script is run.
The current way I have the script set up produces a correct result when run, but given that the script is pulling data from several hundred sources, it is taking a while and will not complete within the current time limit of Google scripts.
The sheet running the script utilizes 3 tabs:
ZIPS, which contains a list of zip code link values pulled from the
site that weather data is to be pulled from
Blank, which is simply an intermediary sheet used in the execution of the script
Result, where the final output is to be placed
In order to try and reduce the amount of read/write as much as possible, I changed the code from writing each result of the ImportHTML command as it was executed to appending to an array and writing this array at the end of the script. The code in it's current form is as follows:
function getTemps() {
var googleSheet = SpreadsheetApp.getActive();
// Read in Zip code link values
var sheet = googleSheet.getSheetByName('ZIPS');
var zipArray = sheet.getDataRange().getValues();
var arrayLength = zipArray.length;
//Set up sheet values
var blankSyntaxA = 'ImportHtml("https://www.wunderground.com/cgi-bin/findweather/getForecast?query=pz:';
var blankSyntaxB = '&zip=1", "table", 1)';
var tempResult = [];
// Writing Section
var sheet = googleSheet.getSheetByName('Blank');
for (var i = 0; i < arrayLength; i++)
{
var liveSyntax = blankSyntaxA+zipArray[i][0]+blankSyntaxB;
sheet.getRange('A1').setFormula(liveSyntax);
var importedData = sheet.getDataRange().getValues();
tempResult = tempResult.concat(importedData);
}
var sheet = googleSheet.getSheetByName('Result');
sheet.getRange(1,1,tempResult.length,8).setValues(tempResult);
}
I know the run time of the script could be reduced by eliminating the read/write contained within the For loop, but I'm not sure how to obtain the necessary HTML table without running the ImportHTML command within the 'Blank' sheet. Is there a way to run that command to fill the 'importedData' array without writing to a sheet?
Alternatively, I had considered utilizing a check on the runtime of the function and implementing a break as it neared the ~5 minute runtime limit, followed by a recursive call back to the original function, but I wasn't sure if this would actually mitigate the runtime issue, or even be possible given the nature of the recursive call.
Any advice on how this script could be modified to run within the script timeout parameter or modified to produce the complete desired outcome with all the necessary imported data would be appreciated. Thanks!
Is there a way to run that command to fill the 'importedData' array without writing to a sheet?
IMPORTHMTL is a Google Sheets built-in spreadsheet function. This kind of functions can't be ran / evaluated by Google Apps Script.
Related
How to evaluate a spreadsheet formula within a custom function?
Alternatively, I had considered utilizing a check on the runtime of the function and implementing a break as it neared the ~5 minute runtime limit, followed by a recursive call back to the original function, but I wasn't sure if this would actually mitigate the runtime issue, or even be possible given the nature of the recursive call.
Rather than a "mitigator" this is a workaround. There are several techniques like batch processing and parallel processing.
Reference
Exceeded maximum execution time in Google Apps Script
From answer to Threading in Google App Script
There is a great example from Bruce
Mcphearson. His example
Parallel Processing in Apps
Script uses
Map Reduce in exercise. He is utilizing triggers as well, but perhaps
may provide some different perspective.
Another alternative is to sign in to the Early Access Program to extend the execution time limit to 30 minutes.

How to back up a Google Apps Script's scriptdb

If I have a mission critical db, that needs to be regularly backed up, and I store it as a scriptdb in GAS, is there any way to back up the actual database file? It seems the db is embedded in a way that makes it invisible outside of scripts?
Well, you can always query all your values and JSON.stringify them.
If you ever need to restore a database you from this, the only difference I can notice is that each item id will change.
Here is an example:
function backupDB() {
var db = ScriptDb.getMyDb();
var res = db.query({});
var array = [];
while( res.hasNext() )
array.push(res.next().toJson());
var dbString = JSON.stringify(array);
Logger.log(dbString); //you'll obviously save this string somewhere else e.g. as a docs file
}
You may also need to do this in chunks, as your db may have too much data for the script to handle at once, like this.
I also feel that this "backup" procedure should be taken care of by the API as well. The code above is just an idea I just had.
I think I found decent soluton for my question above, in an unexpected place. Rather than use a scriptdb, I can use Google Fusion Table/s - these have SQL-type access, are concrete docs that can be exported, backed up, viewed etc, and can act as the data store for my app...
The actual answer is: you do not store mission critical data on a scriptdb, for many reasons:
appscript does not have SLA. Google has many other storages that do have guarantees.
Because that db does not support transactions you will not be able to guarantee that a batch process might process twice the same data (in cases where the script fails in the middle of a chunk backup or restore).
It will get complex if you store the ids inside other objects in the db.
Maybe you can copy your critical data from the scriptdb to a Google Spreadsheet. Given its a example in Google developers I think it is an interesting option.
Here is the link: Copy a Database to a New Sheet in a Spreadsheet.