Skip to content

Commit aa17664

Browse files
authored
Merge pull request #1434 from ahoppen/process-priority
Set the priority of processes launched for background indexing
2 parents 0ee9351 + db9662b commit aa17664

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

Sources/SKSupport/CMakeLists.txt

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

Sources/SKSupport/Process+LaunchWithWorkingDirectoryIfPossible.swift

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

Sources/SKSupport/Process+Run.swift

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
@@ -623,15 +623,14 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
623623
let stdoutHandler = PipeAsStringHandler { logMessageToIndexLog(logID, $0) }
624624
let stderrHandler = PipeAsStringHandler { logMessageToIndexLog(logID, $0) }
625625

626-
let process = try Process.launch(
626+
let result = try await Process.run(
627627
arguments: arguments,
628628
workingDirectory: nil,
629629
outputRedirection: .stream(
630630
stdout: { stdoutHandler.handleDataFromPipe(Data($0)) },
631631
stderr: { stderrHandler.handleDataFromPipe(Data($0)) }
632632
)
633633
)
634-
let result = try await process.waitUntilExitSendingSigIntOnTaskCancellation()
635634
logMessageToIndexLog(logID, "Finished in \(start.duration(to: .now))")
636635
switch result.exitStatus.exhaustivelySwitchable {
637636
case .terminated(code: 0):

Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift

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

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

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

0 commit comments

Comments
 (0)