Performance Guide
View SourceThis guide provides best practices for optimizing performance and reliability when using ExMacOSControl for macOS automation.
Overview
ExMacOSControl executes AppleScript, JXA, and Shortcuts via the osascript command-line tool. Understanding the performance characteristics of these operations can help you build more reliable and efficient automation.
Common Bottlenecks
1. Script Execution Time
Issue: AppleScript and JXA execution can be slow, especially when interacting with applications or the UI.
Causes:
- Application responsiveness (apps may be busy)
- UI operations (finding windows, clicking buttons)
- System Events interactions
- Script complexity and loops
Solutions:
- Use appropriate timeouts for operations
- Minimize UI interactions where possible
- Prefer direct API calls over UI automation
- Break complex scripts into smaller operations
Example:
# Instead of one large timeout for complex operations
{:ok, result} = ExMacOSControl.run_applescript(complex_script, timeout: 30_000)
# Break it down into smaller operations with shorter timeouts
{:ok, result1} = ExMacOSControl.run_applescript(script1, timeout: 5_000)
{:ok, result2} = ExMacOSControl.run_applescript(script2, timeout: 5_000)
{:ok, result3} = ExMacOSControl.run_applescript(script3, timeout: 5_000)2. Application Launch Time
Issue: Scripts that launch or activate applications may take several seconds.
Solution: Use longer timeouts when launching applications (5-10 seconds):
script = """
tell application "Safari"
activate
make new document
end tell
"""
{:ok, _} = ExMacOSControl.run_applescript(script, timeout: 10_000)3. File System Operations
Issue: File operations (copying, moving, searching) can be slow for large files or directories.
Solution:
- Use native Elixir File module when possible
- Only use AppleScript for operations that require Finder integration
- Consider background tasks for large operations
# Prefer Elixir's File module
File.cp!(source, destination)
# Only use Finder when you need Finder-specific features
ExMacOSControl.Finder.move_to_trash(file_path)4. Repeated osascript Calls
Issue: Each call to osascript has overhead (process spawn, script compilation).
Solution: Combine operations into a single script when possible:
# Less efficient - multiple osascript calls
{:ok, name} = ExMacOSControl.Finder.get_frontmost_window()
{:ok, bounds} = ExMacOSControl.Finder.get_window_bounds(name)
{:ok, position} = ExMacOSControl.Finder.get_window_position(name)
# More efficient - single script with multiple operations
script = """
tell application "Finder"
set frontWindow to front window
set windowInfo to {name of frontWindow, bounds of frontWindow, position of frontWindow}
return windowInfo
end tell
"""
{:ok, result} = ExMacOSControl.run_applescript(script)Timeout Configuration
Default Behavior
By default, ExMacOSControl operations have NO timeout. This means they will wait indefinitely for completion.
When to Use Timeouts
Always use timeouts for:
- Production applications
- Operations that interact with external applications
- UI automation
- Network-dependent scripts
- Any operation that could hang
Timeout not needed for:
- Simple calculations or string operations
- Scripts you've tested extensively
- Operations with guaranteed fast completion
Recommended Timeout Values
| Operation Type | Recommended Timeout |
|---|---|
| Simple script (calculations, strings) | 1,000ms (1s) |
| Application queries (get window name) | 3,000ms (3s) |
| Application launch/activation | 10,000ms (10s) |
| UI automation (clicking, typing) | 5,000ms (5s) |
| File operations | 5,000-15,000ms (5-15s) |
| Complex multi-step operations | 15,000-30,000ms (15-30s) |
Example Usage
# Quick operation
{:ok, result} = ExMacOSControl.run_applescript(
~s(return "hello"),
timeout: 1_000
)
# UI automation
{:ok, _} = ExMacOSControl.SystemEvents.click_button(
"OK",
"Safari",
timeout: 5_000
)
# Complex operation
{:ok, data} = ExMacOSControl.run_applescript(
complex_workflow_script,
timeout: 30_000
)Retry Logic
When to Use Retry
The ExMacOSControl.Retry module provides automatic retry functionality for transient failures.
Use retry for:
- Timeout errors that may succeed on subsequent attempts
- Operations that depend on application state
- Network-dependent operations within scripts
- UI automation affected by system responsiveness
Do NOT use retry for:
- Syntax errors (won't be fixed by retrying)
- Permission errors (user intervention required)
- Not found errors (resources won't appear)
- Logic errors in your scripts
Retry Examples
alias ExMacOSControl.Retry
# Basic retry with exponential backoff (default)
# Attempts: 1st immediately, 2nd after 200ms, 3rd after 400ms
{:ok, result} = Retry.with_retry(fn ->
ExMacOSControl.Finder.get_frontmost_window()
end)
# Custom max attempts with linear backoff
# Attempts: 1st immediately, 2nd-5th after 1000ms each
{:ok, result} = Retry.with_retry(fn ->
ExMacOSControl.SystemEvents.click_button("OK", "MyApp", timeout: 5_000)
end, max_attempts: 5, backoff: :linear)
# Combining timeout and retry
{:ok, windows} = Retry.with_retry(fn ->
ExMacOSControl.run_applescript(
list_all_windows_script,
timeout: 10_000
)
end, max_attempts: 3, backoff: :exponential)Backoff Strategies
Exponential Backoff (default)
- Doubles wait time between retries
- Formula:
2^attempt * 100ms - Best for: Operations that may need increasing time to succeed
- Wait times: 200ms, 400ms, 800ms, 1600ms, etc.
Linear Backoff
- Constant wait time between retries
- Wait time: 1000ms (1 second)
- Best for: Operations with consistent retry timing
- Wait times: 1000ms, 1000ms, 1000ms, etc.
# Exponential: Good for gradual system recovery
Retry.with_retry(fn ->
ExMacOSControl.run_applescript(script, timeout: 5_000)
end, backoff: :exponential)
# Linear: Good for operations with known fixed delay
Retry.with_retry(fn ->
ExMacOSControl.run_applescript(script, timeout: 5_000)
end, backoff: :linear)Telemetry and Monitoring
ExMacOSControl emits telemetry events for all operations, allowing you to monitor performance and reliability.
AppleScript Execution Events
Events:
[:ex_macos_control, :applescript, :start]- Script execution begins[:ex_macos_control, :applescript, :stop]- Script execution succeeds[:ex_macos_control, :applescript, :exception]- Script execution fails
Measurements:
script_length- Length of the script in bytesduration- Execution time in microseconds (stop/exception only)
Metadata:
command- Command being executed ("osascript")script- First 100 characters of the scripttimeout- Configured timeout (or nil)result_type-:successor:erroroutput_length- Length of output (success only)error- Error details (exception only)
Retry Events
Events:
[:ex_macos_control, :retry, :start]- Retry logic begins[:ex_macos_control, :retry, :attempt]- Each retry attempt[:ex_macos_control, :retry, :sleep]- Sleeping before retry[:ex_macos_control, :retry, :stop]- Retry succeeds[:ex_macos_control, :retry, :error]- All retries exhausted
Metadata:
attempt- Current attempt numbermax_attempts- Maximum configured attemptsbackoff- Backoff strategy (:exponentialor:linear)sleep_time- Time to sleep before next retry (sleep event only)error- Error that triggered retry
Setting Up Telemetry
# In your application.ex
defmodule MyApp.Application do
use Application
def start(_type, _args) do
# Attach telemetry handlers
:telemetry.attach_many(
"ex-macos-control-handler",
[
[:ex_macos_control, :applescript, :start],
[:ex_macos_control, :applescript, :stop],
[:ex_macos_control, :applescript, :exception],
[:ex_macos_control, :retry, :start],
[:ex_macos_control, :retry, :stop],
[:ex_macos_control, :retry, :error]
],
&handle_telemetry_event/4,
nil
)
children = [
# Your app's children
]
Supervisor.start_link(children, strategy: :one_for_one)
end
defp handle_telemetry_event(event, measurements, metadata, _config) do
# Log or send to monitoring service
Logger.info("Telemetry event: #{inspect(event)}")
Logger.debug("Measurements: #{inspect(measurements)}")
Logger.debug("Metadata: #{inspect(metadata)}")
end
endExample: Tracking Slow Operations
defmodule MyApp.TelemetryHandler do
require Logger
def handle_event([:ex_macos_control, :applescript, :stop], measurements, metadata, _) do
duration_ms = measurements.duration / 1_000
if duration_ms > 5_000 do
Logger.warning("""
Slow AppleScript execution detected:
Duration: #{duration_ms}ms
Script: #{metadata.script}
Timeout: #{metadata.timeout}
""")
end
end
def handle_event([:ex_macos_control, :retry, :error], _measurements, metadata, _) do
Logger.error("""
Retry exhausted after #{metadata.max_attempts} attempts:
Error: #{inspect(metadata.error)}
""")
end
def handle_event(_, _, _, _), do: :ok
end
# Attach in your application.ex
:telemetry.attach_many(
"slow-operations-tracker",
[
[:ex_macos_control, :applescript, :stop],
[:ex_macos_control, :retry, :error]
],
&MyApp.TelemetryHandler.handle_event/4,
nil
)Benchmarking
Simple Benchmarking
defmodule MyApp.Benchmark do
def measure(label, fun) do
{time, result} = :timer.tc(fun)
IO.puts("#{label}: #{time / 1_000}ms")
result
end
end
# Usage
MyApp.Benchmark.measure("Get Finder windows", fn ->
ExMacOSControl.Finder.list_windows()
end)
# => "Get Finder windows: 234.5ms"Using Benchee
For more comprehensive benchmarking, use the Benchee library:
# In mix.exs
{:benchee, "~> 1.3", only: :dev}
# In a benchmark file
Benchee.run(%{
"direct applescript" => fn ->
ExMacOSControl.run_applescript(~s(tell application "Finder" to return name of every window))
end,
"via Finder module" => fn ->
ExMacOSControl.Finder.list_windows()
end,
"with timeout" => fn ->
ExMacOSControl.run_applescript(
~s(tell application "Finder" to return name of every window),
timeout: 5_000
)
end
})Best Practices Summary
- Use timeouts in production for all operations
- Choose appropriate timeout values based on operation type
- Use retry logic for transient failures (timeouts)
- Monitor with telemetry to identify slow operations
- Combine operations into single scripts when possible
- Prefer native Elixir for non-automation tasks
- Break down complex scripts into smaller operations
- Test timeout values in your specific environment
- Use exponential backoff for gradual recovery scenarios
- Use linear backoff for known fixed delays
Troubleshooting Performance Issues
Operation Taking Too Long
- Check if timeout is appropriate for the operation
- Verify the application is responsive
- Simplify the script if possible
- Consider breaking into smaller operations
- Check telemetry data for actual execution time
Frequent Timeouts
- Increase timeout value
- Add retry logic with exponential backoff
- Check system resources (CPU, memory)
- Verify the application is not hanging
- Consider if the operation is too complex
Retry Not Working
- Verify error type is
:timeout - Check max_attempts is sufficient
- Consider increasing timeout before retry
- Review telemetry events for retry attempts
- Ensure the operation can succeed eventually