Skip to content

Commit d10db98

Browse files
committed
Introduce preferredBufferSize parameter to allow custom buffer size when streaming subprocess output
1 parent 680621b commit d10db98

File tree

3 files changed

+153
-11
lines changed

3 files changed

+153
-11
lines changed

Sources/Subprocess/API.swift

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public func run<
106106
// MARK: - Custom Execution Body
107107

108108
/// Run an executable with given parameters and a custom closure
109-
/// to manage the running subprocess' lifetime and stream its standard output.
109+
/// to manage the running subprocess' lifetime.
110110
/// - Parameters:
111111
/// - executable: The executable to run.
112112
/// - arguments: The arguments to pass to the executable.
@@ -160,6 +160,11 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: Out
160160
/// when running the executable.
161161
/// - input: The input to send to the executable.
162162
/// - error: How to manager executable standard error.
163+
/// - preferredBufferSize: The preferred size in bytes for the buffer used when reading
164+
/// from the subprocess's standard error stream. If `nil`, uses the system page size
165+
/// as the default buffer size. Larger buffer sizes may improve performance for
166+
/// subprocesses that produce large amounts of output, while smaller buffer sizes
167+
/// may reduce memory usage and improve responsiveness for interactive applications.
163168
/// - isolation: the isolation context to run the body closure.
164169
/// - body: The custom execution body to manually control the running process
165170
/// - Returns an executableResult type containing the return value
@@ -172,6 +177,7 @@ public func run<Result, Input: InputProtocol, Error: OutputProtocol>(
172177
platformOptions: PlatformOptions = PlatformOptions(),
173178
input: Input = .none,
174179
error: Error = .discarded,
180+
preferredOutputSize: Int? = nil,
175181
isolation: isolated (any Actor)? = #isolation,
176182
body: ((Execution, AsyncBufferSequence) async throws -> Result)
177183
) async throws -> ExecutionResult<Result> where Error.OutputType == Void {
@@ -186,6 +192,7 @@ public func run<Result, Input: InputProtocol, Error: OutputProtocol>(
186192
configuration,
187193
input: input,
188194
error: error,
195+
preferredBufferSize: preferredOutputSize,
189196
isolation: isolation,
190197
body: body
191198
)
@@ -202,6 +209,11 @@ public func run<Result, Input: InputProtocol, Error: OutputProtocol>(
202209
/// when running the executable.
203210
/// - input: The input to send to the executable.
204211
/// - output: How to manager executable standard output.
212+
/// - preferredBufferSize: The preferred size in bytes for the buffer used when reading
213+
/// from the subprocess's standard error stream. If `nil`, uses the system page size
214+
/// as the default buffer size. Larger buffer sizes may improve performance for
215+
/// subprocesses that produce large amounts of output, while smaller buffer sizes
216+
/// may reduce memory usage and improve responsiveness for interactive applications.
205217
/// - isolation: the isolation context to run the body closure.
206218
/// - body: The custom execution body to manually control the running process
207219
/// - Returns an executableResult type containing the return value
@@ -214,6 +226,7 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol>(
214226
platformOptions: PlatformOptions = PlatformOptions(),
215227
input: Input = .none,
216228
output: Output,
229+
preferredBufferSize: Int? = nil,
217230
isolation: isolated (any Actor)? = #isolation,
218231
body: ((Execution, AsyncBufferSequence) async throws -> Result)
219232
) async throws -> ExecutionResult<Result> where Output.OutputType == Void {
@@ -228,6 +241,7 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol>(
228241
configuration,
229242
input: input,
230243
output: output,
244+
preferredBufferSize: preferredBufferSize,
231245
isolation: isolation,
232246
body: body
233247
)
@@ -244,6 +258,11 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol>(
244258
/// - platformOptions: The platform specific options to use
245259
/// when running the executable.
246260
/// - error: How to manager executable standard error.
261+
/// - preferredBufferSize: The preferred size in bytes for the buffer used when reading
262+
/// from the subprocess's standard output stream. If `nil`, uses the system page size
263+
/// as the default buffer size. Larger buffer sizes may improve performance for
264+
/// subprocesses that produce large amounts of output, while smaller buffer sizes
265+
/// may reduce memory usage and improve responsiveness for interactive applications.
247266
/// - isolation: the isolation context to run the body closure.
248267
/// - body: The custom execution body to manually control the running process
249268
/// - Returns an executableResult type containing the return value
@@ -255,6 +274,7 @@ public func run<Result, Error: OutputProtocol>(
255274
workingDirectory: FilePath? = nil,
256275
platformOptions: PlatformOptions = PlatformOptions(),
257276
error: Error = .discarded,
277+
preferredBufferSize: Int? = nil,
258278
isolation: isolated (any Actor)? = #isolation,
259279
body: ((Execution, StandardInputWriter, AsyncBufferSequence) async throws -> Result)
260280
) async throws -> ExecutionResult<Result> where Error.OutputType == Void {
@@ -268,6 +288,7 @@ public func run<Result, Error: OutputProtocol>(
268288
return try await run(
269289
configuration,
270290
error: error,
291+
preferredBufferSize: preferredBufferSize,
271292
isolation: isolation,
272293
body: body
273294
)
@@ -284,6 +305,11 @@ public func run<Result, Error: OutputProtocol>(
284305
/// - platformOptions: The platform specific options to use
285306
/// when running the executable.
286307
/// - output: How to manager executable standard output.
308+
/// - preferredBufferSize: The preferred size in bytes for the buffer used when reading
309+
/// from the subprocess's standard error stream. If `nil`, uses the system page size
310+
/// as the default buffer size. Larger buffer sizes may improve performance for
311+
/// subprocesses that produce large amounts of output, while smaller buffer sizes
312+
/// may reduce memory usage and improve responsiveness for interactive applications.
287313
/// - isolation: the isolation context to run the body closure.
288314
/// - body: The custom execution body to manually control the running process
289315
/// - Returns an executableResult type containing the return value
@@ -295,6 +321,7 @@ public func run<Result, Output: OutputProtocol>(
295321
workingDirectory: FilePath? = nil,
296322
platformOptions: PlatformOptions = PlatformOptions(),
297323
output: Output,
324+
preferredBufferSize: Int? = nil,
298325
isolation: isolated (any Actor)? = #isolation,
299326
body: ((Execution, StandardInputWriter, AsyncBufferSequence) async throws -> Result)
300327
) async throws -> ExecutionResult<Result> where Output.OutputType == Void {
@@ -308,6 +335,7 @@ public func run<Result, Output: OutputProtocol>(
308335
return try await run(
309336
configuration,
310337
output: output,
338+
preferredBufferSize: preferredBufferSize,
311339
isolation: isolation,
312340
body: body
313341
)
@@ -323,6 +351,11 @@ public func run<Result, Output: OutputProtocol>(
323351
/// - workingDirectory: The working directory in which to run the executable.
324352
/// - platformOptions: The platform specific options to use
325353
/// when running the executable.
354+
/// - preferredBufferSize: The preferred size in bytes for the buffer used when reading
355+
/// from the subprocess's standard output and error stream. If `nil`, uses the system page size
356+
/// as the default buffer size. Larger buffer sizes may improve performance for
357+
/// subprocesses that produce large amounts of output, while smaller buffer sizes
358+
/// may reduce memory usage and improve responsiveness for interactive applications.
326359
/// - isolation: the isolation context to run the body closure.
327360
/// - body: The custom execution body to manually control the running process
328361
/// - Returns an executableResult type containing the return value
@@ -333,6 +366,7 @@ public func run<Result>(
333366
environment: Environment = .inherit,
334367
workingDirectory: FilePath? = nil,
335368
platformOptions: PlatformOptions = PlatformOptions(),
369+
preferredBufferSize: Int? = nil,
336370
isolation: isolated (any Actor)? = #isolation,
337371
body: (
338372
(
@@ -352,6 +386,7 @@ public func run<Result>(
352386
)
353387
return try await run(
354388
configuration,
389+
preferredBufferSize: preferredBufferSize,
355390
isolation: isolation,
356391
body: body
357392
)
@@ -561,6 +596,11 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: Out
561596
/// - configuration: The configuration to run.
562597
/// - input: The input to send to the executable.
563598
/// - error: How to manager executable standard error.
599+
/// - preferredBufferSize: The preferred size in bytes for the buffer used when reading
600+
/// from the subprocess's standard output stream. If `nil`, uses the system page size
601+
/// as the default buffer size. Larger buffer sizes may improve performance for
602+
/// subprocesses that produce large amounts of output, while smaller buffer sizes
603+
/// may reduce memory usage and improve responsiveness for interactive applications.
564604
/// - isolation: the isolation context to run the body closure.
565605
/// - body: The custom execution body to manually control the running process
566606
/// - Returns an executableResult type containing the return value
@@ -569,6 +609,7 @@ public func run<Result, Input: InputProtocol, Error: OutputProtocol>(
569609
_ configuration: Configuration,
570610
input: Input = .none,
571611
error: Error = .discarded,
612+
preferredBufferSize: Int? = nil,
572613
isolation: isolated (any Actor)? = #isolation,
573614
body: ((Execution, AsyncBufferSequence) async throws -> Result)
574615
) async throws -> ExecutionResult<Result> where Error.OutputType == Void {
@@ -594,7 +635,10 @@ public func run<Result, Input: InputProtocol, Error: OutputProtocol>(
594635
}
595636

596637
// Body runs in the same isolation
597-
let outputSequence = AsyncBufferSequence(diskIO: outputIOBox.take()!.consumeIOChannel())
638+
let outputSequence = AsyncBufferSequence(
639+
diskIO: outputIOBox.take()!.consumeIOChannel(),
640+
preferredBufferSize: preferredBufferSize
641+
)
598642
let result = try await body(execution, outputSequence)
599643
try await group.waitForAll()
600644
return result
@@ -608,6 +652,11 @@ public func run<Result, Input: InputProtocol, Error: OutputProtocol>(
608652
/// - configuration: The configuration to run.
609653
/// - input: The input to send to the executable.
610654
/// - output: How to manager executable standard output.
655+
/// - preferredBufferSize: The preferred size in bytes for the buffer used when reading
656+
/// from the subprocess's standard error stream. If `nil`, uses the system page size
657+
/// as the default buffer size. Larger buffer sizes may improve performance for
658+
/// subprocesses that produce large amounts of output, while smaller buffer sizes
659+
/// may reduce memory usage and improve responsiveness for interactive applications.
611660
/// - isolation: the isolation context to run the body closure.
612661
/// - body: The custom execution body to manually control the running process
613662
/// - Returns an executableResult type containing the return value
@@ -616,6 +665,7 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol>(
616665
_ configuration: Configuration,
617666
input: Input = .none,
618667
output: Output,
668+
preferredBufferSize: Int? = nil,
619669
isolation: isolated (any Actor)? = #isolation,
620670
body: ((Execution, AsyncBufferSequence) async throws -> Result)
621671
) async throws -> ExecutionResult<Result> where Output.OutputType == Void {
@@ -641,7 +691,10 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol>(
641691
}
642692

643693
// Body runs in the same isolation
644-
let errorSequence = AsyncBufferSequence(diskIO: errorIOBox.take()!.consumeIOChannel())
694+
let errorSequence = AsyncBufferSequence(
695+
diskIO: errorIOBox.take()!.consumeIOChannel(),
696+
preferredBufferSize: preferredBufferSize
697+
)
645698
let result = try await body(execution, errorSequence)
646699
try await group.waitForAll()
647700
return result
@@ -655,13 +708,19 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol>(
655708
/// - Parameters:
656709
/// - configuration: The `Configuration` to run.
657710
/// - error: How to manager executable standard error.
711+
/// - preferredBufferSize: The preferred size in bytes for the buffer used when reading
712+
/// from the subprocess's standard output stream. If `nil`, uses the system page size
713+
/// as the default buffer size. Larger buffer sizes may improve performance for
714+
/// subprocesses that produce large amounts of output, while smaller buffer sizes
715+
/// may reduce memory usage and improve responsiveness for interactive applications.
658716
/// - isolation: the isolation context to run the body closure.
659717
/// - body: The custom execution body to manually control the running process
660718
/// - Returns an executableResult type containing the return value
661719
/// of the closure.
662720
public func run<Result, Error: OutputProtocol>(
663721
_ configuration: Configuration,
664722
error: Error = .discarded,
723+
preferredBufferSize: Int? = nil,
665724
isolation: isolated (any Actor)? = #isolation,
666725
body: ((Execution, StandardInputWriter, AsyncBufferSequence) async throws -> Result)
667726
) async throws -> ExecutionResult<Result> where Error.OutputType == Void {
@@ -673,7 +732,10 @@ public func run<Result, Error: OutputProtocol>(
673732
error: try error.createPipe()
674733
) { execution, inputIO, outputIO, errorIO in
675734
let writer = StandardInputWriter(diskIO: inputIO!)
676-
let outputSequence = AsyncBufferSequence(diskIO: outputIO!.consumeIOChannel())
735+
let outputSequence = AsyncBufferSequence(
736+
diskIO: outputIO!.consumeIOChannel(),
737+
preferredBufferSize: preferredBufferSize
738+
)
677739
return try await body(execution, writer, outputSequence)
678740
}
679741
}
@@ -684,13 +746,19 @@ public func run<Result, Error: OutputProtocol>(
684746
/// - Parameters:
685747
/// - configuration: The `Configuration` to run.
686748
/// - output: How to manager executable standard output.
749+
/// - preferredBufferSize: The preferred size in bytes for the buffer used when reading
750+
/// from the subprocess's standard error stream. If `nil`, uses the system page size
751+
/// as the default buffer size. Larger buffer sizes may improve performance for
752+
/// subprocesses that produce large amounts of output, while smaller buffer sizes
753+
/// may reduce memory usage and improve responsiveness for interactive applications.
687754
/// - isolation: the isolation context to run the body closure.
688755
/// - body: The custom execution body to manually control the running process
689756
/// - Returns an executableResult type containing the return value
690757
/// of the closure.
691758
public func run<Result, Output: OutputProtocol>(
692759
_ configuration: Configuration,
693760
output: Output,
761+
preferredBufferSize: Int? = nil,
694762
isolation: isolated (any Actor)? = #isolation,
695763
body: ((Execution, StandardInputWriter, AsyncBufferSequence) async throws -> Result)
696764
) async throws -> ExecutionResult<Result> where Output.OutputType == Void {
@@ -702,14 +770,24 @@ public func run<Result, Output: OutputProtocol>(
702770
error: try error.createPipe()
703771
) { execution, inputIO, outputIO, errorIO in
704772
let writer = StandardInputWriter(diskIO: inputIO!)
705-
let errorSequence = AsyncBufferSequence(diskIO: errorIO!.consumeIOChannel())
773+
let errorSequence = AsyncBufferSequence(
774+
diskIO: errorIO!.consumeIOChannel(),
775+
preferredBufferSize: preferredBufferSize
776+
)
706777
return try await body(execution, writer, errorSequence)
707778
}
708779
}
709780

710781
/// Run an executable with given parameters specified by a `Configuration`
782+
/// and a custom closure to manage the running subprocess' lifetime, write to its
783+
/// standard input, and stream its standard output and standard error.
711784
/// - Parameters:
712785
/// - configuration: The `Subprocess` configuration to run.
786+
/// - preferredBufferSize: The preferred size in bytes for the buffer used when reading
787+
/// from the subprocess's standard output and error stream. If `nil`, uses the system page size
788+
/// as the default buffer size. Larger buffer sizes may improve performance for
789+
/// subprocesses that produce large amounts of output, while smaller buffer sizes
790+
/// may reduce memory usage and improve responsiveness for interactive applications.
713791
/// - isolation: the isolation context to run the body closure.
714792
/// - body: The custom configuration body to manually control
715793
/// the running process, write to its standard input, stream
@@ -718,6 +796,7 @@ public func run<Result, Output: OutputProtocol>(
718796
/// of the closure.
719797
public func run<Result>(
720798
_ configuration: Configuration,
799+
preferredBufferSize: Int? = nil,
721800
isolation: isolated (any Actor)? = #isolation,
722801
body: ((Execution, StandardInputWriter, AsyncBufferSequence, AsyncBufferSequence) async throws -> Result)
723802
) async throws -> ExecutionResult<Result> {
@@ -730,8 +809,14 @@ public func run<Result>(
730809
error: try error.createPipe()
731810
) { execution, inputIO, outputIO, errorIO in
732811
let writer = StandardInputWriter(diskIO: inputIO!)
733-
let outputSequence = AsyncBufferSequence(diskIO: outputIO!.consumeIOChannel())
734-
let errorSequence = AsyncBufferSequence(diskIO: errorIO!.consumeIOChannel())
812+
let outputSequence = AsyncBufferSequence(
813+
diskIO: outputIO!.consumeIOChannel(),
814+
preferredBufferSize: preferredBufferSize
815+
)
816+
let errorSequence = AsyncBufferSequence(
817+
diskIO: errorIO!.consumeIOChannel(),
818+
preferredBufferSize: preferredBufferSize
819+
)
735820
return try await body(execution, writer, outputSequence, errorSequence)
736821
}
737822
}

Sources/Subprocess/AsyncBufferSequence.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ public struct AsyncBufferSequence: AsyncSequence, @unchecked Sendable {
3636
public typealias Element = Buffer
3737

3838
private let diskIO: DiskIO
39+
private let preferredBufferSize: Int
3940
private var buffer: [Buffer]
4041

41-
internal init(diskIO: DiskIO) {
42+
internal init(diskIO: DiskIO, preferredBufferSize: Int?) {
4243
self.diskIO = diskIO
4344
self.buffer = []
45+
self.preferredBufferSize = preferredBufferSize ?? readBufferSize
4446
}
4547

4648
public mutating func next() async throws -> Buffer? {
@@ -51,7 +53,7 @@ public struct AsyncBufferSequence: AsyncSequence, @unchecked Sendable {
5153
// Read more data
5254
let data = try await AsyncIO.shared.read(
5355
from: self.diskIO,
54-
upTo: readBufferSize
56+
upTo: self.preferredBufferSize
5557
)
5658
guard let data else {
5759
// We finished reading. Close the file descriptor now
@@ -77,13 +79,18 @@ public struct AsyncBufferSequence: AsyncSequence, @unchecked Sendable {
7779
}
7880

7981
private let diskIO: DiskIO
82+
private let preferredBufferSize: Int?
8083

81-
internal init(diskIO: DiskIO) {
84+
internal init(diskIO: DiskIO, preferredBufferSize: Int?) {
8285
self.diskIO = diskIO
86+
self.preferredBufferSize = preferredBufferSize
8387
}
8488

8589
public func makeAsyncIterator() -> Iterator {
86-
return Iterator(diskIO: self.diskIO)
90+
return Iterator(
91+
diskIO: self.diskIO,
92+
preferredBufferSize: self.preferredBufferSize
93+
)
8794
}
8895

8996
// [New API: 0.0.1]

0 commit comments

Comments
 (0)