Optimizing AppleScript

AppleScript is a scripting language, but it's performance is really very good. There are some tricks and some things to keep in mind that can help significantly with certain kinds of tasks though.

Inside of the computer, nothing takes any time at all unless you're doing it more than once. Once you've got a lot of looping going on for any reason then it's time to pay attention to what you're doing.

Evaluating Loop Parameters

Remember that anything you use as a counter or a check in a loop will get evaluated each and every time you go through the loop. So you might end up doing a lot of extra work. These are kind of silly examples, but might help to illustrate the point:

repeat with i from 0 to (10000 * 5)
  set something to something or other
end repeat

The (10000 * 5) will get evaluated 50 thousand times. When the parameter isn't changing just use the number. This may seem obvious but certain things in AppleScript are almost always written in that kind of notation, take time for instance:

 Say you were looping through a list of units to check their last activity date...
--this gets a list of all the units assigned to an interface called "WirelessReceiver"
set MyUnitList to UnitList of xInterface "WirelessReceiver"
 
--now we'll loop through the list of units and write an alert to the log
--if any are older than a week
 
repeat with WorkingUnitName in MyUnitList
  if time delta of WorkingUnitName is greater than (7 * days) then
    write log "Unit: " & WorkingUnitName & " Hasn't been heard from in more than a week!"
  end if
end repeat

In that example it's evaluating (7 * days) for each unit. This is probably a trivial example, but that sort of thing adds up. You can do the math yourself and put in a constant, 7 days * 24 hours * 60 minutes * 60 seconds is 604800 seconds in total, or if you dont have a calculator handy you can evaluate that line ahead of the loop and put the answer into a property to check against. Doing it just once is no penalty we need worry about.

set MyUnitList to UnitList of xInterface "WirelessReceiver"
 
set WarningTime to (7 * days)
 
repeat with WorkingUnitName in MyUnitList
  if time delta of WorkingUnitName is greater than WarningTime then
    write log "Unit: " & WorkingUnitName & " hasn't been heard from in over a week!"
  end if
end repeat

Sending AppleEvents

Sending an AppleEvent to an external app to ask it for some information is an expensive operation timewise. Whenever possible get your data in as few events as necessary. Back in OS9 there was a very significant penalty for the context switch that was required for the running app to send an event to another app, wait for it to wake up and start processing, then wait for the processor to get back around to the app waiting for the response. On OSX this is improved significantly, but one should still keep this in mind. Most of the commands that you'll use in Xtension are “send to self” type apple events and so do not require a context shift and so are very fast and nothing to worry about. There are some ways to minimize the sending of events though. Here is another slightly silly example. This gets a list of units and walks through them. What this is looking for is if it's ON and how long it's been on. If it's been on for more than 10 minutes we turn it off thinking that we missed an off for a motion sensor. I actually do something similar to this as for some reason I regularly miss off's for some motion sensors and I like everything in my lists to show off when I look at them if they are supposed to be off.

--this assumes you have a list created in XTension called "All Motion Sensors"
--and gets the names of them all in a list
set MyUnitList to all of list "All Motion Sensors"
 
--do the math here rather than each time through the loop!
set MyTimeout to (10 * minutes)
 
repeat with WorkingUnitName in MyUnitList
  if (time delta of WorkingUnitName is greater than MyTimeout) and
          (status of WorkingUnitName is True) then
    write log "missed an off for unit " & WorkingUnitName
    turn off WorkingUnitName
  end if
end repeat

In this example the checks for the time and status of the unit are 2 separate events. We can reduce this to none by using the unit extract to get all the data about the units from the “all of list” command and then not have to send another event until we find one thats a problem.

  set MyUnitList to all of list "All Motion Sensors" with extract
 
  -- we now have a list of records with all the data about those 
  -- units in them, not just a list of names
  -- we can access that information by the keywords in the scripting dictionary
 
  set MyTimeout to (10 * minutes)
 
  repeat with ThisUnitRecord in MyUnitList
    if (xtTimeDelta of ThisUnitRecord is greater than MyTimeout) and
          (xtValue is not 0) then
 
       set ThisUnitName to xtName of ThisUnitRecord
 
       write log "missed an off for unit " & ThisUnitName
       turn off ThisUnitName
    end if
  end repeat

So we get the entire record for all the units in only 1 event. The names of the fields in the record are documented in the AppleScript Dictionary section of this document.

X2Web: Internet Control And Monitoring for XTension is another place where special events are available to reduce the number of times you need to send an event to work with something. When building complicated web pages in X2Web you may have to ask for dozens of form elements submitted from the page. Originally this took sending an individual event to the program for each one like:

  set TheName to getFormVariable "name"
  set TheUnit to getFormVariable "unit"
  set TheDay to getFormVariable "day"
  set theRequest to getFormVariable "request"

the “getFormVariable” verb returns the keyed form variable as submited in an HTML form and you'll need some of that data to actually do anything. X2Web 4.2 and newer allows to you to make that entire set of 4 events with only 1 event like this:

  set {TheName, TheUnit, TheDay, TheRequest} to getFormVariable {"name", "unit", "day", "request"}

Another feature of X2Web that can accept it's data as lists as well as individually is the “replacePageTag” verb. This verb replaces a tag in a page you're processing with some data. For example you might want to dynamically create the title of an html page so you'd do something like:

  <title>[MyGeneratedTitle]</title>

in a script on the page you might replace that with the date dynamically generated for example:

  replacePageTag tag "MyGeneratedTitle" value "welcome: " & (current date as text)

but if you had multiple values to replace you can send a list of lists containing key/value pairs such as:

  replacePageTag {{"tag name", "value to insert"}, {"name", "peter"}, {"hair", "none"}, ...}

you can generate that list of lists however you need to and then pass it up to X2Web for processing with just a single event instead of a long list of individual replacePageTag verbs.

Script Variables vs Local Variables

There is a really odd difference in the speed between local and global lists in AppleScript. This is actually documented by Apple and this has been this way for as long as I can remember. Perhaps it's a bug, maybe it's a feature, regardless you can use it to dramatically speed up operations that work on lists.

  --create a list to work with
  set MyList to {}
 
  --put 5000 elements in the list for us to walk through for testing
  repeat with i from 0 to 5000
    set the end of MyList to i
  end repeat
 
  --now walk the list again and add up all the number
  set MyTotal to 0
  repeat with ThisValue in MyList
   set MyTotal to MyTotal + ThisValue
  end repeat
 
  return MyTotal

if you run that in the script editor it takes just shy of 5 seconds on my Macbook here. If you make a very minor addition to the top of the script though like this:

--create a global scope list to work with
  property MyGlobalList:{}
 
--make sure it's empty as these are persistent between runs
  set MyGlobalList to {}
 
--now work with a reference to that global list
  set MyList to a reference to MyGlobalList
 
  --put 5000 elements in the list for us to walk through for testing
  repeat with i from 0 to 5000
    set the end of MyList to i
  end repeat
 
  --now walk the list again and add up all the number
  set MyTotal to 0
  repeat with ThisValue in MyList
   set MyTotal to MyTotal + ThisValue
  end repeat
 
  return MyTotal

it returns the answer almost immediately! This is a very significant improvement! All thats necessary is to make a global variable via the property command, and operate on it with another local variable containing the reference. You can make this change to scripts like I did here without even changing the logic at all, just redeclaring the variables you use at the very beginning like that. In the script editor it is possible to just add the “my” keyword before you access the variable in the loop and not mess with setting up “a reference to”, and because the script is running in it's own context, there is no parent or other inheritance going on, that will work fine. Within XTension and X2Web scripting though you're more likely to just get an error if you try that as all scripts in XTension have the attachments script setup as it's parent, and it gets more complicated again if you're writing inside a handler rather than in the script proper. I would recommend just creating a global via the property statement outside the handler and then creating a local variable inside the handler that is set to a reference to the global. Anytime you're walking through or building up a large list this will make a really significant difference.

Concatenating Strings

This might apply more to X2Web: Internet Control And Monitoring for XTension than to XTension, but can be useful anywhere you're building up reports with lots of little sections. Many times in the code above already we've added strings together to make the display we wanted.

  set MyString to "this " & Something & " is an example"

adds 3 substrings together. If you're only doing this a couple of times like that then there is no reason to worry, but if you're adding a lot of data together as you would if you were dynamically creating a webpage in X2Web: Internet Control And Monitoring for XTension or creating a large report or something from XTension then you need to pay attention to how you're doing it. This example:

  set MyString to ""
  repeat 10000 times
    set MyString to MyString & " hello from the loop"
  end repeat

takes about 6 seconds on my MacBook. Back in the OS9 days working with lists was much faster than working with strings. When I first started working on X2Web: Internet Control And Monitoring for XTension I recommended doing it like this:

  set MyList to {}
  repeat 10000 times
    set the end of MyList to " hello from the loop"
  end repeat
  return MyList as text

but on my MacBook here with OSx 10.5.6 that is even worse, taking over 10 seconds to complete. However, if we combine that with what we know from the above section on local vs script variables we get a really dramatic speed increase.

  property MyGlobalList:{}
 
  --clear the global as it will live between runs
  set MyGlobalList to {}
 
--make the local reference
  set MyList to a reference to MyGlobalList
 
  repeat 10000 times
    set the end of MyList to " hello from the loop"
  end repeat
 
  return MyList as text

That code does exactly the same thing, but returns in less than a second! a 10x improvement? By going through all my concatenating in the current version of the InstantX pages I was able to shave a full third off the page creation times for my large and complicated home page. That is an excellent improvement for just some very simple code changes.

NOTE:

After having written this it occurs to me that there is an important limitation to using lists in this way that we may need to keep in mind. It seems that there is a limitation of 2k for the length of a list element in text. I ran into this when trying to use this to output the motion sensor reports. The individual lines I was trying to insert were greater than 2048 characters and they were then truncated. Evidently this is a long standing feature/bug of Apple Script but I was unaware of it until I hit it myself. There does not appear to be a limitation on the number of elements of the list, and the entirety of it can be quite large without problems. But the individual strings that you're stitching together need to be less than 2k.

tutorials/optimizing_applescript.txt · Last modified: 2009/01/31 10:06 by Michael Ferguson
CC Attribution-No Derivative Works 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0