chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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
|
||||
Reference in New Issue
Block a user