Skip to content

Commit d545914

Browse files
authored
Merge pull request #529 from insidegui/install-ui-perf
Fix excessive CPU usage during download/installation
2 parents 666ad21 + d7290d1 commit d545914

File tree

4 files changed

+72
-15
lines changed

4 files changed

+72
-15
lines changed

VirtualBuddy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

VirtualUI/Source/Installer/Steps/Restore Image Selection/Components/VirtualBuddyMonoProgressView.swift

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,9 @@ private struct RamRodProgressView: View {
5353

5454
var body: some View {
5555
ZStack {
56-
Rectangle()
57-
.fill(Color(white: 0.16))
58-
GeometryReader { proxy in
59-
Color(white: 0.82)
60-
.frame(width: proxy.size.width * (progress / 1.0), alignment: .leading)
61-
.animation(.linear, value: progress)
62-
}
56+
Rectangle().fill(Color(white: 0.16))
57+
58+
ProgressBarShapeView(progress: progress)
6359
}
6460
.clipShape(shape)
6561
.overlay(shape.stroke(Color(white: 0.25), lineWidth: 1))
@@ -71,6 +67,62 @@ private struct RamRodProgressView: View {
7167
}
7268
}
7369

70+
/**
71+
You see, animating a white rectangle growing in width is a very expensive operation that SwiftUI
72+
is completely unable to do by itself without consuming an unhealthy amount of CPU,
73+
so this uses Core Animation instead to offload that expensive computation to the WindowServer/GPU.
74+
*/
75+
private struct ProgressBarShapeView: NSViewRepresentable {
76+
typealias NSViewType = _Representable
77+
78+
var progress: Double = 0
79+
80+
func makeNSView(context: Context) -> _Representable {
81+
_Representable(frame: .zero)
82+
}
83+
84+
func updateNSView(_ nsView: _Representable, context: Context) {
85+
nsView.progress = progress
86+
}
87+
88+
final class _Representable: NSView {
89+
private lazy var bar = CALayer()
90+
91+
@Invalidating(.layout)
92+
var progress: Double = 0
93+
94+
override init(frame frameRect: NSRect) {
95+
super.init(frame: frameRect)
96+
97+
platformLayer.addSublayer(bar)
98+
bar.backgroundColor = .white
99+
bar.anchorPoint = CGPoint(x: 0, y: 0.5)
100+
}
101+
102+
required init?(coder: NSCoder) {
103+
fatalError()
104+
}
105+
106+
override func layout() {
107+
super.layout()
108+
109+
CATransaction.begin()
110+
CATransaction.setDisableActions(true)
111+
112+
bar.position = CGPoint(x: bounds.minX, y: bounds.midY)
113+
bar.frame.size.height = bounds.height
114+
115+
CATransaction.commit()
116+
117+
CATransaction.begin()
118+
CATransaction.setAnimationDuration(0.2)
119+
CATransaction.setAnimationTimingFunction(.init(name: .linear))
120+
bar.frame.size.width = bounds.width * progress
121+
CATransaction.commit()
122+
}
123+
}
124+
}
125+
74126
#if DEBUG
75127
#Preview {
76128
VMInstallationWizard.preview(step: .download)

VirtualUI/Source/Installer/Steps/RestoreImageDownloadView.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import Combine
1111

1212
struct RestoreImageDownloadView: View {
1313
@EnvironmentObject var viewModel: VMInstallationViewModel
14+
@State private var downloadState = DownloadState.idle
1415

1516
private var progress: Double? {
16-
switch viewModel.downloadState {
17+
switch downloadState {
1718
case .idle, .preCheck: 0
1819
case .failed: nil
1920
case .downloading(let progress, _): progress ?? 0
@@ -22,7 +23,7 @@ struct RestoreImageDownloadView: View {
2223
}
2324

2425
private var status: Text {
25-
switch viewModel.downloadState {
26+
switch downloadState {
2627
case .idle: Text("Preparing Download")
2728
case .preCheck(let message): Text(message)
2829
case .downloading(_, let eta): eta.flatMap { Text(formattedETA(from: $0)) } ?? Text("Downloading")
@@ -32,7 +33,7 @@ struct RestoreImageDownloadView: View {
3233
}
3334

3435
private var style: VirtualBuddyMonoStyle {
35-
switch viewModel.downloadState {
36+
switch downloadState {
3637
case .idle, .downloading, .preCheck: .default
3738
case .failed: .failure
3839
case .done: .success
@@ -45,6 +46,7 @@ struct RestoreImageDownloadView: View {
4546
status: status,
4647
style: style
4748
)
49+
.onReceive(viewModel.$downloadState) { downloadState = $0 }
4850
}
4951

5052
private func formattedETA(from eta: Double) -> String {

VirtualUI/Source/Installer/VMInstallationViewModel.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ final class VMInstallationViewModel: ObservableObject, @unchecked Sendable {
166166
}
167167

168168
@Published private(set) var downloader: DownloadBackend?
169-
@Published private(set) var downloadState: DownloadState = .idle
169+
@SubjectPublisher private(set) var downloadState: DownloadState = .idle
170170

171171
private func validate() {
172172
disableNextButton = !data.canContinue(from: step)
@@ -354,7 +354,10 @@ final class VMInstallationViewModel: ObservableObject, @unchecked Sendable {
354354
private func startDownload(with url: URL) {
355355
let backend = createDownloadBackend(cookie: data.cookie)
356356

357-
backend.statePublisher.assign(to: &$downloadState)
357+
backend.statePublisher.sink { [weak self] state in
358+
self?.downloadState = state
359+
}
360+
.store(in: &cancellables)
358361

359362
self.downloader = backend
360363

0 commit comments

Comments
 (0)