Skip to content

Commit db9662b

Browse files
committed
Set the priority of processes launched for background indexing
Unfortunately, `setpriority` only allows reduction of a process’s priority and doesn’t support priority elevation (unless you are a super user). I still think that it’s valuable to set the process’s priority based on the task priority when it is launched because many indexing processes never get their priority escalated and should thus run in the background. On Windows, we can elevate the process’s priority. rdar://127474245
1 parent da84c30 commit db9662b

9 files changed

+214
-120
lines changed

Sources/Diagnose/DiagnoseCommand.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,8 +407,7 @@ public struct DiagnoseCommand: AsyncParsableCommand {
407407
// is responsible for showing the diagnose bundle location to the user
408408
if self.bundleOutputPath == nil {
409409
do {
410-
let process = try Process.launch(arguments: ["open", "-R", bundlePath.path], workingDirectory: nil)
411-
try await process.waitUntilExitSendingSigIntOnTaskCancellation()
410+
_ = try await Process.run(arguments: ["open", "-R", bundlePath.path], workingDirectory: nil)
412411
} catch {
413412
// If revealing the bundle in Finder should fail, we don't care. We still printed the bundle path to stdout.
414413
}

Sources/SKCore/TaskScheduler.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,3 +542,25 @@ fileprivate extension Collection<Int> {
542542
return result
543543
}
544544
}
545+
546+
/// Version of the `withTaskPriorityChangedHandler` where the body doesn't throw.
547+
fileprivate func withTaskPriorityChangedHandler(
548+
initialPriority: TaskPriority = Task.currentPriority,
549+
pollingInterval: Duration = .seconds(0.1),
550+
@_inheritActorContext operation: @escaping @Sendable () async -> Void,
551+
taskPriorityChanged: @escaping @Sendable () -> Void
552+
) async {
553+
do {
554+
try await withTaskPriorityChangedHandler(
555+
initialPriority: initialPriority,
556+
pollingInterval: pollingInterval,
557+
operation: operation as @Sendable () async throws -> Void,
558+
taskPriorityChanged: taskPriorityChanged
559+
)
560+
} catch is CancellationError {
561+
} catch {
562+
// Since `operation` does not throw, the only error we expect `withTaskPriorityChangedHandler` to throw is a
563+
// `CancellationError`, in which case we can just return.
564+
logger.fault("Unexpected error thrown from withTaskPriorityChangedHandler: \(error.forLogging)")
565+
}
566+
}

Sources/SKSupport/CMakeLists.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ add_library(SKSupport STATIC
88
FileSystem.swift
99
LineTable.swift
1010
PipeAsStringHandler.swift
11-
Process+LaunchWithWorkingDirectoryIfPossible.swift
12-
Process+WaitUntilExitWithCancellation.swift
11+
Process+Run.swift
1312
Random.swift
1413
Result.swift
1514
SwitchableProcessResultExitStatus.swift

Sources/SKSupport/Process+LaunchWithWorkingDirectoryIfPossible.swift

Lines changed: 0 additions & 70 deletions
This file was deleted.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
import LSPLogging
15+
import SwiftExtensions
16+
17+
import struct TSCBasic.AbsolutePath
18+
import class TSCBasic.Process
19+
import enum TSCBasic.ProcessEnv
20+
import struct TSCBasic.ProcessEnvironmentBlock
21+
import struct TSCBasic.ProcessResult
22+
23+
#if os(Windows)
24+
import WinSDK
25+
#endif
26+
27+
extension Process {
28+
/// Wait for the process to exit. If the task gets cancelled, during this time, send a `SIGINT` to the process.
29+
@discardableResult
30+
public func waitUntilExitSendingSigIntOnTaskCancellation() async throws -> ProcessResult {
31+
return try await withTaskCancellationHandler {
32+
try await waitUntilExit()
33+
} onCancel: {
34+
signal(SIGINT)
35+
}
36+
}
37+
38+
/// Launches a new process with the given parameters.
39+
///
40+
/// - Important: If `workingDirectory` is not supported on this platform, this logs an error and falls back to launching the
41+
/// process without the working directory set.
42+
private static func launch(
43+
arguments: [String],
44+
environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block,
45+
workingDirectory: AbsolutePath?,
46+
outputRedirection: OutputRedirection = .collect,
47+
startNewProcessGroup: Bool = true,
48+
loggingHandler: LoggingHandler? = .none
49+
) throws -> Process {
50+
let process =
51+
if let workingDirectory {
52+
Process(
53+
arguments: arguments,
54+
environmentBlock: environmentBlock,
55+
workingDirectory: workingDirectory,
56+
outputRedirection: outputRedirection,
57+
startNewProcessGroup: startNewProcessGroup,
58+
loggingHandler: loggingHandler
59+
)
60+
} else {
61+
Process(
62+
arguments: arguments,
63+
environmentBlock: environmentBlock,
64+
outputRedirection: outputRedirection,
65+
startNewProcessGroup: startNewProcessGroup,
66+
loggingHandler: loggingHandler
67+
)
68+
}
69+
do {
70+
try process.launch()
71+
} catch Process.Error.workingDirectoryNotSupported where workingDirectory != nil {
72+
// TODO (indexing): We need to figure out how to set the working directory on all platforms.
73+
logger.error(
74+
"Working directory not supported on the platform. Launching process without working directory \(workingDirectory!.pathString)"
75+
)
76+
return try Process.launch(
77+
arguments: arguments,
78+
environmentBlock: environmentBlock,
79+
workingDirectory: nil,
80+
outputRedirection: outputRedirection,
81+
startNewProcessGroup: startNewProcessGroup,
82+
loggingHandler: loggingHandler
83+
)
84+
}
85+
return process
86+
}
87+
88+
/// Runs a new process with the given parameters and waits for it to exit, sending SIGINT if this task is cancelled.
89+
///
90+
/// The process's priority tracks the priority of the current task.
91+
@discardableResult
92+
public static func run(
93+
arguments: [String],
94+
environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block,
95+
workingDirectory: AbsolutePath?,
96+
outputRedirection: OutputRedirection = .collect,
97+
startNewProcessGroup: Bool = true,
98+
loggingHandler: LoggingHandler? = .none
99+
) async throws -> ProcessResult {
100+
let process = try Self.launch(
101+
arguments: arguments,
102+
environmentBlock: environmentBlock,
103+
workingDirectory: workingDirectory,
104+
outputRedirection: outputRedirection,
105+
startNewProcessGroup: startNewProcessGroup,
106+
loggingHandler: loggingHandler
107+
)
108+
return try await withTaskPriorityChangedHandler(initialPriority: Task.currentPriority) { @Sendable in
109+
setProcessPriority(pid: process.processID, newPriority: Task.currentPriority)
110+
return try await process.waitUntilExitSendingSigIntOnTaskCancellation()
111+
} taskPriorityChanged: {
112+
setProcessPriority(pid: process.processID, newPriority: Task.currentPriority)
113+
}
114+
}
115+
}
116+
117+
/// Set the priority of the given process to a value that's equivalent to `newPriority` on the current OS.
118+
private func setProcessPriority(pid: Process.ProcessID, newPriority: TaskPriority) {
119+
#if os(Windows)
120+
guard let handle = OpenProcess(UInt32(PROCESS_SET_INFORMATION), /*bInheritHandle*/ false, UInt32(pid)) else {
121+
logger.error("Failed to get process handle for \(pid) to change its priority: \(GetLastError())")
122+
return
123+
}
124+
defer {
125+
CloseHandle(handle)
126+
}
127+
if !SetPriorityClass(handle, UInt32(newPriority.windowsProcessPriority)) {
128+
logger.error("Failed to set process priority of \(pid) to \(newPriority.rawValue): \(GetLastError())")
129+
}
130+
#elseif canImport(Darwin)
131+
// `setpriority` is only able to decrease a process's priority and cannot elevate it. Since Swift task’s priorities
132+
// can only be elevated, this means that we can effectively only change a process's priority once, when it is created.
133+
// All subsequent calls to `setpriority` will fail. Because of this, don't log an error.
134+
setpriority(PRIO_PROCESS, UInt32(pid), newPriority.posixProcessPriority)
135+
#else
136+
setpriority(__priority_which_t(PRIO_PROCESS.rawValue), UInt32(pid), newPriority.posixProcessPriority)
137+
#endif
138+
}
139+
140+
fileprivate extension TaskPriority {
141+
#if os(Windows)
142+
var windowsProcessPriority: Int32 {
143+
if self >= .high {
144+
// SourceKit-LSP’s request handling runs at `TaskPriority.high`, which corresponds to the normal priority class.
145+
return NORMAL_PRIORITY_CLASS
146+
}
147+
if self >= .medium {
148+
return BELOW_NORMAL_PRIORITY_CLASS
149+
}
150+
return IDLE_PRIORITY_CLASS
151+
}
152+
#else
153+
var posixProcessPriority: Int32 {
154+
if self >= .high {
155+
// SourceKit-LSP’s request handling runs at `TaskPriority.high`, which corresponds to the base 0 niceness value.
156+
return 0
157+
}
158+
if self >= .medium {
159+
return 5
160+
}
161+
if self >= .low {
162+
return 10
163+
}
164+
return 15
165+
}
166+
#endif
167+
}

Sources/SKSupport/Process+WaitUntilExitWithCancellation.swift

Lines changed: 0 additions & 28 deletions
This file was deleted.

Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -606,15 +606,14 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
606606
let stdoutHandler = PipeAsStringHandler { logMessageToIndexLog(logID, $0) }
607607
let stderrHandler = PipeAsStringHandler { logMessageToIndexLog(logID, $0) }
608608

609-
let process = try Process.launch(
609+
let result = try await Process.run(
610610
arguments: arguments,
611611
workingDirectory: nil,
612612
outputRedirection: .stream(
613613
stdout: { stdoutHandler.handleDataFromPipe(Data($0)) },
614614
stderr: { stderrHandler.handleDataFromPipe(Data($0)) }
615615
)
616616
)
617-
let result = try await process.waitUntilExitSendingSigIntOnTaskCancellation()
618617
logMessageToIndexLog(logID, "Finished in \(start.duration(to: .now))")
619618
switch result.exitStatus.exhaustivelySwitchable {
620619
case .terminated(code: 0):

Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -357,20 +357,19 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
357357
let stdoutHandler = PipeAsStringHandler { logMessageToIndexLog(logID, $0) }
358358
let stderrHandler = PipeAsStringHandler { logMessageToIndexLog(logID, $0) }
359359

360-
let process = try Process.launch(
361-
arguments: processArguments,
362-
workingDirectory: workingDirectory,
363-
outputRedirection: .stream(
364-
stdout: { stdoutHandler.handleDataFromPipe(Data($0)) },
365-
stderr: { stderrHandler.handleDataFromPipe(Data($0)) }
366-
)
367-
)
368360
// Time out updating of the index store after 2 minutes. We don't expect any single file compilation to take longer
369361
// than 2 minutes in practice, so this indicates that the compiler has entered a loop and we probably won't make any
370362
// progress here. We will try indexing the file again when it is edited or when the project is re-opened.
371363
// 2 minutes have been chosen arbitrarily.
372364
let result = try await withTimeout(.seconds(120)) {
373-
try await process.waitUntilExitSendingSigIntOnTaskCancellation()
365+
try await Process.run(
366+
arguments: processArguments,
367+
workingDirectory: workingDirectory,
368+
outputRedirection: .stream(
369+
stdout: { stdoutHandler.handleDataFromPipe(Data($0)) },
370+
stderr: { stderrHandler.handleDataFromPipe(Data($0)) }
371+
)
372+
)
374373
}
375374

376375
logMessageToIndexLog(logID, "Finished in \(start.duration(to: .now))")

0 commit comments

Comments
 (0)