AppleScript's capabilities are greatly enhanced by being able to call Objective-C methods with AppleScript-ObjC (ASObjC/ASOC).
When the OSA scripting component instance executes (not imports) the first line of ASOC code, it has to load the bridging files that enable ASOC interoperability. This can take upwards of 0.5–1.0 seconds on some systems.
Subsequent AppleScript runs that re-use the same scripting component instance (even on different files) do not need to re-load these bridging files, so execution is much faster (almost instantaneous on simple scripts).
BTT:
BTT now has great support for AppleScript.
From what I can tell, BTT uses a separate process as a script runner to run AppleScripts, and re-uses it for future script runs (which is perfect). This means that the above behaviour is readily observed (the first run of an ASOC script in the helper process takes much longer, but subsequent runs are much faster).
Feature Request:
BTT's AppleScript-running processes can be "primed" after creation by running a short AppleScript that executes a couple of throwaway ASOC calls.
An example AppleScript that would accomplish this is:
use framework "Foundation"
use framework "AppKit"
current application's NSString's |string|()
current application's NSWorkspace's sharedWorkspace()
return
(This bridging files are loaded by the code in lines 3 & 4, not the "use" statements themselves.)
FastScripts 3 did a similar optimization that proved very effective (reference).
Andreas, I'm hoping this is something you could implement, since it would make ASOC execution times reliable (and fast enough for more use cases).
Please let me know if I can provide any other info!
i would recommend to run your priming script when BTT launches via the „BetterTouchTool did launch on specific machine“ trigger.
I think not all systems will need the bridge loaded - it probably adds quite a bit to the memory consumption
I did think of this initially, but it would rely on knowning how BTT manages its script helpers in great detail – I'm assuming there's more than one instance of the script runner, and there's of course no way for the user to specify which instance of the helper script BTT uses when triggering an action.
For the last couple of years, I've been triggering my AppleScripts through BTT & using a convoluted workaround** to have FastScripts run the AppleScripts – all to avoid this delay.
(**The BTT action runs a shell script, which calls osascript, which sends an Apple Event to FastScripts, which runs the AppleScript.)
I don't believe the bridging files actually take up that much memory.
I haven't run any proper memory profiling tools, but running the above script via osascript in Terminal & inspecting with Activity Manager shows that its process is allocated 14.6 MB vs. 4.1 MB for a non-ASOC equivalent script.
So, it looks like it would take about 10 MB of extra memory. I don't know if you'd consider that significant or not.
Most likely it's more than that - while the BTT processes will only have minimal overhead in memory consumption this triggers the load of various system frameworks in other processes. It's hard to profile though.
I'll check whether I can add an advanced option to trigger this for every script runner, but I think I won't make it the default, at least for now.
(Most of the time BTT only uses one script runner, it's just for stuff that is calling into BTT where two or maybe three are needed.)
Sorry for the long delay in getting back to you about this; it was quite a treat trying to figure out how to measure sub-second intervals in a portable AppleScript.
I've set the default you provided & restarted BTT, but that preference doesn't appear to have had any effect even on the latest stable BTT version (5.158) running on Sequoia 15.3.
Regardless of whether the preference is set, I'm getting the following run times with a simple ASOC script (2019 iMac 27" i5):
1st run after starting BTT - ~600 ms
2nd run after starting BTT - ~70 ms
3rd & subsequent runs - ~30 ms
Here is the AppleScript I've written to measure the execution time:
use scripting additions
use framework "Foundation"
use framework "AppKit"
set start_ticks to getTicks()
set nss to current application's NSString's stringWithString:"Hello"
set nsw to current application's NSWorkspace's sharedWorkspace()
set runtime to ticksSince(start_ticks)
display dialog "Execution time: " & (runtime as string) & " s" with title (my name as string) buttons {"Dismiss"} default button 1
return runtime
-- Sub-second time measurement handlers.
to getTicks() -- Returns string.
return do shell script "perl -MTime::HiRes=time -e \"printf \\\"%.9f\\n\\\", time\""
end getTicks
to ticksSince(old_ticks) -- Manual math to avoid limited precision of AppleScript number types.
-- Typically has ~15 ms of overhead.
set new_ticks to getTicks()
set previous_TIDs to AppleScript's text item delimiters
set AppleScript's text item delimiters to "."
set {old_integer_component, old_decimal_component} to text items of old_ticks
set {new_integer_component, new_decimal_component} to text items of new_ticks
set AppleScript's text item delimiters to previous_TIDs
set integer_component to ((new_integer_component as real) - (old_integer_component as real)) as integer
set decimal_component to ((new_decimal_component as real) - (old_decimal_component as real)) / 1.0E+9
return integer_component + decimal_component
end ticksSince
If you exclude the time taken for the time measurement, the first run is still taking ~50X as long to run for short scripts. The lag is definitely noticeable in use.
Please let me know if I can provide any other info or further assist with testing. Now that the timing script is sorted, I'll be able to respond much quicker – I'm still very motivated to help with this!
the remaining performance differences might not be due to the objective-c stuff but because BTT keeps a compiled version of the script in memory once you have executed it
Maybe I could add an option to keep compiled versions, I‘ll need to look into that
What might also help is not using a scpt file but instead use one of the other options (inline/preset/external file)
Thanks for getting back to me. I was just about to add another data point with similar results on an ARM machine (16" M1 Max MacBook Pro, BTT v5.199):
1st run after starting BTT - ~400 ms
2nd run after starting BTT - ~60 ms
3rd & subsequent runs - ~30 ms
I do think it's still related to the AppleScript-Objective C bridge – if you comment out the 2 lines that call the Objective C methods (effectively making it a non-ASOC script):
# set nss to current application's NSString's stringWithString:"Hello"
# set nsw to current application's NSWorkspace's sharedWorkspace()
Then all runs of the script (including the 1st) take only about 30 ms to execute
If you look at some comparisons with other applications using the ASOC script above (i.e. without commenting out the ASOC lines) on the M1 Max MBP:
Script Debugger v8.0.10 (which doesn't preload or "prime" an ASOC runner):
The first run after launch takes ~400 ms, but any subsequent run only takes ~10 ms. (The slow 1st run isn't really an issue, since Script Debugger isn't really used as a background "Script Runner".)
If Script Debugger first runs a different ASOC script after launching, then the first run only takes ~10 ms because the ASOC bridging files are already loaded.
FastScripts v3.3.4 (which does preload the ASOC bridging files on launch as described in the first post):
All runs (first after launch & subsequent) take only ~45 ms.
I tested this with your measurement script and it definitely works here (immediately after BTT restart I get the ~30ms that you mentioned), without the defaults setting it takes much longer
I'm using the non-SetApp version of BTT (directly-licensed / perpetual), v5.294 (current release).
I am very familiar with defaults, and have triple-checked that the defaults are in the right place. The write commands were entered when BTT wasn't running (and I've erased/toggled them multiple times to be sure).
Here is the output of the read command, so that you can be sure:
I've re-tested everything again today (both the "async in background" & "blocking" AppleScript actions) & have also entered the command for the SetApp version, just in case (although I have never had any SetApp applications on this Mac). The BTT SetApp defaults domain now reads:
On a 2019 27" iMac (Intel), I'm still getting ~650 ms for the first run of the test script, then ~75 ms for the second, then ~33 ms for the third & subsequent runs after launching BTT.
That's great to hear! Perhaps that means there's a bug in how BTT is checking the defaults, or some other build/debug setting that's letting it work for you?
I've also tried on an M1 MBP & am still getting similar results to the above (~400 ms on the first run).
Does the preference key you supplied (BTTAppleScriptPreloadAppkitAndFoundationBridge) have the correct casing throughout? (As I'm sure you know, NSUserDefaults keys are case sensitive.) I've tried making the k in Appkit a capital in case that was the issue, but to no effect.
The key is correct (BTTAppleScriptPreloadAppkitAndFoundationBridge)
You can check whether the init scripts are being executed by opening the logs from ~/Library/Application Support/BetterTouchTool/Logs in a text editor and look for
|AS|INITBRIDGE
If the preload scripts are executed it writes logs like this:
2025/04/04 23:14:20:349|AS|INITBRIDGE1|
2025/04/04 23:14:20:349|AS|INITBRIDGE2|
However I think I might now have seen what's causing your issue. You are using scpt files, correct? I think for these precompiled Apple Scripts this optimization (at least with the BTT setup) can not work. You'd need to switch them to raw text files:
I can confirm that I do see the 4 entries in the log when BTT launches if the BTTAppleScriptPreloadAppkitAndFoundationBridge key is set.
You're correct – I was using pre-compiled .scpt files. If I switch to using raw text like in your screenshot, I can see that the preference key above is having an effect – I'm getting 110 ms on first run (vs. 380 ms if the key is unset) on the 2019 27" iMac (Intel). The second run takes 50 ms & the third run takes 33 ms regardless of whether the key is set.
Thank you for working through that.
Though I admit, I'm surprised that the pre-compiled .scpt files are so much slower when kicked off through BTT (650 ms regardless of whether the preference key is set). Unfortunately, I don't think it will be personally feasible for me to transition all my AppleScripts to be contained within BTT as raw text, since I also run (and modify) many of the from outside of BTT.
In the original post up at the top, I mentioned how FastScripts was able to greatly speed up its ASOC cold-run time by first running a 5-line AppleScript on launch.
I should clarify that FastScripts is able to accomplish this on pre-compiled .scpt files using this technique:
Do you think that BTT could also support this optimization on pre-compiled .scpt files using a similar approach? I obviously don't know in detail how BTT implements its script runners, so I'm not sure if this is feasible with the current architecture without a lot of development effort.
yes it could, but I‘ll need to change how scpt files are run. I‘ll look into it, but might take a bit.
For modifying outside of BTT, isn’t it easier to modify raw text files? Any app should be able to edit them. Using .applescript instead of .scpt using the „external file“ option (see first of my last two screenshots) should ensure compatibility across apps.
could you please make sure that you still execute the scripts in the app context in this action and not in a separate script-runner that is then terminated when the script ends? I subscribe to some observers when starting BTT via AppleScript, which would then no longer work. That would be a pity, because otherwise I would have to start an extra app for this.
That's fantastic. Really appreciate you looking into it!
I think a lot of us use .scpt files or .scptd bundles with Script Debugger, FastScripts, and /usr/bin/osascript for standalone scripts. Pre-compiled scripts execute a lot faster in these context or if launched from Applications' own script menus. In addition to the compile-time syntax checks, they also support other AppleScript features like persistent script properties, compile-time terminology resolution, properties that are computed at compile time, bundled script libraries & resources, etc. that aren't possible with raw text.
I think the raw text .applescript files are generally preferred when combined with Swift/ObjC in Xcode projects, for the reasons you mentioned & for compatibility with git. A lot of the core AppleScript "features" become more of a hindrance in these scenarios.
Thanks again.
I'll just add for posterity that the action Dirk is using here is the older BTT action for running AppleScripts, before the 2 new AppleScript-specific actions were added to BTT a few of years ago. This action only supports external pre-compiled AppleScripts, is not affected by the new preference key you created, and shows similar performance to the pre-compiled scripts in the new AppleScript action (~380 ms on first run on an M1 MBP).
Dirk, I'd be interested in knowing how you make use of observers here.