Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion Sources/ContainerClient/Core/ContainerStats.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ public struct ContainerStats: Sendable, Codable {
/// Number of processes in the container
public var numProcesses: UInt64

// Extended I/O metrics
/// Read operations per second
public var readOpsPerSec: Double?
/// Write operations per second
public var writeOpsPerSec: Double?
/// Average read latency in milliseconds
public var readLatencyMs: Double?
/// Average write latency in milliseconds
public var writeLatencyMs: Double?
/// Average fsync latency in milliseconds
public var fsyncLatencyMs: Double?
/// I/O queue depth
public var queueDepth: UInt64?
/// Percentage of dirty pages
public var dirtyPagesPercent: Double?
/// Storage backend type (e.g., "apfs", "ext4", "virtio")
public var storageBackend: String?

public init(
id: String,
memoryUsageBytes: UInt64,
Expand All @@ -46,7 +64,15 @@ public struct ContainerStats: Sendable, Codable {
networkTxBytes: UInt64,
blockReadBytes: UInt64,
blockWriteBytes: UInt64,
numProcesses: UInt64
numProcesses: UInt64,
readOpsPerSec: Double? = nil,
writeOpsPerSec: Double? = nil,
readLatencyMs: Double? = nil,
writeLatencyMs: Double? = nil,
fsyncLatencyMs: Double? = nil,
queueDepth: UInt64? = nil,
dirtyPagesPercent: Double? = nil,
storageBackend: String? = nil
) {
self.id = id
self.memoryUsageBytes = memoryUsageBytes
Expand All @@ -57,5 +83,13 @@ public struct ContainerStats: Sendable, Codable {
self.blockReadBytes = blockReadBytes
self.blockWriteBytes = blockWriteBytes
self.numProcesses = numProcesses
self.readOpsPerSec = readOpsPerSec
self.writeOpsPerSec = writeOpsPerSec
self.readLatencyMs = readLatencyMs
self.writeLatencyMs = writeLatencyMs
self.fsyncLatencyMs = fsyncLatencyMs
self.queueDepth = queueDepth
self.dirtyPagesPercent = dirtyPagesPercent
self.storageBackend = storageBackend
}
}
66 changes: 66 additions & 0 deletions Sources/ContainerCommands/Container/ContainerStats.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ extension Application {
@Flag(name: .long, help: "Disable streaming stats and only pull the first result")
var noStream = false

@Flag(name: .long, help: "Display detailed I/O statistics (IOPS, latency, fsync, queue depth)")
var io = false

@OptionGroup
var global: Flags.Global

Expand Down Expand Up @@ -225,6 +228,14 @@ extension Application {
}

private func printStatsTable(_ statsData: [StatsSnapshot]) {
if io {
printIOStatsTable(statsData)
} else {
printDefaultStatsTable(statsData)
}
}

private func printDefaultStatsTable(_ statsData: [StatsSnapshot]) {
let header = [["Container ID", "Cpu %", "Memory Usage", "Net Rx/Tx", "Block I/O", "Pids"]]
var rows = header

Expand Down Expand Up @@ -253,6 +264,61 @@ extension Application {
print(formatter.format())
}

private func printIOStatsTable(_ statsData: [StatsSnapshot]) {
let header = [["CONTAINER", "READ/s", "WRITE/s", "LAT(ms)", "FSYNC(ms)", "QD", "DIRTY", "BACKEND"]]
var rows = header

for snapshot in statsData {
let stats2 = snapshot.stats2

// Calculate throughput from bytes (convert to MB/s)
let readMBps = Self.formatThroughput(stats2.blockReadBytes)
let writeMBps = Self.formatThroughput(stats2.blockWriteBytes)

// Format latency metrics
let latency = stats2.readLatencyMs != nil ? String(format: "%.1f", stats2.readLatencyMs!) : "N/A"
let fsyncLatency = stats2.fsyncLatencyMs != nil ? String(format: "%.1f", stats2.fsyncLatencyMs!) : "N/A"

// Format queue depth
let queueDepth = stats2.queueDepth != nil ? "\(stats2.queueDepth!)" : "N/A"

// Format dirty pages percentage
let dirty = stats2.dirtyPagesPercent != nil ? String(format: "%.1f%%", stats2.dirtyPagesPercent!) : "N/A"

// Storage backend
let backend = stats2.storageBackend ?? "unknown"

rows.append([
snapshot.container.id,
readMBps,
writeMBps,
latency,
fsyncLatency,
queueDepth,
dirty,
backend,
])
}

// Always print header, even if no containers
let formatter = TableOutput(rows: rows)
print(formatter.format())
}

static func formatThroughput(_ bytes: UInt64) -> String {
let mb = 1024.0 * 1024.0
let kb = 1024.0
let value = Double(bytes)

if value >= mb {
return String(format: "%.0fMB", value / mb)
} else if value >= kb {
return String(format: "%.0fKB", value / kb)
} else {
return "\(bytes)B"
}
}

private func clearScreen() {
// Move cursor to home position and clear from cursor to end of screen
print("\u{001B}[H\u{001B}[J", terminator: "")
Expand Down
39 changes: 36 additions & 3 deletions Sources/Services/ContainerSandboxService/SandboxService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -274,16 +274,49 @@ public actor SandboxService {
let containerInfo = try await self.getContainer()
let stats = try await containerInfo.container.statistics()

// Calculate I/O operations per second (IOPS) from block device stats
// TODO: The Containerization framework needs to provide readOps and writeOps
// from blockIO.devices. For now, we estimate from bytes assuming 4KB operations.
let totalReadBytes = stats.blockIO.devices.reduce(0) { $0 + $1.readBytes }
let totalWriteBytes = stats.blockIO.devices.reduce(0) { $0 + $1.writeBytes }
let estimatedReadOps = Double(totalReadBytes) / 4096.0
let estimatedWriteOps = Double(totalWriteBytes) / 4096.0

// TODO: Collect latency metrics from Containerization framework
// These would ideally come from blockIO.devices with new properties:
// - readLatencyMicros, writeLatencyMicros, fsyncLatencyMicros
let readLatency: Double? = nil // stats.blockIO.averageReadLatencyMs
let writeLatency: Double? = nil // stats.blockIO.averageWriteLatencyMs
let fsyncLatency: Double? = nil // stats.blockIO.averageFsyncLatencyMs

// TODO: Get queue depth from Containerization framework
let queueDepth: UInt64? = nil // stats.blockIO.queueDepth

// TODO: Get dirty pages percentage from memory stats
let dirtyPages: Double? = nil // stats.memory.dirtyPagesPercent

// TODO: Detect storage backend type from device information
// This would require inspecting the block device type in the VM
let backend: String? = "virtio" // Default for VM-based containers

let containerStats = ContainerStats(
id: stats.id,
memoryUsageBytes: stats.memory.usageBytes,
memoryLimitBytes: stats.memory.limitBytes,
cpuUsageUsec: stats.cpu.usageUsec,
networkRxBytes: stats.networks.reduce(0) { $0 + $1.receivedBytes },
networkTxBytes: stats.networks.reduce(0) { $0 + $1.transmittedBytes },
blockReadBytes: stats.blockIO.devices.reduce(0) { $0 + $1.readBytes },
blockWriteBytes: stats.blockIO.devices.reduce(0) { $0 + $1.writeBytes },
numProcesses: stats.process.current
blockReadBytes: totalReadBytes,
blockWriteBytes: totalWriteBytes,
numProcesses: stats.process.current,
readOpsPerSec: estimatedReadOps,
writeOpsPerSec: estimatedWriteOps,
readLatencyMs: readLatency,
writeLatencyMs: writeLatency,
fsyncLatencyMs: fsyncLatency,
queueDepth: queueDepth,
dirtyPagesPercent: dirtyPages,
storageBackend: backend
)

let reply = message.reply()
Expand Down
8 changes: 7 additions & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,10 +383,12 @@ No options.

Displays real-time resource usage statistics for containers. Shows CPU percentage, memory usage, network I/O, block I/O, and process count. By default, continuously updates statistics in an interactive display (like `top`). Use `--no-stream` for a single snapshot.

With the `--io` flag, displays detailed I/O performance metrics including IOPS, latency, fsync performance, queue depth, dirty pages, and storage backend type - useful for diagnosing database workloads and I/O bottlenecks.

**Usage**

```bash
container stats [--format <format>] [--no-stream] [--debug] [<container-ids> ...]
container stats [--format <format>] [--no-stream] [--io] [--debug] [<container-ids> ...]
```

**Arguments**
Expand All @@ -397,6 +399,7 @@ container stats [--format <format>] [--no-stream] [--debug] [<container-ids> ...

* `--format <format>`: Format of the output (values: json, table; default: table)
* `--no-stream`: Disable streaming stats and only pull the first result
* `--io`: Display detailed I/O statistics (IOPS, latency, fsync, queue depth)

**Examples**

Expand All @@ -410,6 +413,9 @@ container stats web db cache
# get a single snapshot of stats (non-interactive)
container stats --no-stream web

# display detailed I/O statistics for database workload analysis
container stats --io --no-stream postgres

# output stats as JSON
container stats --format json --no-stream web
```
Expand Down
26 changes: 26 additions & 0 deletions docs/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,32 @@ You can also output statistics in JSON format for scripting:
- **Block I/O**: Disk bytes read and written.
- **Pids**: Number of processes running in the container.

### Detailed I/O Performance Statistics

For database workloads, build systems, or performance-sensitive applications, use the `--io` flag to display detailed I/O metrics:

```console
% container stats --io --no-stream postgres
CONTAINER READ/s WRITE/s LAT(ms) FSYNC(ms) QD DIRTY BACKEND
postgres 280MB 195MB 4.8 1.4 1 2.1% virtio
```

This mode provides:

- **READ/s / WRITE/s**: Read and write throughput per second
- **LAT(ms)**: Average I/O latency in milliseconds (helps identify slow disk operations)
- **FSYNC(ms)**: Average fsync latency (critical for database durability)
- **QD**: I/O queue depth (indicates I/O concurrency)
- **DIRTY**: Percentage of dirty pages waiting to be written
- **BACKEND**: Storage backend type (virtio, apfs, ext4, etc.)

**Use cases for I/O statistics:**

- **Database performance tuning**: Monitor fsync latency and queue depth for Postgres, MySQL, MongoDB
- **Build system optimization**: Track I/O patterns during Docker builds or compilation
- **Diagnosing bottlenecks**: Identify whether slowness is due to CPU, memory, or disk I/O
- **Capacity planning**: Understand actual I/O requirements for workload sizing

## Expose virtualization capabilities to a container

> [!NOTE]
Expand Down