Clarify memory units in help and documentation. (#657)

Closes #519.

## Type of Change
- [ ] Bug fix
- [ ] New feature  
- [ ] Breaking change
- [x] Documentation update

## Motivation and Context
Clarifies memory units.

## Testing
- [x] Tested locally
- [x] Added/updated tests
- [x] Added/updated docs
This commit is contained in:
J Logan
2025-09-22 11:05:06 -07:00
committed by GitHub
parent 6eaf51b8bc
commit 06eab455c8
6 changed files with 233 additions and 20 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ public struct Flags {
@Option(
name: [.customLong("memory"), .customShort("m")],
help:
"Amount of memory in bytes, kilobytes (K), megabytes (M), or gigabytes (G) for the container, with MB granularity (for example, 1024K will result in 1MB being allocated for the container)"
"Amount of memory (1MiByte granularity), with optional K, M, G, T, or P suffix"
)
public var memory: String?
}
+19 -15
View File
@@ -16,7 +16,7 @@
import Foundation
private let units: [Character: UnitInformationStorage] = [
private let binaryUnits: [Character: UnitInformationStorage] = [
"b": .bytes,
"k": .kibibytes,
"m": .mebibytes,
@@ -40,20 +40,22 @@ extension Measurement {
}
}
/// parse the provided string into a measurement that is able to be converted to various byte sizes
/// parseMemory the provided string into a measurement that is able to be converted to various byte sizes using binary exponents
public static func parse(parsing: String) throws -> Measurement<UnitInformationStorage> {
let check = "01234567890. "
let i = parsing.lastIndex {
check.contains($0)
}
guard let i else {
let check = "01234567890."
let trimmed = parsing.trimmingCharacters(in: .whitespaces).lowercased()
guard !trimmed.isEmpty else {
throw ParseError.invalidSize
}
let after = parsing.index(after: i)
let rawValue = parsing[..<after].trimmingCharacters(in: .whitespaces)
let rawUnit = parsing[after...]
.trimmingCharacters(in: .whitespaces)
.lowercased()
let i = trimmed.firstIndex {
!check.contains($0)
}
let rawValue =
i
.map { trimmed[..<$0].trimmingCharacters(in: .whitespaces) }
?? trimmed
let rawUnit = i.map { trimmed[$0...].trimmingCharacters(in: .whitespaces) } ?? ""
let value = Double(rawValue)
guard let value else {
@@ -61,7 +63,7 @@ extension Measurement {
}
let unitSymbol = try Self.parseUnit(rawUnit)
let unit = units[unitSymbol]
let unit = binaryUnits[unitSymbol]
guard let unit else {
throw ParseError.invalidSymbol(rawUnit)
}
@@ -70,9 +72,11 @@ extension Measurement {
static func parseUnit(_ unit: String) throws -> Character {
let s = unit.dropFirst()
let unitSymbol = unit.first ?? "b"
switch s {
case "", "b", "ib":
return unit.first ?? "b"
case "", "ib", "b":
return unitSymbol
default:
throw ParseError.invalidSymbol(unit)
}
+1 -1
View File
@@ -44,7 +44,7 @@ extension Application {
@Option(
name: [.customLong("memory"), .customShort("m")],
help:
"Amount of memory in bytes, kilobytes (K), megabytes (M), or gigabytes (G) for the container, with MB granularity (for example, 1024K will result in 1MB being allocated for the container)"
"Amount of builder container memory (1MiByte granularity), with optional K, M, G, T, or P suffix"
)
var memory: String = "2048MB"
@@ -0,0 +1,209 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
import Testing
@testable import ContainerClient
struct MeasurementParseTests {
@Test("Parse binary units - bare unit symbols")
func testBinaryUnits() throws {
let result1 = try Measurement<UnitInformationStorage>.parse(parsing: "4k")
#expect(result1.value == 4.0)
#expect(result1.unit == .kibibytes)
let result2 = try Measurement<UnitInformationStorage>.parse(parsing: "2m")
#expect(result2.value == 2.0)
#expect(result2.unit == .mebibytes)
let result3 = try Measurement<UnitInformationStorage>.parse(parsing: "1g")
#expect(result3.value == 1.0)
#expect(result3.unit == .gibibytes)
let result4 = try Measurement<UnitInformationStorage>.parse(parsing: "512b")
#expect(result4.value == 512.0)
#expect(result4.unit == .bytes)
}
@Test("Parse binary units - ib suffix")
func testBinaryUnitsWithIbSuffix() throws {
let result1 = try Measurement<UnitInformationStorage>.parse(parsing: "4kib")
#expect(result1.value == 4.0)
#expect(result1.unit == .kibibytes)
let result2 = try Measurement<UnitInformationStorage>.parse(parsing: "2mib")
#expect(result2.value == 2.0)
#expect(result2.unit == .mebibytes)
let result3 = try Measurement<UnitInformationStorage>.parse(parsing: "1gib")
#expect(result3.value == 1.0)
#expect(result3.unit == .gibibytes)
let result4 = try Measurement<UnitInformationStorage>.parse(parsing: "3tib")
#expect(result4.value == 3.0)
#expect(result4.unit == .tebibytes)
let result5 = try Measurement<UnitInformationStorage>.parse(parsing: "1pib")
#expect(result5.value == 1.0)
#expect(result5.unit == .pebibytes)
}
@Test("Parse binary units - all suffixes now use binary")
func testAllSuffixesUseBinary() throws {
let result1 = try Measurement<UnitInformationStorage>.parse(parsing: "4kb")
#expect(result1.value == 4.0)
#expect(result1.unit == .kibibytes)
let result2 = try Measurement<UnitInformationStorage>.parse(parsing: "2mb")
#expect(result2.value == 2.0)
#expect(result2.unit == .mebibytes)
let result3 = try Measurement<UnitInformationStorage>.parse(parsing: "1gb")
#expect(result3.value == 1.0)
#expect(result3.unit == .gibibytes)
let result4 = try Measurement<UnitInformationStorage>.parse(parsing: "3tb")
#expect(result4.value == 3.0)
#expect(result4.unit == .tebibytes)
let result5 = try Measurement<UnitInformationStorage>.parse(parsing: "1pb")
#expect(result5.value == 1.0)
#expect(result5.unit == .pebibytes)
}
@Test("Parse with whitespace")
func testParsingWithWhitespace() throws {
let result1 = try Measurement<UnitInformationStorage>.parse(parsing: " 4k ")
#expect(result1.value == 4.0)
#expect(result1.unit == .kibibytes)
let result2 = try Measurement<UnitInformationStorage>.parse(parsing: " 2.5mb ")
#expect(result2.value == 2.5)
#expect(result2.unit == .mebibytes)
}
@Test("Parse decimal values")
func testDecimalValues() throws {
let result1 = try Measurement<UnitInformationStorage>.parse(parsing: "4.5k")
#expect(result1.value == 4.5)
#expect(result1.unit == .kibibytes)
let result2 = try Measurement<UnitInformationStorage>.parse(parsing: "1.25gb")
#expect(result2.value == 1.25)
#expect(result2.unit == .gibibytes)
let result3 = try Measurement<UnitInformationStorage>.parse(parsing: "0.5mib")
#expect(result3.value == 0.5)
#expect(result3.unit == .mebibytes)
}
@Test("Parse case insensitive")
func testCaseInsensitive() throws {
let result1 = try Measurement<UnitInformationStorage>.parse(parsing: "4K")
#expect(result1.value == 4.0)
#expect(result1.unit == .kibibytes)
let result2 = try Measurement<UnitInformationStorage>.parse(parsing: "2GB")
#expect(result2.value == 2.0)
#expect(result2.unit == .gibibytes)
let result3 = try Measurement<UnitInformationStorage>.parse(parsing: "1MIB")
#expect(result3.value == 1.0)
#expect(result3.unit == .mebibytes)
}
@Test("Parse bytes unit")
func testBytesUnit() throws {
let result1 = try Measurement<UnitInformationStorage>.parse(parsing: "1024")
#expect(result1.value == 1024.0)
#expect(result1.unit == .bytes)
let result2 = try Measurement<UnitInformationStorage>.parse(parsing: "512b")
#expect(result2.value == 512.0)
#expect(result2.unit == .bytes)
}
@Test("Parse invalid size throws error")
func testInvalidSizeThrowsError() {
#expect {
_ = try Measurement<UnitInformationStorage>.parse(parsing: "abc")
} throws: { error in
guard let parseError = error as? Measurement<UnitInformationStorage>.ParseError else {
return false
}
return parseError.description == "invalid size"
}
#expect {
_ = try Measurement<UnitInformationStorage>.parse(parsing: "k4")
} throws: { error in
guard let parseError = error as? Measurement<UnitInformationStorage>.ParseError else {
return false
}
return parseError.description == "invalid size"
}
}
@Test("Parse invalid symbol throws error")
func testInvalidSymbolThrowsError() {
#expect {
_ = try Measurement<UnitInformationStorage>.parse(parsing: "4x")
} throws: { error in
guard let parseError = error as? Measurement<UnitInformationStorage>.ParseError else {
return false
}
return parseError.description == "invalid symbol: x"
}
#expect {
_ = try Measurement<UnitInformationStorage>.parse(parsing: "4kx")
} throws: { error in
guard let parseError = error as? Measurement<UnitInformationStorage>.ParseError else {
return false
}
return parseError.description == "invalid symbol: kx"
}
}
@Test("Parse empty string throws error")
func testEmptyStringThrowsError() {
#expect {
_ = try Measurement<UnitInformationStorage>.parse(parsing: "")
} throws: { error in
guard let parseError = error as? Measurement<UnitInformationStorage>.ParseError else {
return false
}
return parseError.description == "invalid size"
}
}
@Test("Verify all suffixes now use binary units")
func testAllSuffixesUseBinaryUnits() throws {
let bareK = try Measurement<UnitInformationStorage>.parse(parsing: "1k")
let kib = try Measurement<UnitInformationStorage>.parse(parsing: "1kib")
let kb = try Measurement<UnitInformationStorage>.parse(parsing: "1kb")
#expect(bareK.unit == .kibibytes)
#expect(kib.unit == .kibibytes)
#expect(kb.unit == .kibibytes)
let allInBytes = bareK.converted(to: .bytes).value
#expect(allInBytes == 1024.0)
}
}
+2 -2
View File
@@ -4,7 +4,7 @@ How to use the features of `container`.
## Configure memory and CPUs for your containers
Since the containers created by `container` are lightweight virtual machines, consider the needs of your containerized application when you use `container run`. The `--memory` and `--cpus` options allow you to override the default memory and CPU limits for the virtual machine. The default values are 1 gigabyte of RAM and 4 CPUs. You can use abbreviations for memory units; for example, to run a container for image `big` with 8 CPUs and 32 gigabytes of memory, use:
Since the containers created by `container` are lightweight virtual machines, consider the needs of your containerized application when you use `container run`. The `--memory` and `--cpus` options allow you to override the default memory and CPU limits for the virtual machine. The default values are 1 gigabyte of RAM and 4 CPUs. You can use abbreviations for memory units; for example, to run a container for image `big` with 8 CPUs and 32 GiBytes of memory, use:
```bash
container run --rm --cpus 8 --memory 32g big
@@ -14,7 +14,7 @@ container run --rm --cpus 8 --memory 32g big
When you first run `container build`, `container` starts a *builder*, which is a utility container that builds images from your `Dockerfile`s. As with anything you run with `container run`, the builder runs in a lightweight virtual machine, so for resource-intensive builds, you may need to increase the memory and CPU limits for the builder VM.
By default, the builder VM receives 2 gigabytes of RAM and 2 CPUs. You can change these limits by starting the builder container before running `container build`:
By default, the builder VM receives 2 GiBytes of RAM and 2 CPUs. You can change these limits by starting the builder container before running `container build`:
```bash
container builder start --cpus 8 --memory 32g
+1 -1
View File
@@ -61,7 +61,7 @@ socat TCP-LISTEN:8000,fork,bind=192.168.64.1 TCP:127.0.0.1:8000
### Releasing container memory to macOS
The macOS Virtualization framework implements only partial support for memory ballooning, which is a technology that allows virtual machines to dynamically use and relinquish host memory. When you create a container, the underlying virtual machine only uses the amount of memory that the containerized application needs. For example, you might start a container using the option `--memory 16g`, but see that the application is only using 2 gigabytes of RAM in the macOS Activity Monitor.
The macOS Virtualization framework implements only partial support for memory ballooning, which is a technology that allows virtual machines to dynamically use and relinquish host memory. When you create a container, the underlying virtual machine only uses the amount of memory that the containerized application needs. For example, you might start a container using the option `--memory 16g`, but see that the application is only using 2 GiBytes of RAM in the macOS Activity Monitor.
Currently, memory pages freed to the Linux operating system by processes running in the container's VM are not relinquished to the host. If you run many memory-intensive containers, you may need to occasionally restart them to reduce memory utilization.