I finished writing a Swift library, and I need to test its performance on some examples.
I am creating a script in Swift to take some data into files, execute them using my library, and then return results such as execution time.
Among the list of these different examples extracted from these files, some of them could take hours to execute, whereas others could only take some seconds. I do not expect to have an answer for all examples, and I just want the results of those that finish in less than 'x' seconds. For this reason, I want to introduce a timeout if a call of my function takes more than 'x' seconds. It should stop the current execution and go to the next one.
I looked for solutions, some of them using async/await or dispatchQueue, but nothing that worked for me. Below is one of the solutions that I tried:
func evalWithTimer(key: String, somethingToExecute: SomethingToExecute) -> SVS? {
var res: SVS? = nil
// Create a semaphore
let semaphore = DispatchSemaphore(value: 0)
// Define the time limit in seconds
let timeLimit: DispatchTime = .now() + 1 // 1 seconds
// Define the task you want to run (replace this with your actual task)
let task = {
res = somethingToExecute.eval()
semaphore.signal() // Signal that the task is done
}
// Execute the task asynchronously
DispatchQueue.global().async(execute: task)
// Wait for the task to complete or time out
let result = semaphore.wait(timeout: timeLimit)
if result == .timedOut {
// Task did not complete within the time limit, you can cancel it if needed
print("Task evaluation timed out and was canceled")
return res
} else {
// Task completed within the time limit
print("Task evaluation completed within the time limit")
return res
}
}
My function evalWithTimer is called for different values inside somethingToExecute. When I call eval()
on it, it can take more than 1 second, and it should be stopped if this is true. In this current version, it does not work because the task eval()
is not canceled correctly. At least this is my hypothesis. Even adding task.cancel()
when the timeout is true does not change anything.
The only solutions I found add an asynchronous mechanism, which can be understandable. I am looking for a solution to this problem, based or not on the code above.
Note that somethingToExecute.eval()
is not asynchronous.
I would suggest using XCTest if you are doing "tests". You can wait for XCTestExpectation
s with a timeout.
func testExample() {
let expect = expectation(description: "Eval completed in 1 second")
DispatchQueue.global().async {
somethingToExecute.eval()
expect.fulfill()
}
wait(for: [expect], timeout: 1)
}
When the timeout is exceeded, the test process gets killed.
In general though, stopping things should be a cooperative effort. eval
should check whether itself is cancelled, and if it is, stop whatever it is doing.
For example, using Swift Concurrency, eval
can check Task.isCancelled
at appropriate points in its execution, or if it throws
, you can try Task.checkCancellation()
instead.
Suppose eval
is a long-running loop, you can put the check in each iteration:
while something {
if Task.isCancelled { return nil /* or however you'd like to stop it*/ }
// or in a throwing function:
// try Task.checkCancellation()
// do work
}
Then, you can make two tasks, one runs eval
, another measures 1 second and then cancels the first one.
func evalWithTimer(key: String, somethingToExecute: SomethingToExecute) async throws -> SVS? {
let evalTask = Task {
somethingToExecute.eval()
// if eval is throwing,
// try somethingToExecute.eval()
}
let timeoutTask = Task {
try await Task.sleep(for: .seconds(1))
evalTask.cancel()
// here you know the timeout is exceeded
}
// if eval is throwing, "try" await, a CancellationError would be thrown if the timeout exceeds
let result = await evalTask.value
timeoutTask.cancel()
return result
}