Files
swift-mirror/test/Concurrency/async_task_priority.swift

369 lines
13 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// RUN: %empty-directory(%t)
// RUN: %target-build-swift %s -parse-as-library -o %t/async_task_priority
// RUN: %target-codesign %t/async_task_priority
// RUN: %target-run %t/async_task_priority
// REQUIRES: VENDOR=apple
// REQUIRES: executable_test
// REQUIRES: concurrency
// REQUIRES: libdispatch
// rdar://76038845
// REQUIRES: concurrency_runtime
// UNSUPPORTED: back_deployment_runtime
// UNSUPPORTED: back_deploy_concurrency
// rdar://101077408 Temporarily disable on watchOS & iOS simulator
// UNSUPPORTED: DARWIN_SIMULATOR=watchos
// UNSUPPORTED: DARWIN_SIMULATOR=ios
// UNSUPPORTED: DARWIN_SIMULATOR=tvos
// rdar://107390341 - Temporarily disable for arm64e
// UNSUPPORTED: CPU=arm64e
import Darwin
@preconcurrency import Dispatch
import StdlibUnittest
@available(SwiftStdlib 5.9, *)
func loopUntil(priority: TaskPriority) async {
var currentPriority = Task.currentPriority
while (currentPriority != priority) {
print("Current priority = \(currentPriority) != \(priority)")
await Task.sleep(1_000_000)
currentPriority = Task.currentPriority
}
}
func print(_ s: String = "") {
fputs("\(s)\n", stderr)
}
@available(SwiftStdlib 5.9, *)
func expectedBasePri(priority: TaskPriority) -> TaskPriority {
let basePri = Task.basePriority!
print("Testing basePri matching expected pri - \(basePri) == \(priority)")
expectEqual(basePri, priority)
withUnsafeCurrentTask { unsafeTask in
guard let unsafeTask else {
fatalError("Expected to be able to get current task, but could not!")
}
// The UnsafeCurrentTask must return the same value
expectEqual(basePri, unsafeTask.basePriority)
}
return basePri
}
@available(SwiftStdlib 5.9, *)
func expectedEscalatedPri(priority: TaskPriority) -> TaskPriority {
let curPri = Task.currentPriority
print("Testing escalated matching expected pri - \(curPri) == \(priority)")
expectEqual(curPri, priority)
return curPri
}
@available(SwiftStdlib 5.9, *)
func testNestedTaskPriority(basePri: TaskPriority, curPri: TaskPriority) async {
let _ = expectedBasePri(priority: basePri)
let _ = expectedEscalatedPri(priority: curPri)
}
@available(SwiftStdlib 5.9, *)
func childTaskWaitingForEscalation(sem: DispatchSemaphore, basePri: TaskPriority, curPri : TaskPriority) async {
sem.wait() /* Wait to be escalated */
let _ = await testNestedTaskPriority(basePri: basePri, curPri: curPri)
}
@available(SwiftStdlib 5.9, *)
actor Test {
private var value = 0
init() { }
func increment() -> Int {
let cur = value
value = value + 1
return cur
}
func blockActorThenIncrement(semToSignal: DispatchSemaphore, semToWait : DispatchSemaphore, priExpected: TaskPriority) -> Int {
semToSignal.signal()
semToWait.wait();
sleep(1)
// FIXME: insert a test to verify that thread priority has actually escalated to match priExpected
return increment()
}
}
@main struct Main {
static func main() async {
let top_level = Task.detached { /* To detach from main actor when running work */
let tests = TestSuite("Task Priority manipulations")
if #available(SwiftStdlib 5.9, *) {
tests.test("Basic escalation test when task is running") {
let parentPri = Task.currentPriority
let sem = DispatchSemaphore(value: 0)
let task = Task.detached(priority: .background) {
let _ = expectedBasePri(priority: .background)
// Wait until task is running before asking to be escalated
sem.signal()
sleep(1)
let _ = expectedEscalatedPri(priority: parentPri)
}
// Wait till child runs and asks to be escalated
sem.wait()
await task.value
}
tests.test("Basic escalation when task is suspended") {
let parentPri = Task.currentPriority
let task = Task(priority: .background) {
await loopUntil(priority: parentPri) /* Suspend task until it is escalated */
let _ = expectedEscalatedPri(priority: parentPri)
}
await task.value // Escalate task BG -> DEF
}
tests.test("Structured concurrency priority propagation") {
let task = Task(priority: .background) {
await loopUntil(priority: .medium)
let basePri = expectedBasePri(priority: .background)
let curPri = expectedEscalatedPri(priority: .medium)
// Structured concurrency via async let, escalated priority of
// parent should propagate
print("Testing propagation for async let structured concurrency child")
async let child: () = testNestedTaskPriority(basePri: basePri, curPri: curPri)
await child
let dispatchGroup = DispatchGroup()
// Structured concurrency via task groups, escalated priority should
// propagate
await withTaskGroup(of: Void.self, returning: Void.self) { group in
dispatchGroup.enter()
group.addTask {
print("Testing propagation for task group regular child")
let _ = await testNestedTaskPriority(basePri: basePri, curPri: curPri)
dispatchGroup.leave()
return
}
dispatchGroup.enter()
group.addTask(priority: .utility) {
print("Testing propagation for task group child with specified priority")
let _ = await testNestedTaskPriority(basePri: .utility, curPri: curPri)
dispatchGroup.leave()
return
}
// Wait for child tasks to finish running, don't await since that
// will escalate them
dispatchGroup.wait()
}
}
await task.value // Escalate task BG->DEF
}
tests.test("Unstructured tasks priority propagation") {
let task = Task.detached(priority: .background) {
await loopUntil(priority: .medium)
let basePri = expectedBasePri(priority: .background)
let _ = expectedEscalatedPri(priority: .medium)
let group = DispatchGroup()
// Create an unstructured task
group.enter()
let _ = Task {
let _ = await testNestedTaskPriority(basePri: basePri, curPri: basePri)
group.leave()
}
// Wait for unstructured task to finish running, don't await it
// since that will escalate
group.wait()
}
await task.value // Escalate task BG->DEF
}
tests.test("Task escalation propagation to structured concurrency child tasks") {
// Create a task tree and then escalate the parent
let parentPri = Task.currentPriority
let basePri : TaskPriority = .background
let sem = DispatchSemaphore(value: 0)
let sem2 = DispatchSemaphore(value: 0)
let task = Task.detached(priority: basePri) {
async let child = childTaskWaitingForEscalation(sem: sem2, basePri: basePri, curPri: parentPri)
await withTaskGroup { group in
group.addTask {
let _ = await childTaskWaitingForEscalation(sem: sem2, basePri: basePri, curPri: parentPri)
}
group.addTask(priority: .utility) {
let _ = await childTaskWaitingForEscalation(sem: sem2, basePri: .utility, curPri: parentPri)
}
sem.signal() // Ask for escalation after creating full task tree
sleep(1)
let _ = expectedBasePri(priority: basePri)
let _ = expectedEscalatedPri(priority: parentPri)
sem2.signal() // Ask async let child to evaluate
sem2.signal() // Ask task group child 1 to evaluate
sem2.signal() // Ask task group child 2 to evaluate
}
}
// Wait until children are created and then ask for escalation of top level
sem.wait()
await task.value
}
tests.test("Simple task escalation to a future") {
let task1Pri: TaskPriority = .background
let task2Pri: TaskPriority = .utility
let parentPri: TaskPriority = Task.currentPriority
print("Top level task current priority = \(parentPri)")
// After task2 has suspended waiting for task1,
// escalating task2 should cause task1 to escalate
let task1 = Task.detached(priority: task1Pri) {
// Wait until task2 has blocked on task1 and escalated it
sleep(1)
_ = expectedEscalatedPri(priority: task2Pri)
// Wait until task2 itself has been escalated
sleep(5)
_ = expectedEscalatedPri(priority: parentPri)
}
let task2 = Task.detached(priority: task2Pri) {
await task1.value
}
// Wait for task2 and task1 to run and for task2 to now block on
// task1
sleep(3)
await task2.value
}
tests.test("Simple task escalation to a future 2") {
// top level task -> unstructured task2 -> child task -> unstructured
// task1
let task1Pri: TaskPriority = .background
let task2Pri: TaskPriority = .utility
let parentPri: TaskPriority = Task.currentPriority
print("Top level task current priority = \(parentPri)")
let task1 = Task(priority: task1Pri) {
await loopUntil(priority: parentPri)
}
sleep(1) // Wait for task1 to start running
let task2 = Task.detached(priority: task2Pri) {
@Sendable func childTask() async {
await task1.value
}
async let child = childTask()
}
sleep(1) // Wait for task2 to start running
await task2.value
}
tests.test("Task escalation of a task enqueued on an actor") {
let task1Pri: TaskPriority = .background
let task2Pri: TaskPriority = .background
let parentPri: TaskPriority = Task.currentPriority
let sem1 = DispatchSemaphore(value: 0) // to unblock enqueue of task2
let sem2 = DispatchSemaphore(value: 0)
let testActor = Test()
let task1 = Task.detached(priority: task1Pri) {
_ = expectedBasePri(priority: task1Pri)
await testActor.blockActorThenIncrement(semToSignal: sem1, semToWait: sem2, priExpected: parentPri);
}
sem1.wait() // Wait until task1 is on the actor
let task2 = Task(priority: task2Pri) {
_ = expectedBasePri(priority: task2Pri)
await testActor.increment()
}
sleep(1)
sem2.signal() // task2 is probably enqueued on the actor at this point, unblock task1
await task2.value // Escalate task2 which should be queued behind task1 on the actor
}
// This test will only work properly if Dispatch supports lowering the base priority of a thread
if #available(macOS 9998, iOS 9998, tvOS 9998, watchOS 9998, *) {
tests.test("Task escalation doesn't impact qos_class_self") {
let task = Task(priority: .utility) {
let initialQos = DispatchQoS(
qosClass: DispatchQoS.QoSClass(rawValue: qos_class_self())!,
relativePriority: 0)
expectEqual(initialQos, DispatchQoS.utility)
let innerTask = Task {
let qosBeforeEscalate = DispatchQoS(
qosClass: DispatchQoS.QoSClass(rawValue: qos_class_self())!,
relativePriority: 0)
// Unstructured task should inherit utility priority
expectEqual(qosBeforeEscalate, DispatchQoS.utility)
// Escalate priority override, not base QoS
withUnsafeCurrentTask {
$0!.escalatePriority(to: .userInitiated)
}
let qosAfterEscalate = DispatchQoS(
qosClass: DispatchQoS.QoSClass(rawValue: qos_class_self())!,
relativePriority: 0)
// qos_class_self should remain utility after escalation
expectEqual(qosAfterEscalate, DispatchQoS.utility)
await Task.yield()
let qosAfterYield = DispatchQoS(
qosClass: DispatchQoS.QoSClass(rawValue: qos_class_self())!,
relativePriority: 0)
// qos_class_self should remain utility after yield
expectEqual(qosAfterYield, DispatchQoS.utility)
}
await innerTask.value
}
await task.value
}
}
}
await runAllTestsAsync()
}
await top_level.value
}
}