diff --git a/Sources/Services/ContainerAPIService/Client/Parser.swift b/Sources/Services/ContainerAPIService/Client/Parser.swift index baa3002d..ef209df5 100644 --- a/Sources/Services/ContainerAPIService/Client/Parser.swift +++ b/Sources/Services/ContainerAPIService/Client/Parser.swift @@ -24,6 +24,14 @@ import ContainerizationOS import Foundation import SystemPackage +// MARK: - Collection capacity hints +// Methods in this file build arrays and dictionaries in loops where the final +// size is known from the input parameter count. reserveCapacity() and +// Dictionary(minimumCapacity:) avoid O(log n) reallocation copies as the +// collection grows incrementally. While this is a micro-optimization for each +// individual call, these methods execute on every `container run/create` and +// the savings compound at scale. + /// A parsed volume specification from user input public struct ParsedVolume { public let name: String @@ -124,7 +132,7 @@ public struct Parser { } combined.append(contentsOf: Parser.env(envList: envs)) - let deduped = combined.reduce(into: [String: String]()) { map, entry in + let deduped = combined.reduce(into: [String: String](minimumCapacity: combined.count)) { map, entry in let key = String(entry.split(separator: "=", maxSplits: 1).first ?? Substring(entry)) map[key] = entry } @@ -232,7 +240,7 @@ public struct Parser { } public static func labels(_ rawLabels: [String]) throws -> [String: String] { - var result: [String: String] = [:] + var result: [String: String] = Dictionary(minimumCapacity: rawLabels.count) for label in rawLabels { if label.isEmpty { throw ContainerizationError(.invalidArgument, message: "label cannot be an empty string") @@ -330,8 +338,9 @@ public struct Parser { public static let defaultDirectives = ["type": "virtiofs"] public static func tmpfsMounts(_ mounts: [String]) throws -> [Filesystem] { - var result: [Filesystem] = [] let mounts = mounts.dedupe() + var result: [Filesystem] = [] + result.reserveCapacity(mounts.count) for tmpfs in mounts { let fs = Filesystem.tmpfs(destination: tmpfs, options: []) try validateMount(.filesystem(fs)) @@ -341,8 +350,9 @@ public struct Parser { } public static func mounts(_ rawMounts: [String], relativeTo basePath: URL? = nil) throws -> [VolumeOrFilesystem] { - var mounts: [VolumeOrFilesystem] = [] let rawMounts = rawMounts.dedupe() + var mounts: [VolumeOrFilesystem] = [] + mounts.reserveCapacity(rawMounts.count) for mount in rawMounts { let m = try Parser.mount(mount, relativeTo: basePath) try validateMount(m) @@ -484,6 +494,7 @@ public struct Parser { public static func volumes(_ rawVolumes: [String], relativeTo basePath: URL? = nil) throws -> [VolumeOrFilesystem] { var mounts: [VolumeOrFilesystem] = [] + mounts.reserveCapacity(rawVolumes.count) for volume in rawVolumes { let m = try Parser.volume(volume, relativeTo: basePath) try Parser.validateMount(m) @@ -593,6 +604,7 @@ public struct Parser { /// - Throws: ContainerizationError if parsing fails public static func publishPorts(_ rawPublishPorts: [String]) throws -> [PublishPort] { var publishPorts: [PublishPort] = [] + publishPorts.reserveCapacity(rawPublishPorts.count) // Process each raw port string for socket in rawPublishPorts { @@ -726,6 +738,7 @@ public struct Parser { /// - Throws: ContainerizationError if parsing fails or a path is invalid public static func publishSockets(_ rawPublishSockets: [String]) throws -> [PublishSocket] { var sockets: [PublishSocket] = [] + sockets.reserveCapacity(rawPublishSockets.count) // Process each raw socket string for socket in rawPublishSockets { @@ -919,6 +932,7 @@ public struct Parser { /// - nofile=1024:unlimited (soft=1024, hard=UINT64_MAX) public static func rlimits(_ rawUlimits: [String]) throws -> [ProcessConfiguration.Rlimit] { var rlimits: [ProcessConfiguration.Rlimit] = [] + rlimits.reserveCapacity(rawUlimits.count) var seenTypes: Set = [] for ulimit in rawUlimits { @@ -1010,6 +1024,7 @@ public struct Parser { /// Returns normalized uppercase CAP_* strings. public static func capabilities(capAdd: [String], capDrop: [String]) throws -> (capAdd: [String], capDrop: [String]) { var normalizedAdd: [String] = [] + normalizedAdd.reserveCapacity(capAdd.count) for cap in capAdd { let upper = cap.uppercased() if upper == "ALL" { @@ -1024,6 +1039,7 @@ public struct Parser { } var normalizedDrop: [String] = [] + normalizedDrop.reserveCapacity(capDrop.count) for cap in capDrop { let upper = cap.uppercased() if upper == "ALL" { diff --git a/Sources/Services/ContainerAPIService/Client/Utility.swift b/Sources/Services/ContainerAPIService/Client/Utility.swift index bfea2bbc..ce937ebd 100644 --- a/Sources/Services/ContainerAPIService/Client/Utility.swift +++ b/Sources/Services/ContainerAPIService/Client/Utility.swift @@ -24,6 +24,11 @@ import Foundation import Logging import TerminalProgress +// MARK: - Collection capacity hints +// Dictionary(minimumCapacity:) and reserveCapacity() are used in this file to +// pre-allocate storage when the final collection size is known from the input. +// This avoids incremental reallocation overhead in hot-path parser methods. + public struct Utility { static let publishedPortCountLimit = 64 @@ -349,7 +354,7 @@ public struct Utility { /// - Parameter pairs: Array of strings in "key=value" format /// - Returns: Dictionary mapping keys to values public static func parseKeyValuePairs(_ pairs: [String]) -> [String: String] { - var result: [String: String] = [:] + var result: [String: String] = Dictionary(minimumCapacity: pairs.count) for pair in pairs { let components = pair.split(separator: "=", maxSplits: 1) if components.count == 2 { diff --git a/Tests/ContainerAPIClientTests/ParserTest.swift b/Tests/ContainerAPIClientTests/ParserTest.swift index 8a1deabb..0dcc6f7c 100644 --- a/Tests/ContainerAPIClientTests/ParserTest.swift +++ b/Tests/ContainerAPIClientTests/ParserTest.swift @@ -1336,4 +1336,61 @@ struct ParserTest { func testManagementFlagsAcceptsNoDNSAlone() throws { _ = try Flags.Management.parse(["--no-dns"]) } + + // MARK: - Collection capacity hints + + @Test("labels with large input preserves all entries") + func testLabelsLargeInput() throws { + let labels = (0..<100).map { "key\($0)=value\($0)" } + let result = try Parser.labels(labels) + #expect(result.count == 100) + #expect(result["key42"] == "value42") + #expect(result["key99"] == "value99") + } + + @Test("resolve with large input preserves all entries") + func testParseKeyValuePairsLargeInput() { + let pairs = (0..<100).map { "key\($0)=value\($0)" } + let result = Utility.parseKeyValuePairs(pairs) + #expect(result.count == 100) + #expect(result["key0"] == "value0") + #expect(result["key99"] == "value99") + } + + @Test("tmpfsMounts with large input") + func testTmpfsMountsLargeInput() throws { + let mounts = (0..<20).map { "/mnt/tmpfs\($0)" } + let result = try Parser.tmpfsMounts(mounts) + #expect(result.count == 20) + } + + @Test("volumes with large input") + func testVolumesLargeInput() throws { + let volumes = (0..<20).map { "vol\($0):/mnt/vol\($0)" } + let result = try Parser.volumes(volumes) + #expect(result.count == 20) + } + + @Test("capabilities with large input") + func testCapabilitiesLargeInput() throws { + let result = try Parser.capabilities(capAdd: ["ALL", "SYS_ADMIN", "NET_RAW", "CHOWN"], capDrop: ["SETUID", "KILL"]) + #expect(result.capAdd.count == 4) + #expect(result.capDrop.count == 2) + #expect(result.capAdd.first == "ALL") + } + + @Test("rlimits with large input") + func testRlimitsLargeInput() throws { + let result = try Parser.rlimits(["nofile=1024:2048", "nproc=100:200", "memlock=65536:65536"]) + #expect(result.count == 3) + #expect(result[0].limit == "RLIMIT_NOFILE") + } + + @Test("allEnv with large env lists") + func testAllEnvLargeInput() throws { + let imageEnvs = (0..<50).map { "IMAGE_VAR\($0)=value\($0)" } + let envs = (0..<50).map { "USER_VAR\($0)=value\($0)" } + let result = try Parser.allEnv(imageEnvs: imageEnvs, envFiles: [], envs: envs) + #expect(result.count == 100) + } }