One of the concerns I have about add-ons is that the there is no context maintained between invocations. It’s the same thing with custom functions. What that means is that the document or spreadsheet structure needs to be rebuilt each time, using something like.

var doc = DocumentApp.openById(id);

In a normal Apps Script function, the document object persists throughout the function so there no need to keep opening it. With an add-on, you have to open it each time. With a several hundred page document this can take minutes – performance like this is simply not viable.  I decided to look into it to see if I could figure out what was going on.  

The test

Performing repeated actions on a growing document and timing how long each of them take to see if there is an inflection point at which the performance suddenly degrades and if so then what is it. Running the tests from an HtmlService and comparing it against a baseline running the same thing with a persisted document should provide some insight.  

The results

At first I was a little disappointed, because I noticed that the time to open a document was increasing proportionately to the growing size of the document (and in any case was under a second). It wasn’t increasing exponentially like the overall run time – so it wasn’t going to be that easy to track down what was causing this.

Screenshot 2015-10-05 at 15.49.17

Delay

I noticed that there was an exponential delay between request and the open action.
Screenshot 2015-10-05 at 16.01.58
But the delay was between the end of the previous cycle and before the document open start. In other words, the request to open the document was taking longer to ‘be received’, but it wasn’t clear what was happening during that delay – which could be close to a minute for a document with 1mb content. 

Saving and closing.

My first thought was that saving and closing the document at the end of each cycle might help to reduce the delay in responding to the next cycle (although since the request didn’t know it was going to process a document, it’s not clear how).  However that process of saving is in itself exponential. As the document content reaches 1mb, it takes over a minute to close.

Screenshot 2015-10-05 at 15.37.22

However the good news is that the delay between request and action has gone  (implying that there is some kind of implicit document save happening at each google.script.run request).


Screenshot 2015-10-05 at 16.11.02

What’s going on?

I’m now starting to suspect this is not completely related to the size of the file, but more about how many times its called. This time I’ll clear out the document between each cycle, so each cycle’s document should be roughly the same size – and in theory should suffer the same delay – but that is more or less steady, so it is related to the size of the document.

Screenshot 2015-10-05 at 16.26.35

 

Comparing the request time

A timestamp in the request can be compared against the time  the request gets received to confirm it is indeed processing something in between the time the call is sent and received. 

google.script.run.performanceTest (packet, new Date().getTime());

this is received and logged in the stats package. 

function performanceTest (package,initiated) {
  return PerformanceTest.createChunk(
    '1MR7QCbasRsAWRCeEc2baeNfyZBr2ZpMPsXxvNAHFfB4',
    package,
    initiated
  ); 
}

The lag (the time between the execution request and it starting) is not exponential, whereas the delay (the time between the previous cycle finishing and the next one starting) is. This pretty much points to there being a non sequential implicit save and close happening outside the flow of the script during which time nothing else can happen. 


Screenshot 2015-10-05 at 17.31.17

 

Conclusion

Assuming this is what’s supposed to happen, it makes add-ons involving large files pretty much impractical. One approach could be to always do a save and close at the end of some add-on action in the hope that another one will not be required before it’s finished.  

The code

The process is controlled from a webapp, which repeatedly provokes a performance test, dumping the results at the end


The performance test

This adds some random data to a table and appends it to the document, in each case measuring how long operations like opening, reading text and so on take. The data is random to minimize any skew that might be to do with some kind of compression or caching. This example has the save and close enabled.

function performanceTest (package) {
  return PerformanceTest.createChunk('1MR7QCbasRsAWRCeEc2baeNfyZBr2ZpMPsXxvNAHFfB4',package);
}
function dumpResult (package) {
  // write the data to the sheet
  new cSheetExec.SheetExec()
    .sheetOpen ('1Higg488LQnUzuvfXphD0fTaOqqpdAPDbwlWfokZYPyk','opentest')
    .clearContent()
    .setData (package);
}
var PerformanceTest = (function(pt) {
  'use strict';
  var MAX = 200, MIN = 50, ROWS = 40, COLUMNS = 10;
  function action_ ( packet , action , argOb , measure, func ) {
    var start = new Date().getTime();
    var result = func (argOb);
    packet.push ( {
      action:action,
      start:start,
      elapsed:new Date().getTime() - start,
      measure:measure
    });
    return result;
  }
  /**
   * create a bunch of pages
   */
  pt.createChunk = function (docId,packet) {
    if (!packet) {
      // first in, clear the document
      DocumentApp.openById (docId).getBody().clear();
    }
    var packet = packet || []; 
    action_ (packet , 'cycle', null, 0 , function () {
      // open a document
      var doc = action_ (packet , 'open' , docId , 0, function (id) {
        return DocumentApp.openById (id);
      }); 
      var openPacket = packet[packet.length-1];
      // get the body
      var body = action_ (packet , 'body' , null , 0 ,function () {
        return doc.getBody(); 
      }); 
      // get the body as text before adding some more
      var text = action_ (packet , 'pretext' , null , 0 ,function () {
        return body.getText(); 
      });
      openPacket.measure = text.length;
      var para = 'Writing another table';
      var elem = action_ (packet , 'paragraph' , para, para.length  , function (stuff) {
        return body.appendParagraph(stuff); 
      });
      // add some stuff
      var stuff = cUseful.getRandomSheetStrings(ROWS,COLUMNS,MAX,MIN);
      var elem = action_ (packet , 'chunk' , stuff, 0  , function () {
        return body.appendTable(stuff); 
      });
      // get the body as text
      var postText = action_ (packet , 'posttext' , null , 0 ,function () {
        return body.getText(); 
      });
      // add the size of the text now
      packet[packet.length-1].measure = postText.length;
      // and on the previous
      packet[packet.length-2].measure = postText.length - text.length;
      // close
      action_ (packet , 'saveandclose' , null, postText.length  , function () {
        doc.saveAndClose();
      });
    });
    return packet;
  };
  return pt;
})(PerformanceTest || {});
For more like this see Google Apps Scripts Snippets

Why not join our forum, follow the blog or follow me on Twitter to ensure you get updates when they are available.