Nice Agents

I’ve written about issues with deploying too many system agents before but in a managed environment some things are unavoidable. We can do some things to make them better behaved though, and ensure that usability of the system by the human at the keyboard is prioritised over other processes.

The main issue is that a lot of agents install with default performance settings and as such, will compete for system resources on equal footing with user apps and other processes.

A greedy system agent might want to wake up on a certain schedule and consume as much CPU as it can before going back to sleep. While running, CPU usage can spike, the system can become sluggish and the user experience can become degraded.

As a sysadmin, we want to balance system performance so we can meet any security or management requirements while minimising the impact to the end user trying to actually do work.

What is Nice

Nice, in BSD terms, is a property that informs the OS how “nice” the process should be to other processes on the system when it comes to system resources like CPU usage. This can range from -20 to +20. If unset, the default is 0.

https://www.manpagez.com/man/3/nice/

The higher the value, the “more nice” a process can be with regard to giving up resources to higher priority tasks. Given a limited resource, macOS will prioritise processes with lower (i.e. less “nice”) nice values.

Note that this does not limit the resources a process can use, only how the process is scheduled by the OS. If a process wants to use 100% CPU and 100% of CPU is available then the OS will allocate it. If a higher priority task comes along though then resources will be re-allocated as required.

From a sysadmin perspective, this means you can set the nice property on running system agents and let the OS manage the resources, so for example if your video conference app has a higher priority than your inventory collection daemon, the latter won’t interfere as much as it would if they both had the same priority, but if the system is idle, the system agent will be able to ramp up and complete its tasks.

Making a process behave nicely

launchd has a property in the agent/daemon plist that can set this:

<key>Nice</key>
<integer>5</integer>

…however most of the time you don’t want to be maintaining this directly for third party launchd plists (although you can if that’s what you really want to do). Instead we can use the renice command to set the nice property of an existing process like so:

pid=$(pgrep -f "$PROCESS_NAME")
renice +20 -p "$pid"

This will set the nice property of the identified process to the highest value and nearly every other process on the system will take precedence. If you have particularly resource hungry processes, this can improve user experience in the UI quite a bit, while letting the system agent do its thing. The nice value will also applies to any child processes the agent might spawn.

You can validate the nice value of a running process by running:

ps -o ni= -p "$pid"

Disclaimer: Running renice on third party software should have no issues but you should verify that doing so doesn’t have any adverse effects on how the software needs to operate. Testing and validation is very much recommended.

The last time renice was mentioned on the MacAdmins Slack was in 2019 so I figured it was worth blogging about.

Limiting CPU resources

While we can’t set CPU resources directly, like “use 10% CPU only” we can limit the process on Apple Silicon macs to run on efficiency cores only. Howard Oakley has a great writeup on that on the Eclectic Light Company blog

To do this we can use the taskpolicy command like so:

taskpolicy -b -p "$pid"

This will limit the process to running on efficiency cores only, further improving resource utalisation for low priority tasks.

Tying things together

Here is a script that can perform all the above. It takes a process name as an argument and optionally a nice value in the range of -20 to +20. (link)

This would need to run after the system has booted and agents are running. An option would be to use this with an Outset login-privileged-every task, but could also run from an MDM or other device management framework automatically or on request.

#!/bin/bash

# Script to renice processes.

# Author: Bart Reardon
# Date: 2025-04-24
# Version: 1.0

## Description:
# This sets the priority of processes to the specified value.
# It will check for the process every 60 seconds and renice it if found.
# If the process is not found, it will retry for a maximum of 10 times.
# If the process is not found after 10 retries, it will exit with an error.
# Log output is written to /var/log/renice-<process_name>.log

# Usage: ./renice-process.sh <process_name> [nice_value]
# Example: ./renice-process.sh 'MyProcess' +10

# Process name will match any process name that contains the string.
# For example, if the process name is 'MyProcess', it will match 'MyProcess', 'MyProcess2', 'MyProcess3', etc.


PROCESS_NAME="$1"
NICE_VALUE="${2:-+10}" # Default to +20 if not provided
# If using in a jamf script:
# PROCESS_NAME="$4"
# NICE_VALUE="${5:-+10}" # Default to +20 if not provided

# Constants
RETRY_INTERVAL=60 # seconds
MAX_RETRIES=10 # maximum number of retries
TOTAL_RUNTIME=600 # seconds (10 minutes)
START_TIME=$(date +%s)

print_usage() {
    echo "Usage: $0 <process_name> [nice_value]"
    echo "Example: $0 'MyProcess' +10"
}

# check if running as root
if [ "$(id -u)" -ne 0 ]; then
    echo "This script must be run as root. Please run with sudo."
    exit 1
fi
# check PROCESS_NAME is not empty
if [ -z "$PROCESS_NAME" ]; then
    echo "Process name is required."
    print_usage
    exit 1
fi
# NICE_VALUE needs to be in the range -20 to +20
if ! [[ "$NICE_VALUE" =~ ^[-+]?[0-9]+$ ]] || [ "$NICE_VALUE" -lt -20 ] || [ "$NICE_VALUE" -gt 20 ]; then
    echo "nice_value must be an integer between -20 and +20."
    print_usage
    exit 1
fi

# log file
LOG_FILE="/var/log/renice-${PROCESS_NAME}.log"
# Create log directory if it doesn't exist
mkdir -p "$(dirname "$LOG_FILE")"

write_log() {
  local message="$1"
  # echo to stdout
  echo "$message"
  # echo to log file
  echo "$(date +'%Y-%m-%d %H:%M:%S') - $message" >> "$LOG_FILE"
}

# Function to renice  processes
renice_process() {
    pids=$(pgrep -f "$PROCESS_NAME")

    if [ -n "$pids" ]; then
        # echo to stderr to avoid confusion with stdout
        write_log "Found one or more processes for ${PROCESS_NAME}:"
        for pid in $pids; do
            write_log "Evaluating $(get_process_name "$pid") with PID $pid"
            # Check if the process is already reniced to the desired value
            if [[ $(get_nice_value "$pid") -eq "$NICE_VALUE" ]]; then
                write_log "Process $pid already has nice value of ${NICE_VALUE}. Skipping..."
                continue
            fi
            /usr/bin/renice ${NICE_VALUE} -p "$pid"
            /usr/sbin/taskpolicy -b -p "$pid"
            write_log "Reniced process $pid to ${NICE_VALUE} priority and limit to E cores only"
        done
        return 0
    else
        return 1
    fi
}

# Return the current nice value of a given PID
get_nice_value() {
    ps -o ni= -p "$1"
}

# Return process name of a given PID
get_process_name() {
    ps -o comm= -p "$1"
}

# Main loop
retry_count=0
write_log "Starting renice script..."
while true; do
    CURRENT_TIME=$(date +%s)
    ELAPSED_TIME=$((CURRENT_TIME - START_TIME))

    if [ "$ELAPSED_TIME" -ge "$TOTAL_RUNTIME" ]; then
        echo "Maximum runtime (10 minutes) exceeded.  Exiting."
        exit 1
    fi

    renice_process
    return_code=$?

    if [[ $return_code == 0 ]]; then
        echo "${PROCESS_NAME} process reniced successfully."
        break
    else
        echo "${PROCESS_NAME} process not found. Retrying in $RETRY_INTERVAL seconds..."
        sleep "$RETRY_INTERVAL"
        retry_count=$((retry_count + 1))

        if [ "$retry_count" -ge "$MAX_RETRIES" ]; then
            echo "Maximum retries reached. Exiting."
            exit 1
        fi
    fi
done

Extra Credit

If we were talking about a Linux system using systemd, we can be quite explicit about setting process priority and resource limitations.

For example for a systemd process foo we can create an override that is persistant and sets the values we want:

sudo systemctl edit foo

will create /etc/systemd/system/foo.service.d/override.conf

In that we can say something like:

[Service]
CPUWeight=5
CPUQuota=10%

And the process, and child tasks, will be hard limited to 10% utilisation.

If only this type of override were possible in launchd, but unfortunatly for us, it’s not 🙂 (that I’m aware of - very happy to be corrected)


<
Previous
Script compacting script
>
Archive
All Articles