Skip to content

Commit f7bcb68

Browse files
sbhavaniclaude
andauthored
Add --max-concurrent-downloads flag for parallel layer downloads (#716)
Adds `--max-concurrent-downloads` flag to `container image pull` for configurable concurrent layer downloads. Fixes #715 Depends on apple/containerization#311 **Usage**: ```bash container image pull nginx:latest --max-concurrent-downloads 6 ``` **Changes**: - Add CLI flag (default: 3) - Thread parameter through XPC stack - Update to use forked containerization with configurable concurrency **Performance**: ~1.2-1.3x faster pulls for multi-layer images with higher concurrency **Tests**: Included standalone tests verify concurrency behavior and parameter flow --------- Co-authored-by: Claude <[email protected]>
1 parent 1e19a4d commit f7bcb68

File tree

11 files changed

+84
-11
lines changed

11 files changed

+84
-11
lines changed

Sources/ContainerClient/Core/ClientImage.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,13 @@ extension ClientImage {
220220
})
221221
}
222222

223-
public static func pull(reference: String, platform: Platform? = nil, scheme: RequestScheme = .auto, progressUpdate: ProgressUpdateHandler? = nil) async throws -> ClientImage {
223+
public static func pull(
224+
reference: String, platform: Platform? = nil, scheme: RequestScheme = .auto, progressUpdate: ProgressUpdateHandler? = nil, maxConcurrentDownloads: Int = 3
225+
) async throws -> ClientImage {
226+
guard maxConcurrentDownloads > 0 else {
227+
throw ContainerizationError(.invalidArgument, message: "maximum number of concurrent downloads must be greater than 0, got \(maxConcurrentDownloads)")
228+
}
229+
224230
let client = newXPCClient()
225231
let request = newRequest(.imagePull)
226232

@@ -234,6 +240,7 @@ extension ClientImage {
234240

235241
let insecure = try scheme.schemeFor(host: host) == .http
236242
request.set(key: .insecureFlag, value: insecure)
243+
request.set(key: .maxConcurrentDownloads, value: Int64(maxConcurrentDownloads))
237244

238245
var progressUpdateClient: ProgressUpdateClient?
239246
if let progressUpdate {
@@ -313,8 +320,9 @@ extension ClientImage {
313320
return (totalCount: total, activeCount: active, totalSize: size, reclaimableSize: reclaimable)
314321
}
315322

316-
public static func fetch(reference: String, platform: Platform? = nil, scheme: RequestScheme = .auto, progressUpdate: ProgressUpdateHandler? = nil) async throws -> ClientImage
317-
{
323+
public static func fetch(
324+
reference: String, platform: Platform? = nil, scheme: RequestScheme = .auto, progressUpdate: ProgressUpdateHandler? = nil, maxConcurrentDownloads: Int = 3
325+
) async throws -> ClientImage {
318326
do {
319327
let match = try await self.get(reference: reference)
320328
if let platform {
@@ -327,7 +335,7 @@ extension ClientImage {
327335
guard err.isCode(.notFound) else {
328336
throw err
329337
}
330-
return try await Self.pull(reference: reference, platform: platform, scheme: scheme, progressUpdate: progressUpdate)
338+
return try await Self.pull(reference: reference, platform: platform, scheme: scheme, progressUpdate: progressUpdate, maxConcurrentDownloads: maxConcurrentDownloads)
331339
}
332340
}
333341
}

Sources/ContainerClient/Flags.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,11 @@ public struct Flags {
215215
@Option(name: .long, help: ArgumentHelp("Progress type (format: none|ansi)", valueName: "type"))
216216
public var progress: ProgressType = .ansi
217217
}
218+
219+
public struct ImageFetch: ParsableArguments {
220+
public init() {}
221+
222+
@Option(name: .long, help: "Maximum number of concurrent downloads (default: 3)")
223+
public var maxConcurrentDownloads: Int = 3
224+
}
218225
}

Sources/ContainerClient/Utility.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public struct Utility {
9393
management: Flags.Management,
9494
resource: Flags.Resource,
9595
registry: Flags.Registry,
96+
imageFetch: Flags.ImageFetch,
9697
progressUpdate: @escaping ProgressUpdateHandler
9798
) async throws -> (ContainerConfiguration, Kernel) {
9899
var requestedPlatform = Parser.platform(os: management.os, arch: management.arch)
@@ -112,7 +113,8 @@ public struct Utility {
112113
reference: image,
113114
platform: requestedPlatform,
114115
scheme: scheme,
115-
progressUpdate: ProgressTaskCoordinator.handler(for: fetchTask, from: progressUpdate)
116+
progressUpdate: ProgressTaskCoordinator.handler(for: fetchTask, from: progressUpdate),
117+
maxConcurrentDownloads: imageFetch.maxConcurrentDownloads
116118
)
117119

118120
// Unpack a fetched image before use
@@ -140,7 +142,8 @@ public struct Utility {
140142
let fetchInitTask = await taskManager.startTask()
141143
let initImage = try await ClientImage.fetch(
142144
reference: ClientImage.initImageRef, platform: .current, scheme: scheme,
143-
progressUpdate: ProgressTaskCoordinator.handler(for: fetchInitTask, from: progressUpdate))
145+
progressUpdate: ProgressTaskCoordinator.handler(for: fetchInitTask, from: progressUpdate),
146+
maxConcurrentDownloads: imageFetch.maxConcurrentDownloads)
144147

145148
await progressUpdate([
146149
.setDescription("Unpacking init image"),

Sources/ContainerCommands/Container/ContainerCreate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ extension Application {
4040
@OptionGroup(title: "Registry options")
4141
var registryFlags: Flags.Registry
4242

43+
@OptionGroup(title: "Image fetch options")
44+
var imageFetchFlags: Flags.ImageFetch
45+
4346
@OptionGroup
4447
var global: Flags.Global
4548

@@ -73,6 +76,7 @@ extension Application {
7376
management: managementFlags,
7477
resource: resourceFlags,
7578
registry: registryFlags,
79+
imageFetch: imageFetchFlags,
7680
progressUpdate: progress.handler
7781
)
7882

Sources/ContainerCommands/Container/ContainerRun.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ extension Application {
4747
@OptionGroup(title: "Progress options")
4848
var progressFlags: Flags.Progress
4949

50+
@OptionGroup(title: "Image fetch options")
51+
var imageFetchFlags: Flags.ImageFetch
52+
5053
@OptionGroup
5154
var global: Flags.Global
5255

@@ -97,6 +100,7 @@ extension Application {
97100
management: managementFlags,
98101
resource: resourceFlags,
99102
registry: registryFlags,
103+
imageFetch: imageFetchFlags,
100104
progressUpdate: progress.handler
101105
)
102106

Sources/ContainerCommands/Image/ImagePull.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ extension Application {
3636
@OptionGroup
3737
var progressFlags: Flags.Progress
3838

39+
@OptionGroup
40+
var imageFetchFlags: Flags.ImageFetch
41+
3942
@Option(
4043
name: .shortAndLong,
4144
help: "Limit the pull to the specified architecture"
@@ -100,7 +103,8 @@ extension Application {
100103
let taskManager = ProgressTaskCoordinator()
101104
let fetchTask = await taskManager.startTask()
102105
let image = try await ClientImage.pull(
103-
reference: processedReference, platform: p, scheme: scheme, progressUpdate: ProgressTaskCoordinator.handler(for: fetchTask, from: progress.handler)
106+
reference: processedReference, platform: p, scheme: scheme, progressUpdate: ProgressTaskCoordinator.handler(for: fetchTask, from: progress.handler),
107+
maxConcurrentDownloads: self.imageFetchFlags.maxConcurrentDownloads
104108
)
105109

106110
progress.set(description: "Unpacking image")

Sources/Services/ContainerImagesService/Client/ImageServiceXPCKeys.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public enum ImagesServiceXPCKeys: String {
3535
case ociPlatform
3636
case insecureFlag
3737
case garbageCollect
38+
case maxConcurrentDownloads
3839

3940
/// ContentStore
4041
case digest

Sources/Services/ContainerImagesService/Server/ImageService.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,15 @@ public actor ImagesService {
5959
return try await imageStore.list().map { $0.description.fromCZ }
6060
}
6161

62-
public func pull(reference: String, platform: Platform?, insecure: Bool, progressUpdate: ProgressUpdateHandler?) async throws -> ImageDescription {
63-
self.log.info("ImagesService: \(#function) - ref: \(reference), platform: \(String(describing: platform)), insecure: \(insecure)")
62+
public func pull(reference: String, platform: Platform?, insecure: Bool, progressUpdate: ProgressUpdateHandler?, maxConcurrentDownloads: Int = 3) async throws
63+
-> ImageDescription
64+
{
65+
self.log.info(
66+
"ImagesService: \(#function) - ref: \(reference), platform: \(String(describing: platform)), insecure: \(insecure), maxConcurrentDownloads: \(maxConcurrentDownloads)")
6467
let img = try await Self.withAuthentication(ref: reference) { auth in
6568
try await self.imageStore.pull(
66-
reference: reference, platform: platform, insecure: insecure, auth: auth, progress: ContainerizationProgressAdapter.handler(from: progressUpdate))
69+
reference: reference, platform: platform, insecure: insecure, auth: auth, progress: ContainerizationProgressAdapter.handler(from: progressUpdate),
70+
maxConcurrentDownloads: maxConcurrentDownloads)
6771
}
6872
guard let img else {
6973
throw ContainerizationError(.internalError, message: "failed to pull image \(reference)")

Sources/Services/ContainerImagesService/Server/ImagesServiceHarness.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ public struct ImagesServiceHarness: Sendable {
4747
platform = try JSONDecoder().decode(ContainerizationOCI.Platform.self, from: platformData)
4848
}
4949
let insecure = message.bool(key: .insecureFlag)
50+
let maxConcurrentDownloads = message.int64(key: .maxConcurrentDownloads)
5051

5152
let progressUpdateService = ProgressUpdateService(message: message)
52-
let imageDescription = try await service.pull(reference: ref, platform: platform, insecure: insecure, progressUpdate: progressUpdateService?.handler)
53+
let imageDescription = try await service.pull(
54+
reference: ref, platform: platform, insecure: insecure, progressUpdate: progressUpdateService?.handler, maxConcurrentDownloads: Int(maxConcurrentDownloads))
5355

5456
let imageData = try JSONEncoder().encode(imageDescription)
5557
let reply = message.reply()

Sources/Services/ContainerSandboxService/SandboxService.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ public actor SandboxService {
119119
try bundle.createLogFile()
120120

121121
var config = try bundle.configuration
122+
122123
let vmm = VZVirtualMachineManager(
123124
kernel: try bundle.kernel,
124125
initialFilesystem: bundle.initialFilesystem.asMount,

0 commit comments

Comments
 (0)