Question about large difference in execution speed

I created an AppleScript application that resizes a TextEdit window to be just large enough to accommodate the document’s text without wrapping lines (ie, without enabling the scroll bar.) For the sake of this discussion, I named the application “ResizeWindow.app” and saved it on the desktop. I found a large discrepancy in execution speed as follows:

Normal execution speed:
Either… Open the app as a compiled script in Script Editor, then run the script directly in Script Editor
Or… Run the app from a separate AppleScript app using the following “run script file” command:


run script file ((path to home folder as string) & "Desktop:ResizeWindow.app")

Up to seven times slower execution speed:
Either… Open the app with Finder directly (ie, double-click its icon)
Or… Open the app from a separate AppleScript app using the Finder as follows:


tell application "Finder" to open file "ResizeWindow.app" of desktop

The same difference in execution speeds was observed whether the resizing script was saved as an application (.app) or as a compiled script (.scpt).

My questions are: 1) What is the cause of the discrepancy in execution speeds? and 2) Is there a way to get an app opened by Finder to execute as fast as when it is run by Script Editor or “run script file”? Thank you for any observations or suggestions.

The resizing app with an included sample document is as follows:


-- CREATE A SAMPLE TEXTEDIT DOCUMENT

tell application "TextEdit"
	activate
	make new document with properties {text:"Newton's Three Laws of Motion:

I. Every object in a state of uniform motion will remain in that state of motion ...

II. Force equals mass times acceleration.

III. For every action there is an equal and opposite reaction."}
end tell

-- RESIZE THE WINDOW

tell application "TextEdit"
	tell window 1 to set zoomed to true -- start with a fully zoomed window
end tell
tell application "System Events" to tell process "TextEdit"
	set frontmost to true
	tell window 1
		set {x_max, y_max} to size -- get the starting size of the fully zoomed window
		repeat with xy_axis in {"y", "x"} -- resize the vertical then the horizontal dimension of the window
			if contents of xy_axis is "x" then -- set the maximum and minimum amounts (delta) by which the window will be resized with each iteration, as well as switches to apply the resizing to the horizontal or vertical dimension selectively
				set {delta_max, delta_min, x_delta_switch, y_delta_switch} to {-x_max / 2, 5, 1, 0}
			else if contents of xy_axis is "y" then
				set {delta_max, delta_min, x_delta_switch, y_delta_switch} to {-y_max / 2, 5, 0, 1}
			end if
			set delta_value to delta_max -- set the starting delta to 1/2 the dimension of the fully zoomed window
			repeat
				set abs_delta_value to delta_value * (1 - 2 * ((delta_value < 0) as integer)) -- absolute value of delta
				tell scroll area 1 to tell scroll bar 1 to if enabled then -- if scroll bar is enabled, window size is too small
					if delta_value = delta_max then exit repeat -- quit if fully zoomed window is too small
					if delta_value < 0 then set delta_value to delta_value * -1 -- otherwise, be sure the next window resizing is in the optimal direction
				else -- if scroll bar is not enabled, window size is either optimal or too big
					if abs_delta_value < delta_min then exit repeat -- quit if window size is optimal
					if delta_value > 0 then set delta_value to delta_value * -1 -- otherwise, be sure the next window resizing is in the optimal direction
				end if
				set {x, y} to size
				set size to {x + delta_value * x_delta_switch, y + delta_value * y_delta_switch} -- resize the window
				if abs_delta_value > delta_min then set delta_value to delta_value / 2 -- halve the amount by which the window will next be resized, but no smaller than the minimum amount
			end repeat
		end repeat
		set {x, y} to size
		set position to {round (x_max - x) / 2 rounding down, round (y_max - y) / 2 rounding down} -- center window on screen
	end tell
end tell

Hi bmose,

As you’ve probably guessed, it takes time for an app to open. One way of speeding an app up, is to save as stay open app and keep it open. After the app is open and you want to have it run again, you need to add a reopen handler. e.g.


on run
	beep 3
end run
--
on reopen
	run
end reopen

If you don’t want the stay open app to run in the first opening of it, use ‘launch’.


tell application "reopener" to launch

You can also do this within the script by adding a flag, so it doesn’t run on first launch.

gl,

Here’s some other stuff.

You can just place all your statments in the reopen handler if you don’t want your statements to run on the first run:


on reopen
	beep 3
end reopen

Maybe you don’t know this, but you can run compiled scripts from the Script Menu:

http://www.apple.com/applescript/scriptmenu/

Bear in mind that Applescript is an object-model environment, e.g. complex scripting is best ordered and most efficient when script objects are being called in a hierarchy:


run script file ((path to home folder as string) & "Desktop:ResizeWindow.app")
run script file ((path to home folder as string) & ....)
run script file ((path to home folder as string) & ....)

This way, scripts are called in their compiled form without any overhead of launching an app, de-compilation upon opening a script in an editor (!), etc. etc.

Hi, bmose.

That’s a very clever script! :slight_smile: It gave me rubbish results at first because it presupposes that the document will be created in “Wrap to Text” mode, whereas my own TextEdit prefs are set for “Wrap to Page”. May I therefore suggest the following enhancement?

-- Change this:
tell application "TextEdit"
	tell window 1 to set zoomed to true -- start with a fully zoomed window
end tell
tell application "System Events" to tell process "TextEdit"
	set frontmost to true
	tell window 1
		set {x_max, y_max} to size -- get the starting size of the fully zoomed window

-- . to this:
tell application "System Events" to tell application process "TextEdit"
	set frontmost to true
	tell front window
		-- If the window opens in "Wrap to Page" mode, change to "Wrap to Text".
		if (scroll bar 2 of scroll area 1 exists) then keystroke "w" using {command down, shift down}
		click (first button whose subrole is "AXZoomButton") -- click the zoom button
			
		set {x_max, y_max} to size -- get the starting size of the fully zoomed window

As Kel says, applications take a little time to launch. I think there must also be some sub-optimal to-ing and fro-ing of commands and acknowledgements between the applet running a script and the applications being scripted. The various resizings of your TextEdit window are certainly much slower when the script’s an applet. I’d personally second Kel’s suggestion of saving the script as an ordinary compiled script and running it from the Script Menu, which can be enabled with “AppleScript Utility.app”.

But if you do have to use an applet, a work-round I’ve just discovered is to turn the entire script code into text and just have the applet apply ‘run script’ to it. This way (apparently) the applet only has to broadcast the ‘run script’ command and is presumably then cut out of the loop until it receives an acknowledgement that the script’s been run. So the applet code would be:

run script "-- CREATE A SAMPLE TEXTEDIT DOCUMENT

tell application \"TextEdit\"
	activate
	make new document with properties {text:\"Newton's Three Laws of Motion:

I. Every object in a state of uniform motion will remain in that state of motion ...

II. Force equals mass times acceleration.

III. For every action there is an equal and opposite reaction.\"}
end tell

-- RESIZE THE WINDOW
tell application \"System Events\"
	tell application process \"TextEdit\"
		set frontmost to true
		tell front window
			-- If the window opens in \"Wrap to Page\" mode, change to \"Wrap to Text\".
			if (scroll bar 2 of scroll area 1 exists) then keystroke \"w\" using {command down, shift down}
			click (first button whose subrole is \"AXZoomButton\") -- click the zoom button
			
			set {x_max, y_max} to size -- get the starting size of the fully zoomed window
			repeat with delta_list in {{-y_max / 2, 5, 0, 1}, {-x_max / 2, 5, 1, 0}} -- resize the vertical then the horizontal dimension of the window
				set {delta_max, delta_min, x_delta_switch, y_delta_switch} to delta_list
				set delta_value to delta_max -- set the starting delta to 1/2 the dimension of the fully zoomed window
				repeat
					set abs_delta_value to delta_value * (1 - 2 * ((delta_value < 0) as integer)) -- absolute value of delta
					tell scroll area 1 to tell scroll bar 1 to if enabled then -- if scroll bar is enabled, window size is too small
						if delta_value = delta_max then exit repeat -- quit if fully zoomed window is too small
						if delta_value < 0 then set delta_value to delta_value * -1 -- otherwise, be sure the next window resizing is in the optimal direction
					else -- if scroll bar is not enabled, window size is either optimal or too big
						if abs_delta_value < delta_min then exit repeat -- quit if window size is optimal
						if delta_value > 0 then set delta_value to delta_value * -1 -- otherwise, be sure the next window resizing is in the optimal direction
					end if
					set {x, y} to size
					set size to {x + delta_value * x_delta_switch, y + delta_value * y_delta_switch} -- resize the window
					if abs_delta_value > delta_min then set delta_value to delta_value / 2 -- halve the amount by which the window will next be resized, but no smaller than the minimum amount
				end repeat
			end repeat
			set {x, y} to size
			set position to {(x_max - x) div 2, (y_max - y) div 2} -- center window on screen
		end tell
	end tell
end tell"

You can get the text for this version (which of course I’ve already done here) by opening the compiled script in Script Editor, opening a new SE window in front of it, and running the following code in that window:

tell application "Script Editor" to get text of document 2

You can then copy/paste the text from the Result pane into a main window, type ‘run script’ in front of it, and save the document as an application.

Thank you to kel and Eelco for your suggestions on getting an app to start more quickly.

Even after starting, as I believe Nigel alluded to, an applet must involve suboptimal “to-ing and fro-ing” compared to a compiled script run with Script Editor or “run script file”, because the ongoing execution well after the applet has started remains slow throughout the entire execution time.

So it appears that there are several options for getting a script to run at optimal speed:

  1. As kel suggests, save the compiled script as a file and run it from the Script Menu.

  2. As Nigel suggests, make the entire code into a long text and make that text the “direct object” of the “run script” command.

  3. The method I described also seems to work, namely to save the script as a compiled script file, then run the script using a separate AppleScript applet containing the single command run script file “full file path”

It seems that, when execution speeds really counts, it’s best to avoid executing an applet via the Finder open command.

Finally, Nigel, thank you for your kind words about the resizing script. It was fun to write and kind of entertaining to watch in action. And thank you for pointing out the problem with the “Wrap to Page” preference and for your enhancement of the script to deal with that problem.

bmose

Just to clarify, Nigel’s method (#2 in my previous post) and my method (#3 in my previous post) do allow the use of the Finder to open an applet (for example, by double clicking the applet’s icon) while still maintaining optimal execution speed.

Ah. Sorry. This didn’t sink in when I read your original post. It’s the same principle as my idea “ having something other than the applet issuing the individual commands in the script. So my claim to have “just discovered” it, while strictly true, doesn’t necessarily mean that it hadn’t been discovered before by anyone else. :slight_smile:

One disadvantage of your method is that it needs two scripts per task: a compiled script to carry out the particular task and a dedicated applet to invoke that particular compiled script. But you could have just one compiled script to cover any task “ a generic script runner to which each applet would pass the relevant task code.

-- Script runner code.
-- Save as a compiled script in a convenient
-- location “ your user Scripts folder, say “
-- as "Script Runner.scpt".
on run {myScript}
	run script myScript
end run

An applet could pass code to this script either in compiled form:

script o
	-- Compiled code in here, eg:
	beep 3
	display dialog "Hello!"
end script

run script file ((path to scripts folder as Unicode text) & "Script Runner.scpt") with parameters {o}

. or as text:

set scriptText to "beep 3
display dialog \"Hello!\""

run script file ((path to scripts folder as Unicode text) & "Script Runner.scpt") with parameters {scriptText}

Eureka!!!

Nigel, thanks to all your ideas, it just hit me like a brick:

Let’s say the applet is as follows:


beep 3
display dialog "Hello!"

Then simply convert it to the following applet:


run script the_script

script the_script
beep 3
display dialog "Hello!"
end

The second applet will still be a single double-clickable applet that is easy to read and to code, and yet will execute at optimal speed (much faster than the first applet, particularly for more complex tasks such as my window resizing example)!

Hi, bmose.

I tried that last night with absolutely no speed improvement “ which is why I suggested the string version. But today, it’s having exactly the desired effect! Hmm… :confused: But that’s obviously the best way if it works and another piece of knowledge for the armoury. Great! :slight_smile:

Nigel,

I’m very grateful for your help with this topic. It’s been so informative and fruitful. I hope your experience with slow execution last night but fast execution today does not raise its ugly head on a regular basis!

I found that I could get handlers to execute more quickly as well using the same “run script” technique (again keeping in mind that everything we are talking about has to do with applets that are executed by opening them with the Finder). Assuming each of the following examples is saved as an applet and executed by the Finder (ie, by double-clicking its icon), this is what I learned:

This applet executes slowly…


test_handler()

on test_handler()
	beep 3
	display dialog "Hello!"
end test_handler

whereas all three of these applets execute 5-10 times more quickly…


run script test_handler()

on test_handler()
	script the_script
		beep 3
		display dialog "Hello!"
	end script
end test_handler


run script the_script

script the_script
	test_handler()
end script

on test_handler()
	beep 3
	display dialog "Hello!"
end test_handler


run script the_script

script the_script
	test_handler()

	on test_handler()
		beep 3
		display dialog "Hello!"
	end test_handler
end script

So it seems that any of these latter three techniques allows one to gain the execution speed benefit of “run script” in handlers. I’d love to hear whether you get the same results (of course, using something like the window resizing program to really put it to the test.)

bmose

…and one more that executes quickly:


test_handler()

on test_handler()
	script the_script
		beep 3
		display dialog "Hello!"
	end script
	run script the_script -- this must follow the script definition in order to compile
end test_handler

Actually, I lost track of the overall conclusion on your experiments.
I’d suppose that AS calls to compiled scripts are faster in execution that those that depend on applet launching, on the spot linking to external apps, (de-)compilation and other overhead.
(as can be seen with a (moderately scientific) glance into MenuMeters’ output, registering all processor activity involved)

Or is there some additional learning from this thread…?

Hi, bmose.

Yes. Those four all work effectively here. They’re all variations on the same theme, of course.

Script 1. test_handler() creates the script object and has no commands after that, so the value of the ‘result’ variable when the handler exits is the script object. This result becomes the direct object of the ‘run script’ command in the main script.

Script 2. The handler call “ and therefore the action of the handler when called with it “ are part of the script object. Since the handler is located in the main script, it’s inherited by the script object and available to it as if it were its own.

Script 3. Same as Script 2, except that the handler’s located within the script object. In this situation, the handler only belongs to the script object. If you want to call it from the main script (losing the speed advantage in an applet), you have to use a call like:

the_script's test_handler()

-- Or:
tell the_script to test_handler()

Script 4. The script object’s created (“instantiated”) and run within the handler. The ‘run script’ command thus has to come after the script object definition in order to be executed after the instantiation. Also, as in the other scripts, the script object definition syntax used here assigns the object to the variable ‘the_script’ in the same move as the instantiation. If you use the variable name before this point, the compiler’s clever enough to spot it and throw an error.

on test_handler()
	set the_script to 7
	script the_script -- Doesn't compile.
		beep 3
		display dialog "Hello!"
	end script
end test_handler

You could fool the compiler by assigning the variable after the instantiation .

on test_handler()
	set the_script to 7
	script
		beep 3
		display dialog "Hello!"
	end script
	set the_script to the result
end test_handler

. but there wouldn’t be any point. The ‘run script’ still has to come after the instantiation.

When a script object’s defined with a variable name at the top level of a script (ie. not in a handler), it’s instantiated when the script’s compiled, so you can put the ‘run script’ command anywhere else in the script that’s convenient. But if the object’s defined without a variable name, or is defined with a name that’s been declared local, it’s instantiated at run time and any mention of it has to come after the definition.

script
	beep 3
	display dialog "Hello!"
end script

run script result

Nigel,

Thank you for testing (and corroborating) my results, and for providing a formal basis for my ad hoc observations.

I won’t prolong this discussion any further except to pose a final question. Given that a 5-10 fold gain in speed of execution (of applets executed via the Finder) can be gained by simply wrapping the applet code in a script object, why hasn’t this created a greater stir in the AppleScript community earlier (or has it)? It seems that the rather remarkable speed gain far outweighs the trivial extra coding required. Is it that most applets execute “fast enough”, so why bother? Apple must know of this, so why haven’t they done something (whatever that something might be) to bring Finder–mediated applet executions up to speed with those achieved by “run script”? What am I missing?

Again, thank you for such an informative exchange.

bmose

I’m afraid don’t know why (or if) there’s been no stir over it. I’ve never noticed the effect before, so perhaps we’re sitting on the discovery of the century! :wink:

It’s been known for a few years now that assigning a list to a property of a script object can greatly speed up access to its items if handled properly, but that’s for an entirely different “ and off-topic “ reason.

A point to remember is that an applet is only slower than a compiled script when sending commands to another application. When running vanilla code, there’s no discernible difference. Testing some other code this morning, I find that the difference is more extreme on my Tiger system than on my Jaguar one. If the problem’s relatively new, that might explain the lack of stir over the ‘run script’ solution.

So then, it seems that we have narrowed the speed problem down to commands that meet all of the following three conditions:

(1) they are not wrapped in a script object,
(2) they are sent to other applications, and
(3) they are issued from applets executed by either the Finder open command or, as I discovered this morning, by another applet that tells the applet to run; so either of these commands will reproduce the slow execution speed (using my “ResizeWindow” applet as an example):


tell application "Finder" to open file "ResizeWindow.app" of desktop
-- or --
tell application "ResizeWindow.app" to run

Unfortunately, these three conditions apply to the large majority of useful double-clickable applets that one would want to write!!! For those applets where execution speed is a significant factor, as we have discussed earlier, one could either leave the code as uncompiled text and “run script” the text, wrap the code in a script object and “run script” the object, or save the code as a compiled script file and “run script” the file (which I presume is simply treating the compiled script as a script object).

One last tidbit. I am going to venture a guess as to why you found slow execution speed one time and fast speed the next time when you were testing the “run script” variations, because I ran into the same problem this morning. If you leave out the word “script” in “run script”, execution speed goes back down to a snail’s pace! Thus,

Slow execution speed:


run the_script
script the_script
	beep 3
	display dialog "Hello!"
end script

Fast execution speed:

run script the_script
script the_script
	beep 3
	display dialog "Hello!"
end script

A really excellent example. :slight_smile:

Seems to me (speculating) that in the first example (without the explicit “script” in the run statement) that time is wasted discovering what the script belongs to - shuffling through the Finder perhaps. In the second, the word “script” makes it a direct call to the compiler.

Hi bmose,

You forgot one. Run a stay open script that’s already open and just sitting there. Start time is almost instantaneous. :slight_smile:

gl,