diff --git a/Configuration/Legacy/UTMLegacyAppleConfiguration.swift b/Configuration/Legacy/UTMLegacyAppleConfiguration.swift index c5570dbda..4d331bfcd 100644 --- a/Configuration/Legacy/UTMLegacyAppleConfiguration.swift +++ b/Configuration/Legacy/UTMLegacyAppleConfiguration.swift @@ -313,6 +313,7 @@ struct DiskImage: Codable, Hashable, Identifiable { var sizeMib: Int var isReadOnly: Bool + var isSparse: Bool var isExternal: Bool var imageURL: URL? private var uuid = UUID() // for identifiable @@ -320,6 +321,7 @@ struct DiskImage: Codable, Hashable, Identifiable { private enum CodingKeys: String, CodingKey { case sizeMib case isReadOnly + case isSparse case isExternal case imagePath case imageBookmark @@ -344,6 +346,7 @@ struct DiskImage: Codable, Hashable, Identifiable { let container = try decoder.container(keyedBy: CodingKeys.self) sizeMib = try container.decode(Int.self, forKey: .sizeMib) isReadOnly = try container.decode(Bool.self, forKey: .isReadOnly) + isSparse = try container.decode(Bool.self, forKey: .isSparse) isExternal = try container.decode(Bool.self, forKey: .isExternal) if !isExternal, let imagePath = try container.decodeIfPresent(String.self, forKey: .imagePath) { imageURL = dataURL.appendingPathComponent(imagePath) @@ -357,6 +360,7 @@ struct DiskImage: Codable, Hashable, Identifiable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(sizeMib, forKey: .sizeMib) try container.encode(isReadOnly, forKey: .isReadOnly) + try container.encode(isSparse, forKey: .isSparse) try container.encode(isExternal, forKey: .isExternal) if !isExternal { try container.encodeIfPresent(imageURL?.lastPathComponent, forKey: .imagePath) diff --git a/Configuration/UTMAppleConfigurationDrive.swift b/Configuration/UTMAppleConfigurationDrive.swift index ef53dbbf6..e9b070bd2 100644 --- a/Configuration/UTMAppleConfigurationDrive.swift +++ b/Configuration/UTMAppleConfigurationDrive.swift @@ -24,6 +24,7 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { var sizeMib: Int = 0 var isReadOnly: Bool + var isSparse: Bool var isExternal: Bool var imageURL: URL? var imageName: String? @@ -36,6 +37,7 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { private enum CodingKeys: String, CodingKey { case isReadOnly = "ReadOnly" + case isSparse = "Sparse" case imageName = "ImageName" case bookmark = "Bookmark" // legacy only case identifier = "Identifier" @@ -54,13 +56,22 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { init(newSize: Int) { sizeMib = newSize isReadOnly = false + isSparse = true isExternal = false } + init(newSize: Int, isSparse: Bool = true) { + sizeMib = newSize + isReadOnly = false + isExternal = false + self.isSparse = isSparse + } + init(existingURL url: URL?, isExternal: Bool = false) { self.imageURL = url self.isReadOnly = isExternal self.isExternal = isExternal + self.isSparse = true } init(from decoder: Decoder) throws { @@ -83,6 +94,7 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { isExternal = true } isReadOnly = try container.decodeIfPresent(Bool.self, forKey: .isReadOnly) ?? isExternal + isSparse = try container.decodeIfPresent(Bool.self, forKey: .isSparse) ?? true id = try container.decode(String.self, forKey: .identifier) } @@ -92,12 +104,22 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { try container.encodeIfPresent(imageName, forKey: .imageName) } try container.encode(isReadOnly, forKey: .isReadOnly) + try container.encode(isSparse, forKey: .isSparse) try container.encode(id, forKey: .identifier) } func vzDiskImage() throws -> VZDiskImageStorageDeviceAttachment? { if let imageURL = imageURL { - return try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: isReadOnly) + if #available(macOS 12, *) { + /* + * virtual disk cache mode have bugs, + * when it is disabled or set to auto (default value) + * may cause linux file system corrputed, especially in the case of heavy IO loads + */ + return try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: isReadOnly, cachingMode:VZDiskImageCachingMode.cached, synchronizationMode: VZDiskImageSynchronizationMode.full) + } else { + return try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: isReadOnly) + } } else { return nil } @@ -107,6 +129,7 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { imageName?.hash(into: &hasher) sizeMib.hash(into: &hasher) isReadOnly.hash(into: &hasher) + isSparse.hash(into: &hasher) isExternal.hash(into: &hasher) id.hash(into: &hasher) } @@ -126,6 +149,7 @@ extension UTMAppleConfigurationDrive { init(migrating oldDrive: DiskImage) { sizeMib = oldDrive.sizeMib isReadOnly = oldDrive.isReadOnly + isSparse = oldDrive.isSparse isExternal = oldDrive.isExternal imageURL = oldDrive.imageURL } diff --git a/Configuration/UTMConfigurationDrive.swift b/Configuration/UTMConfigurationDrive.swift index 61103ac98..4c611b87e 100644 --- a/Configuration/UTMConfigurationDrive.swift +++ b/Configuration/UTMConfigurationDrive.swift @@ -28,6 +28,9 @@ protocol UTMConfigurationDrive: Codable, Hashable, Identifiable { /// If true, the drive image will be mounted as read-only. var isReadOnly: Bool { get } + /// If true, the drive image is sparse file. + var isSparse: Bool { get } + /// If true, a bookmark is stored in the package. var isExternal: Bool { get } @@ -77,7 +80,7 @@ extension UTMConfigurationDrive { throw UTMConfigurationError.driveAlreadyExists(newURL) } if isRawImage { - try await createRawImage(at: newURL, size: sizeMib) + try await createRawImage(at: newURL, size: sizeMib, sparse: isSparse) } else { try await createQcow2Image(at: newURL, size: sizeMib) } @@ -90,14 +93,36 @@ extension UTMConfigurationDrive { } } - private func createRawImage(at newURL: URL, size sizeMib: Int) async throws { + private func createRawImage(at newURL: URL, size sizeMib: Int, sparse isSparse: Bool) async throws { let size = UInt64(sizeMib) * bytesInMib try await Task.detached { guard FileManager.default.createFile(atPath: newURL.path, contents: nil, attributes: nil) else { throw UTMConfigurationError.cannotCreateDiskImage } let handle = try FileHandle(forWritingTo: newURL) - try handle.truncate(atOffset: size) + if(isSparse) { + /* truncate command make a sparse file, the space will not alloc before really used + * this should be better at most time. + * but maybe not suitable for virtual machines, especially in the case of heavy IO loads + * this may give extra time delay and operational interruptions when system do really space alloc + * this behavior may cause later write completed before previous write + * the incorrect write order may cause file system corrputed + */ + try handle.truncate(atOffset: size) + } else { + var val = 0 + let scale = 100; // write large block will be faster, but too large may cause OOM + let data = NSMutableData(length: NSNumber(value: bytesInMib).intValue * scale) // 100MB + while val < (sizeMib / scale) { + try handle.write(contentsOf: data!) + val += 1; + } + val = sizeMib % scale + if(val > 0) { + val = val * NSNumber(value: bytesInMib).intValue + try handle.write(contentsOf: data!.subdata(with: NSRange(location: 0, length: val))) + } + } try handle.close() }.value } diff --git a/Configuration/UTMQemuConfigurationDrive.swift b/Configuration/UTMQemuConfigurationDrive.swift index 3c0dea901..fea8f11da 100644 --- a/Configuration/UTMQemuConfigurationDrive.swift +++ b/Configuration/UTMQemuConfigurationDrive.swift @@ -29,6 +29,9 @@ struct UTMQemuConfigurationDrive: UTMConfigurationDrive { /// If true, the drive image will be mounted as read-only. var isReadOnly: Bool = false + /// If true, the drive image is sparse file. + var isSparse: Bool = true + /// If true, a bookmark is stored in the package. var isExternal: Bool = false @@ -60,6 +63,7 @@ struct UTMQemuConfigurationDrive: UTMConfigurationDrive { case interfaceVersion = "InterfaceVersion" case identifier = "Identifier" case isReadOnly = "ReadOnly" + case isSparse = "Sparse" } init() { @@ -78,6 +82,7 @@ struct UTMQemuConfigurationDrive: UTMConfigurationDrive { isExternal = true } isReadOnly = try values.decodeIfPresent(Bool.self, forKey: .isReadOnly) ?? isExternal + isSparse = try values.decodeIfPresent(Bool.self, forKey: .isSparse) ?? true imageType = try values.decode(QEMUDriveImageType.self, forKey: .imageType) interface = try values.decode(QEMUDriveInterface.self, forKey: .interface) interfaceVersion = try values.decodeIfPresent(Int.self, forKey: .interfaceVersion) ?? 0 @@ -90,6 +95,7 @@ struct UTMQemuConfigurationDrive: UTMConfigurationDrive { try container.encodeIfPresent(imageURL?.lastPathComponent, forKey: .imageName) } try container.encode(isReadOnly, forKey: .isReadOnly) + try container.encode(isSparse, forKey: .isSparse) try container.encode(imageType, forKey: .imageType) if imageType == .cd || imageType == .disk { try container.encode(interface, forKey: .interface) @@ -104,6 +110,7 @@ struct UTMQemuConfigurationDrive: UTMConfigurationDrive { imageName?.hash(into: &hasher) sizeMib.hash(into: &hasher) isReadOnly.hash(into: &hasher) + isSparse.hash(into: &hasher) isExternal.hash(into: &hasher) id.hash(into: &hasher) imageType.hash(into: &hasher) diff --git a/Platform/Shared/VMWizardDrivesView.swift b/Platform/Shared/VMWizardDrivesView.swift index 28782c2e7..31c11a230 100644 --- a/Platform/Shared/VMWizardDrivesView.swift +++ b/Platform/Shared/VMWizardDrivesView.swift @@ -18,7 +18,14 @@ import SwiftUI struct VMWizardDrivesView: View { @ObservedObject var wizardState: VMWizardState - + var allocNow: Binding { + return Binding(get: { + return wizardState.allocateAllDiskSpaceNow + }, set: { newValue in + wizardState.allocateAllDiskSpaceNow = newValue + }) + } + var body: some View { VMWizardContent("Storage") { Section { @@ -34,6 +41,9 @@ struct VMWizardDrivesView: View { .frame(maxWidth: 50) Text("GB") } + Toggle(isOn: allocNow, label: { + Text("Allocate all disk space now") + }).help("If checked, allocate all disk space immediately rather than allow the disk space to gradually grow to the maximum amount.") } header: { Text("Size") } diff --git a/Platform/Shared/VMWizardState.swift b/Platform/Shared/VMWizardState.swift index b4803cb00..380c0e4a7 100644 --- a/Platform/Shared/VMWizardState.swift +++ b/Platform/Shared/VMWizardState.swift @@ -123,6 +123,8 @@ enum VMWizardOS: String, Identifiable { @Published var systemMemoryMib: Int = 512 @Published var storageSizeGib: Int = 8 #endif + @Published var allocateAllDiskSpaceNow = false + @Published var systemCpuCount: Int = 0 @Published var isGLEnabled: Bool = false @Published var sharingDirectoryURL: URL? @@ -324,7 +326,7 @@ enum VMWizardOS: String, Identifiable { } } if !isSkipDiskCreate { - config.drives.append(UTMAppleConfigurationDrive(newSize: storageSizeGib * bytesInGib / bytesInMib)) + config.drives.append(UTMAppleConfigurationDrive(newSize: storageSizeGib * bytesInGib / bytesInMib, isSparse: !allocateAllDiskSpaceNow)) } if #available(macOS 12, *), let sharingDirectoryURL = sharingDirectoryURL { config.sharedDirectories = [UTMAppleConfigurationSharedDirectory(directoryURL: sharingDirectoryURL, isReadOnly: sharingReadOnly)] diff --git a/Platform/macOS/VMConfigAppleDriveCreateView.swift b/Platform/macOS/VMConfigAppleDriveCreateView.swift index 5d5244c74..032cd3e84 100644 --- a/Platform/macOS/VMConfigAppleDriveCreateView.swift +++ b/Platform/macOS/VMConfigAppleDriveCreateView.swift @@ -22,13 +22,24 @@ struct VMConfigAppleDriveCreateView: View { @Binding var config: UTMAppleConfigurationDrive @State private var isGiB: Bool = true + var allocNow: Binding { + return Binding(get: { + return !config.isSparse + }, set: { newValue in + config.isSparse = !newValue + }) + } var body: some View { + Form { VStack { Toggle(isOn: $config.isExternal.animation(), label: { Text("Removable") }).help("If checked, the drive image will be stored with the VM.") + Toggle(isOn: allocNow, label: { + Text("Allocate all disk space now") + }).help("If checked, allocate all disk space immediately rather than allow the disk space to gradually grow to the maximum amount.") .onChange(of: config.isExternal) { newValue in if newValue { config.sizeMib = 0 diff --git a/Platform/zh-Hans.lproj/Localizable.strings b/Platform/zh-Hans.lproj/Localizable.strings index 85c2e7145..b577e9cac 100644 --- a/Platform/zh-Hans.lproj/Localizable.strings +++ b/Platform/zh-Hans.lproj/Localizable.strings @@ -779,6 +779,8 @@ /* VMRemovableDrivesView */ "Removable" = "可移除"; +"Allocate all disk space now" = "立即分配所有磁盘空间"; +"If checked, allocate all disk space immediately rather than allow the disk space to gradually grow to the maximum amount." = "立即分配所有磁盘空间,而不是允许磁盘空间逐渐增长到最大。"; /* No comment provided by engineer. */ "Removable Drive" = "可移除驱动器";