Skip to content

Instantly share code, notes, and snippets.

@danmurrelljr
Last active June 2, 2026 12:13
Show Gist options
  • Select an option

  • Save danmurrelljr/b803613dd838ccd4ebd9e352629784ca to your computer and use it in GitHub Desktop.

Select an option

Save danmurrelljr/b803613dd838ccd4ebd9e352629784ca to your computer and use it in GitHub Desktop.
Thermal-, power-, and backlog-aware heavy workload management for iOS/Mac

Below is a fully generalized, copy-pasteable implementation of a battery-aware and thermal-conscious serial job queue and workload policy manager. This structure is designed to pace and serialize any resource-intensive on-device workloads (such as local AI/ML model inference, image processing, large-scale database operations, complex PDF exports, or web scraping pipelines) in iOS/macOS applications using Swift Concurrency and strict Swift 6 safety.

  • WorkPriority.swift: Priority levels for heavy tasks, determining queue precedence and workload gating.
  • WorkloadPolicy.swift: Thermal-, power-, and backlog-aware pacing for resource-intensive operations (see Apple ProcessInfo.thermalState).
  • JobQueue.swift: Serializes resource-heavy tasks with user-initiated priority tasks ahead of background tasks.
  • HeavyWorkloadService.swift: Usage example, a high-level app service orchestrating arbitrary resource-intensive tasks.

Production Integration Best Practices:

  1. Serial Execution Gating: Heavy workloads (like local LLMs, image filter chains, or database serialization) should run serially. Our queue is designed with a concurrency limit of 1 out-of-the-box (serial drainage via a single background task) to prevent concurrent operations from stressing memory and hardware, which often triggers system-enforced Out-Of-Memory (OOM) crashes.
  2. Mocking & Unit Tests: The WorkloadPolicy methods accept thermalState and isLowPowerModeEnabled with standard runtime defaults. During test execution, you can inject mocked properties to test your app's throttling policies without heating up physical hardware.
  3. Queue Drainage Callbacks: The queue exposes a jobCompletedPublisher that fires whenever a job finishes. Callers can subscribe to this publisher to automatically retry deferred background tasks when the queue starts clearing up.
import Foundation
import os
/// A high-level app service orchestrating arbitrary resource-intensive tasks (e.g. image filters, PDF export, large DB writes).
@MainActor
public final class HeavyWorkloadService {
private let queue: JobQueue
private let logger = Logger(subsystem: "com.example.app", category: "Workload")
public init() {
let policy = WorkloadPolicy()
self.queue = JobQueue(policy: policy)
}
/// Runs a heavy task immediately due to direct user request.
/// - Parameter taskInput: The input configuration for the heavy work.
/// - Returns: The processed output result.
public func processImmediately(taskInput: String) async throws -> String {
let jobId = "work_\(taskInput.hashValue)"
return try await queue.enqueue(jobId: jobId, priority: .userInitiated) {
try await self.performExpensiveWork(input: taskInput)
}
}
/// Schedules a background maintenance task if workload policies permit.
/// - Parameters:
/// - taskId: A unique identifier for the specific item to process.
/// - taskInput: The input details.
public func queueBackgroundProcess(taskId: String, taskInput: String) async {
let jobId = "bg_work_\(taskId)"
guard queue.canEnqueueBackgroundWork() else {
self.logger.info("Background queue throttling: cannot enqueue job \(jobId) at this time.")
return
}
do {
let result = try await queue.enqueue(jobId: jobId, priority: .background) {
try await self.performExpensiveWork(input: taskInput)
}
self.logger.info("Background processing complete for \(taskId): \(result.prefix(30))...")
} catch {
self.logger.error("Background task failed: \(error.localizedDescription)")
}
}
/// Dynamically elevates a background task if the user requests it.
/// - Parameter taskId: The identifier of the background job.
public func prioritizeTask(taskId: String) {
let jobId = "bg_work_\(taskId)"
queue.promote(jobId: jobId)
}
// MARK: - Mock Expensive Operations
private func performExpensiveWork(input: String) async throws -> String {
// Replace this with actual resource-heavy operation, e.g.:
// - CoreImage filters
// - PDF generation
// - CoreData batch imports
try await Task.sleep(for: .seconds(2.0)) // Simulate heavy 2.0s computation
return "Processed result for: \(input)"
}
}
import Combine
import Foundation
import UIKit
/// Serializes resource-heavy tasks with user-initiated priority tasks ahead of background tasks.
/// Concurrency is limited to 1 execution at a time to prevent overlapping resource-heavy sessions.
/// Isolated to @MainActor to guarantee thread safety of backlog state and sequence ordering.
@MainActor
public final class JobQueue {
/// Represents a single unit of work in the queue. The typed continuation is captured inside `run`,
/// keeping the queue heterogeneous without value-level type erasure.
private struct Job {
/// A unique identifier for the job.
let id: String
/// The execution priority level of the job.
var priority: WorkPriority
/// A monotonically increasing sequence number used for FIFO sorting within the same priority level.
let sequence: UInt64
/// Executes the work and resumes the caller's typed continuation.
let run: () async -> Void
/// Resumes the caller's continuation with a cancellation error. Used when a duplicate job replaces this one.
let cancel: () -> Void
}
private let policy: WorkloadPolicy
private var backlog: [Job] = []
private var nextSequence: UInt64 = 0
private var drainTask: Task<Void, Never>?
private var backgroundJobsScheduledThisSession = 0
private var cancellables = Set<AnyCancellable>()
private let jobCompletedSubject = PassthroughSubject<Void, Never>()
/// Fires after each job finishes, enabling callers to retry deferred background work.
public var jobCompletedPublisher: AnyPublisher<Void, Never> {
jobCompletedSubject.eraseToAnyPublisher()
}
/// Initializes a new job queue with a workload policy and hooks up foreground session observers.
/// - Parameter policy: The workload policy enforcing thermal/power throttling rules.
public init(policy: WorkloadPolicy) {
self.policy = policy
// Listen to willEnterForegroundNotification to reset session limits (iOS/iPadOS).
// For macOS applications, adapt or omit this observer.
NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.resetForegroundSession()
}
.store(in: &cancellables)
}
/// The number of background jobs currently waiting in the queue.
public var pendingBackgroundJobCount: Int {
backlog.filter { $0.priority == .background }.count
}
/// Checks if a new background task is allowed to be enqueued under current policy parameters.
/// - Returns: `true` if background work is permitted; otherwise, `false`.
public func canEnqueueBackgroundWork() -> Bool {
policy.canEnqueueBackgroundWork(
pendingBackgroundJobs: pendingBackgroundJobCount,
backgroundJobsScheduledThisSession: backgroundJobsScheduledThisSession
)
}
/// Resets the background session task limit counter.
public func resetForegroundSession() {
backgroundJobsScheduledThisSession = 0
}
/// Enqueues a heavy task. Suspends the caller until the job's closure completes execution.
/// This uses continuation closure capturing to support full type safety (`T`) without casting.
/// - Parameters:
/// - jobId: A unique identifier for the job.
/// - priority: The execution priority for the job.
/// - work: An escaping async closure that executes the actual work.
/// - Returns: The resulting generic value `T`.
/// - Throws: An error if the closure throws.
public func enqueue<T>(
jobId: String,
priority: WorkPriority,
work: @escaping () async throws -> T
) async throws -> T {
if priority == .background {
backgroundJobsScheduledThisSession += 1
}
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<T, Error>) in
let seq = nextSequence
nextSequence += 1
let job = Job(
id: jobId,
priority: priority,
sequence: seq,
run: {
do {
continuation.resume(returning: try await work())
} catch {
continuation.resume(throwing: error)
}
},
cancel: { continuation.resume(throwing: CancellationError()) }
)
insert(job)
scheduleDrain()
}
}
/// Upgrades a pending background job's priority to user-initiated so that it jumps ahead of other background tasks.
/// - Parameter jobId: The identifier of the job to promote.
public func promote(jobId: String) {
var changed = false
for idx in backlog.indices where backlog[idx].id == jobId {
if backlog[idx].priority != .userInitiated {
backlog[idx].priority = .userInitiated
changed = true
}
}
if changed {
sortBacklog()
}
}
// MARK: - Core Queue Management
private func insert(_ job: Job) {
// Pending job with same id is cancelled and replaced to avoid redundant runs.
if let existingIdx = backlog.firstIndex(where: { $0.id == job.id }) {
let replaced = backlog.remove(at: existingIdx)
replaced.cancel()
}
backlog.append(job)
sortBacklog()
}
private func sortBacklog() {
backlog.sort { (a, b) in
if a.priority != b.priority {
return a.priority > b.priority
}
return a.sequence < b.sequence
}
}
private func scheduleDrain() {
guard drainTask == nil else { return }
drainTask = Task {
await drainLoop()
}
}
private func drainLoop() async {
while !backlog.isEmpty {
let job = backlog.removeFirst()
// Apply delay based on the workload policy
let delay = policy.interJobDelay(
for: job.priority,
pendingBackgroundJobs: pendingBackgroundJobCount
)
if delay > .zero {
try? await Task.sleep(for: delay)
}
await job.run()
jobCompletedSubject.send()
}
drainTask = nil
}
}
import Foundation
/// Thermal-, power-, and backlog-aware pacing for resource-intensive operations (see Apple `ProcessInfo.thermalState`).
public final class WorkloadPolicy: Sendable {
public struct Configuration: Sendable {
public let maxPendingBackgroundJobs: Int
public let maxBackgroundJobsPerForegroundSession: Int
public let baseNominalDelaySeconds: Double
public let baseFairDelaySeconds: Double
public let baseSeriousDelaySeconds: Double
public let baseCriticalDelaySeconds: Double
public init(
maxPendingBackgroundJobs: Int = 8,
maxBackgroundJobsPerForegroundSession: Int = 20,
baseNominalDelaySeconds: Double = 1.0,
baseFairDelaySeconds: Double = 2.0,
baseSeriousDelaySeconds: Double = 4.5,
baseCriticalDelaySeconds: Double = 8.0
) {
self.maxPendingBackgroundJobs = maxPendingBackgroundJobs
self.maxBackgroundJobsPerForegroundSession = maxBackgroundJobsPerForegroundSession
self.baseNominalDelaySeconds = baseNominalDelaySeconds
self.baseFairDelaySeconds = baseFairDelaySeconds
self.baseSeriousDelaySeconds = baseSeriousDelaySeconds
self.baseCriticalDelaySeconds = baseCriticalDelaySeconds
}
}
public let config: Configuration
public init(config: Configuration = Configuration()) {
self.config = config
}
/// Gates whether a new background task can be enqueued based on the current workload backlog, session budget, and system thermal/power state.
/// - Parameters:
/// - pendingBackgroundJobs: The number of background jobs currently waiting in the queue.
/// - backgroundJobsScheduledThisSession: The total number of background jobs scheduled in the current foreground session.
/// - thermalState: The active device thermal state. Defaults to `ProcessInfo.processInfo.thermalState`.
/// - isLowPowerModeEnabled: A boolean flag indicating whether the user's device is in Low Power Mode. Defaults to `ProcessInfo.processInfo.isLowPowerModeEnabled`.
/// - Returns: A boolean indicating whether the task can be safely enqueued.
public func canEnqueueBackgroundWork(
pendingBackgroundJobs: Int,
backgroundJobsScheduledThisSession: Int,
thermalState: ProcessInfo.ThermalState = ProcessInfo.processInfo.thermalState,
isLowPowerModeEnabled: Bool = ProcessInfo.processInfo.isLowPowerModeEnabled
) -> Bool {
guard backgroundJobsScheduledThisSession < config.maxBackgroundJobsPerForegroundSession else { return false }
guard pendingBackgroundJobs < config.maxPendingBackgroundJobs else { return false }
switch thermalState {
case .critical:
return false
case .serious:
return pendingBackgroundJobs < max(1, config.maxPendingBackgroundJobs / 2)
case .fair, .nominal:
break
@unknown default:
break
}
if isLowPowerModeEnabled, pendingBackgroundJobs >= max(1, config.maxPendingBackgroundJobs / 2) {
return false
}
return true
}
/// Computes the safety cooldown delay required before starting the next job, giving the SoC time to cool down between heavy runs.
/// - Parameters:
/// - priority: The priority of the next job to execute. User-initiated jobs have shorter cooldowns.
/// - pendingBackgroundJobs: The backlog count of background tasks waiting in the queue.
/// - thermalState: The active device thermal state. Defaults to `ProcessInfo.processInfo.thermalState`.
/// - isLowPowerModeEnabled: A boolean flag indicating whether the device is in Low Power Mode. Defaults to `ProcessInfo.processInfo.isLowPowerModeEnabled`.
/// - Returns: A `Duration` indicating how long the worker should sleep.
public func interJobDelay(
for priority: WorkPriority,
pendingBackgroundJobs: Int,
thermalState: ProcessInfo.ThermalState = ProcessInfo.processInfo.thermalState,
isLowPowerModeEnabled: Bool = ProcessInfo.processInfo.isLowPowerModeEnabled
) -> Duration {
// 1. User-initiated jobs bypass standard background delays.
// We only introduce tiny sub-second delays if the device is actively running hot,
// ensuring high responsiveness while still offering minimal thermal mitigation.
if priority == .userInitiated {
switch thermalState {
case .critical:
return .milliseconds(800)
case .serious:
return .milliseconds(350)
case .fair:
return .milliseconds(150)
case .nominal:
return .zero
@unknown default:
return .zero
}
}
// 2. Background jobs are throttled step-wise according to the system thermal state.
// As the device heats up, we force longer pauses between runs to allow
// the hardware (SoC) to cool down.
var seconds: Double
switch thermalState {
case .nominal:
seconds = config.baseNominalDelaySeconds
case .fair:
seconds = config.baseFairDelaySeconds
case .serious:
seconds = config.baseSeriousDelaySeconds
case .critical:
seconds = config.baseCriticalDelaySeconds
@unknown default:
seconds = config.baseNominalDelaySeconds
}
// 3. Apply a fixed penalty under Low Power Mode.
// This spreads background energy draw over time to protect battery health/longevity.
if isLowPowerModeEnabled {
seconds += 0.6
}
// 4. Dynamic Backlog Pacing.
// If a large batch of work is queued (e.g. initial launch indexing or feed refresh),
// we add 250ms per pending task (up to a maximum cap of 2.0s) to flatten the CPU/GPU
// utilization curve and prevent sustained high temperatures.
let backlogBoost = min(Double(pendingBackgroundJobs) * 0.25, 2.0)
seconds += backlogBoost
return .milliseconds(Int(seconds * 1_000))
}
}
import Foundation
/// Priority levels for heavy tasks, determining queue precedence and workload gating.
public enum WorkPriority: Int, Comparable, Sendable {
/// Low priority tasks run in the background (e.g. pre-generating suggestions, cache pre-warming, indexing).
/// These are heavily throttled by thermal and battery constraints.
case background = 0
/// High priority tasks initiated directly by user interaction (e.g. user taps "Export Now").
/// These run with minimal delays and bypass certain background policy limits.
case userInitiated = 1
public static func < (lhs: WorkPriority, rhs: WorkPriority) -> Bool {
lhs.rawValue < rhs.rawValue
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment