Files
apple--containerization/Tests/ContainerizationTests/ImageTests/HeaderScanTimingTests.swift
T
wehub-resource-sync 680845cb1c
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
Linux build / Linux compile check (push) Has been cancelled
Linux build / Determine Swift version (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:25:30 +08:00

177 lines
7.1 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
#if os(macOS)
import ContainerizationArchive
import ContainerizationEXT4
import ContainerizationExtras
import ContainerizationOCI
import Foundation
import SystemPackage
import Testing
@testable import Containerization
/// Measures header scan overhead vs. full unpack time using real container images
/// pulled from a registry.
///
/// Run with:
/// ENABLE_TIMING_TESTS=1 swift test --filter ImageHeaderScanTimingTest
@Suite
struct ImageHeaderScanTimingTest {
private static let isEnabled = ProcessInfo.processInfo.environment["ENABLE_TIMING_TESTS"] != nil
let store: ImageStore
let dir: URL
let contentStore: ContentStore
init() throws {
let dir = FileManager.default.uniqueTemporaryDirectory(create: true)
let cs = try LocalContentStore(path: dir)
let store = try ImageStore(path: dir, contentStore: cs)
self.dir = dir
self.store = store
self.contentStore = cs
}
@Test(.enabled(if: ImageHeaderScanTimingTest.isEnabled))
func measureAlpineAndUbuntu() async throws {
defer { try? FileManager.default.removeItem(at: dir) }
let images: [(reference: String, label: String)] = [
("ghcr.io/linuxcontainers/alpine:3.20", "Alpine 3.20"),
("docker.io/library/ubuntu:24.04", "Ubuntu 24.04"),
]
for image in images {
print("\n==============================")
print("Image: \(image.label)")
print("==============================")
let img = try await store.pull(reference: image.reference, platform: .current)
let manifest = try await img.manifest(for: .current)
for (i, layer) in manifest.layers.enumerated() {
let content = try await img.getContent(digest: layer.digest)
let compression = compressionFilter(for: layer.mediaType)
let compressedSize = try FileManager.default.attributesOfItem(atPath: content.path.path)[.size] as? Int64 ?? 0
let label = "\(image.label) layer \(i + 1)/\(manifest.layers.count) (\(layer.mediaType), \(formatBytes(compressedSize)) compressed)"
try await measureOverhead(url: content.path, compression: compression, label: label)
}
}
}
// MARK: - Helpers
private func compressionFilter(for mediaType: String) -> ContainerizationArchive.Filter {
switch mediaType {
case MediaTypes.imageLayerZstd, MediaTypes.dockerImageLayerZstd:
return .zstd
case MediaTypes.imageLayer, MediaTypes.dockerImageLayer:
return .none
default:
return .gzip
}
}
private func measureOverhead(url: URL, compression: ContainerizationArchive.Filter, label: String) async throws {
let clock = ContinuousClock()
print("\n--- \(label) ---\n")
// For zstd, pre-decompress once (matching the production code path in EXT4Unpacker).
let scanFile: URL
let scanFilter: ContainerizationArchive.Filter
var decompressedFile: URL?
if compression == .zstd {
var decompressed: URL = url
let decompressDuration = try clock.measure {
decompressed = try ArchiveReader.decompressZstd(url)
}
scanFile = decompressed
scanFilter = .none
decompressedFile = decompressed
print(" Zstd decompress: \(decompressDuration)")
} else {
scanFile = url
scanFilter = compression
}
defer {
if let decompressedFile {
ArchiveReader.cleanUpDecompressedZstd(decompressedFile)
}
}
// 1. Header scan only
var scannedTotals: (size: Int64, items: Int) = (0, 0)
let scanDuration = try clock.measure {
scannedTotals = try EXT4.Formatter.scanArchiveHeaders(
format: .paxRestricted, filter: scanFilter, file: scanFile)
}
print(" Scanned total size: \(formatBytes(scannedTotals.size)) (\(scannedTotals.items) items)")
print(" Header scan: \(scanDuration)")
// 2. Full unpack without progress
let tempDir1 = FileManager.default.uniqueTemporaryDirectory()
let fsPath1 = FilePath(tempDir1.appendingPathComponent("no-progress.ext4.img", isDirectory: false))
defer { try? FileManager.default.removeItem(at: tempDir1) }
let unpackOnlyDuration = try await clock.measure {
let formatter = try EXT4.Formatter(fsPath1)
try await formatter.unpack(source: url, compression: compression)
try formatter.close()
}
print(" Unpack (no progress): \(unpackOnlyDuration)")
// 3. Full unpack with progress (includes header scan pass)
let tempDir2 = FileManager.default.uniqueTemporaryDirectory()
let fsPath2 = FilePath(tempDir2.appendingPathComponent("with-progress.ext4.img", isDirectory: false))
defer { try? FileManager.default.removeItem(at: tempDir2) }
let noopProgress: ProgressHandler = { _ in }
let withProgressDuration = try await clock.measure {
let formatter = try EXT4.Formatter(fsPath2)
try await formatter.unpack(source: url, compression: compression, progress: noopProgress)
try formatter.close()
}
print(" Unpack (w/ progress): \(withProgressDuration)")
// Summary
let scanMs = toMs(scanDuration)
let unpackMs = toMs(unpackOnlyDuration)
let withProgressMs = toMs(withProgressDuration)
let overheadMs = withProgressMs - unpackMs
let overheadPct = unpackMs > 0 ? (overheadMs / unpackMs) * 100 : 0
print("\n Summary:")
print(" Header scan alone: \(String(format: "%.1f", scanMs)) ms")
print(" Unpack only: \(String(format: "%.1f", unpackMs)) ms")
print(" Unpack + progress: \(String(format: "%.1f", withProgressMs)) ms")
print(" Overhead: \(String(format: "%.1f", overheadMs)) ms (\(String(format: "%.1f", overheadPct))%)")
}
private func toMs(_ d: Duration) -> Double {
let c = d.components
return Double(c.seconds) * 1000.0 + Double(c.attoseconds) / 1e15
}
private func formatBytes(_ bytes: Int64) -> String {
let mb = Double(bytes) / 1_048_576.0
return "\(String(format: "%.1f", mb)) MB"
}
}
#endif