December 95 - Speeding Up whose Clause Resolution In Your Scriptable Application
Speeding Up whose Clause Resolution In Your
Scriptable Application
Greg Anderson
The Object Support Library provides convenient mechanisms for scriptable
applications to support complex expressions that may return multiple results (such
as every item of container "b" whose name contains "a"). However, the performance of
applications that rely on the default behavior is nowhere near what it could be if the
application took on some of the work itself. This article shows you how to gain ten- to a
hundred-fold increases in the performance of whose clause resolution in your
scriptable application. If your application is not yet scriptable, you'll find that the
foundation classes presented in this article do most of the work required to support
scripting.
One of the greatest strengths of AppleScript is its built-in ability to do complex
operations on groups of objects in a single line of script. For example, suppose you
have a set of shapes in a scriptable drawing program, and you'd like to change the color
of all the red shapes to green. In conventional programming languages, you'd need to
write a loop that iterates over each object in the set, tests to see if its color is red, and
then does a "set color to green" command for each red object that was found. Using
AppleScript, you can do the same operation with the single statement set color of
every shape whose color is red to green. In that statement, every shape
whose color is redis called a whose clause, and it's the inclusion of whose clauses
that makes AppleScript the powerful language it is.
You may at first doubt that using a whose clause is much better than writing the
equivalent script with a loop. After all, the direction of modern processor design has
been toward simplicity of the instruction set; RISC chips are able to gain incredible
performance improvements by doing optimizations that aren't possible in CISC chips.
Also, when all is said and done, the whose clause must finally execute the same
loop-and-compare algorithm that you'd be forced to use if you wrote the script with
the basic flow-of-control script commands, such asdo-while and if-then.
Using a whose clause is, however, much more efficient than the alternative. AppleScript is based on the client/server paradigm: typically your script, the client,
will be running in one application (usually the Script Editor or a script saved as a
miniapplication), with the application being scripted acting as a server. In this
situation, each script command that's directed at the scriptable application needs to be
transferred between the two applications. Awhose clause is a single script command,
but with the loop approach many commands would need to be sent. Furthermore,
AppleScript allows the scriptable application to reside on a different machine than the
application running the script; if your script is running on a machine in Cupertino,
California, and the server is on, say, Mars, reducing the number of round-trip
messages would have a profound impact on the performance of the script. Remember,
you can currently get only about 30 round-trip Apple events per second, so even if
you aren't sending data to Mars, you'll still do a lot better with fewer events than with
many.
There's another, similar reason that using whose clauses is superior to the equivalent
loop-based script: AppleScript compiles scripts into byte codes that are interpreted
during execution, whereas the individual script commands (once interpreted) are
processed by a scriptable application typically written in a language that's compiled
into machine code (be it 680x0 or PowerPC(TM)). The loop-and-compare script will
execute several lines of script for every item that's compared, whereas the whose
clause is but a single line of script that triggers processing in a compiled application.
It should be quite clear which will take less time to execute.
The Object Support Library (OSL) -- the library that provides the API you use to
make your application scriptable -- enables your application to supportwhose
clauses without requiring you to write a lot of additional code. You only need to provide
an object-counting function and an object comparison function, and the OSL can
resolve whose clauses for you. Since supporting whose clauses allows script writers
to write more efficient scripts, you should always do at least this much. However,
there are two other features of the OSL that can vastly increase the performance of
scriptable applications but are often ignored by application writers: whose clause
resolution (a way for your application to find the objects that match a whose test
without using the OSL) and marking (a mechanism for efficiently handling collections
of objects, such as those satisfying a whose clause). Using whose clause resolution,
with the help of marking, will enable you to get the most out of your scriptable
application. Resolving whose clauses can be a bit tricky, but with a little help from
this article, you'll be on your way in no time.
If your application is not yet scriptable, you'll find the sample code included with this
article (and on this issue's CD) to be invaluable in getting you up and running --
particularly since it contains a lot of reusable code.
AN OVERVIEW OF THE OSL
Good descriptions of the OSL can be found in the develop articles "Apple Event Objects
and You" in Issue 10 and "Better Apple Event Coding Through Objects" in Issue 12. If
you need a quick review of the OSL and you don't feel like putting down this issue of
develop to dig through your back issues, read on. If you can already generate tokens and
resolve object specifiers in your sleep, by all means skip ahead to the next section.
When AppleScript is processing a script command such as delete paragraph 2 of
document "sample", it converts the command into an Apple event which it sends to
the scriptable application that's referenced by the script. The Apple event's event class
and message ID together specify the verb of the operation being performed -- in this
case delete. The object being operated on is passed in the keyDirectObject parameter
of the Apple event, which is called, naturally enough, the direct parameter of the
event.
The direct parameter is almost always an object specifier -- a descriptor of type
typeObjectSpecifier -- although in some cases it may be something else. For example,
in addition to object specifiers, the Scriptable Finder accepts alias records and file
specifications in the direct parameter of events sent to it. If the direct parameter of an
event is not of type typeObjectSpecifier, you're on your own to convert it into some
format that's understood by your event handler. For descriptors that are of this type,
though, all you need to do is call the function AEResolve, and the OSL will step in and
help your application resolve the object specifier -- that is, locate the Apple event
objects it describes.
Object specifiers are resolved through object accessor callbacks that your application
installs to allow the OSL to communicate with your application during object
resolution. The accessor callbacks must take the description of the object requested by
the OSL (for example, document "sample) and return atoken that describes the
object in terms that the application can understand (for example, a pointer to a
TDocument object). Tokens are passed back to the OSL in an AEDesc, a structure that
contains a 32-bit descriptor type and a handle. Your application has complete control
over what it stores in the token, as long as the AEDesc is valid (that is, it was created
with AECreateDesc).
When the OSL calls your application's object accessor callbacks, it always passes
either a token that represents the containing object (which it got from an earlier call
to one of your object accessors) or a representation of the default container of the
application, which is also called the null container of the application. So, to resolve the
object specifier paragraph 2 of document "sample", the OSL first asks for
document "sample from the null container. Then it asks the application to provide
a token for paragraph 2 from the token the application provided in response to the
request fordocument "sample. The token that the application provides for
paragraph 2 is returned as the result of the AEResolve call; the application will
presumably use this token to process the Delete event.
Resolving object specifiers is explained in Chapter 6 of Inside
Macintosh: Interapplication Communication. A figure illustrating the process
of resolving object specifiers is on page 6-6.*
MARKING
Inside Macintosh: Interapplication Communication describes marking as a mechanism
whereby items to be operated on are marked with some flag during resolution (that is,
from the callbacks made by the AEResolve function); then, during execution, each
marked item is processed and the mark is cleared. As described, marking doesn't sound
very interesting and appears to be useful only in fringe cases.
Marking is actually very well suited for use as a general-purpose collection
mechanism whenever the OSL needs to group tokens together to process an object
resolution. For example, if the OSL is resolving the whose clause every shape
whose color is red and there are multiple red shapes, the result of the call to
AEResolve must be a collection of all the tokens that represent red objects. If your
application supports marking, the OSL asks your application to create a special mark
token to represent this collection. After your application provides the OSL with a mark
token, the OSL will ask your application to add the tokens it provided for the red shapes
to the mark token's collection. When AEResolve completes, the mark token is returned
as the result of the resolution.
If your application doesn't support marking, the OSL will create collections of tokens
for you by copying the data from your tokens into a descriptor list (an AEDescList). It
calls the standard Apple Event Manager routines for creating descriptor lists, which
copy the data out of the data handle of the AEDesc and then store the token data
somewhere inside the data handle of the descriptor list; the descriptor type of the
AEDesc is similarly encapsulated.
Dealing with descriptor lists of tokens can be inconvenient, particularly if your
application already supports collections of objects in some other way. The OSL
marking mechanism gives you the flexibility to handle collections in any way that's
convenient for your application.
To support marking, you must pass the flag kAEIDoMarking to AEResolve and
implement the three marking callbacks that are passed to AESetObjectCallbacks: the
create-mark-token callback (called just a "mark-token callback" in Inside
Macintosh), the object-marking callback, and the mark-adjusting callback. The
create-mark-token callback doesn't need to do anything more than create an empty
mark token. The OSL will dispose of this token as usual by calling your token disposal
callback when the token is no longer needed. Listing 1 shows an example
implementation of a create-mark-token callback.
Listing 1. Create-mark-token callback
pascal OSErr CreateMark(AEDesc containerToken, DescType desiredClass,
AEDesc* markTokenDesc)
TMarkToken* markToken;
markToken = new TMarkToken;
markToken->IMarkToken();
markTokenDesc->descriptorType = typeTokenObject;
markTokenDesc->dataHandle = markToken;
return noErr;
}
The object-marking callback is passed a mark token created from the
create-mark-token callback and some other token created by one of your application's
object accessor callbacks. Your object-marking callback should add a copy of the other
token into the mark token (or apply a reference count to the token being added),
because the OSL will dispose of the token added to your collection shortly after calling
your object-marking callback. Listing 2 shows one implementation of an
object-marking callback.
Listing 2. Object-marking callback
pascal OSErr TAccessor::AddToMark(AEDesc tokenToAdd, AEDesc
markTokenDesc, long markCount)
AEDesc copyOfToken;
TMarkToken* markToken;
// We know that the OSL will only give us mark tokens created with
// our create-mark-token callback, but real code would do a test
// before typecasting.
markToken = (TMarkToken*) markTokenDesc.TokenObject();
// Add a copy of the token to the collection, because the OSL will
// dispose of tokenToAdd after passing it to you. A reference-
// counting scheme is good here.
copyOfToken = CloneToken(tokenToAdd);
markToken->AddToCollection(copyOfToken);
return noErr;
}
The mark-adjusting callback is called to remove ("unmark") tokens from the
collection. Oddly enough, its parameters specify which tokens in the range to keep; all
tokens outside the specified range should be discarded.
Implementing the marking callbacks is trivial. The only real work involved in
supporting marking is handling collections of tokens when they're ultimately received
by one of your event handlers (handling Move events, for example). The amount of code
required to handle the marking callbacks and maintain your own collections is
minimal; in fact, the time you'll save by not having to hassle with descriptor lists of
tokens will more than make up for the implementation cost. You'll find more
information on handling collections of tokens later in this article. Don't put off
marking as an optimization to be done later; incorporate it into the design of your
application from the very beginning.
For more details on the marking callbacks, see Inside Macintosh:
WHOSE CLAUSE RESOLUTION
The only thing that a scriptable application needs to do to support whoseclauses is
provide an object-counting function and an object comparison function -- the OSL will
do the rest of the work. When the OSL does a whoseclause resolution, however, it has
no choice but to iterate over every element in the search set, repeatedly calling your
application's object accessor, object comparison, and token disposal callbacks. Huge
performance gains can be realized if you resolve whose clauses yourself, because
you'll avoid the overhead the OSL requires to make these callbacks.
Passing the flag kAEIDoWhose to AEResolve tells the OSL that you'll resolve thewhose
clause yourself. The OSL calls your object accessor with the key form formWhose (see
Listing 3). The key data is a whose descriptor -- that is, an AERecord that describes
the comparison to be performed in the search. Your application should interpret the
whose descriptor and test every element of the container token to see if it matches the
specified criteria. If the whosedescriptor is too complex for your application, you can
return the error code errAEEventNotHandled from your object accessor, and the OSL
will do the resolution for you with the default techniques. This is very useful, as it
allows you to maximize the performance of the most common whose clauses, yet still
support complex whose descriptors that are likely to be encountered only rarely.
Listing 3. Handling formWhose in the object accessor
pascal OSErr MyObjectAccessor(DescType desiredClass,
AEDesc container, DescType /*containerClass*/,
DescType keyForm, AEDesc keyData, AEDesc* resultToken,
long /*hRefCon*/)
// case formAbsolutePosition, and so on
...
case formWhose:
// TWhoseDescriptor is a class that knows how to interpret
// a whose descriptor and test tokens for membership in the