mirror of
https://github.com/apple/container.git
synced 2026-09-28 01:46:25 -04:00
Improve accuracy of progress updates (#144)
This PR resolves the problem of dropped progress updates and ensures the accuracy of the information provided in the progress bar. Additionally, it adds the displaying of the finished state to the progress bar. Requires merging and tagging https://github.com/apple/containerization/pull/91.
This commit is contained in:
@@ -19,7 +19,6 @@ import ContainerizationError
|
||||
import Foundation
|
||||
|
||||
public struct XPCClient: Sendable {
|
||||
// Access to `connection` is protected by a lock
|
||||
private nonisolated(unsafe) let connection: xpc_connection_t
|
||||
private let q: DispatchQueue?
|
||||
private let service: String
|
||||
|
||||
@@ -61,23 +61,24 @@ extension ProgressBar {
|
||||
|
||||
/// Performs a check to see if the progress bar should be finished.
|
||||
public func checkIfFinished() {
|
||||
if let totalTasks = state.totalTasks {
|
||||
var finished = true
|
||||
var defined = false
|
||||
if let totalTasks = state.totalTasks, totalTasks > 0 {
|
||||
// For tasks, we're showing the current task rather then the number of completed tasks.
|
||||
guard state.tasks > totalTasks else {
|
||||
return
|
||||
}
|
||||
finished = finished && state.tasks == totalTasks
|
||||
defined = true
|
||||
}
|
||||
if let totalItems = state.totalItems {
|
||||
guard state.items == totalItems else {
|
||||
return
|
||||
}
|
||||
if let totalItems = state.totalItems, totalItems > 0 {
|
||||
finished = finished && state.items == totalItems
|
||||
defined = true
|
||||
}
|
||||
if let totalSize = state.totalSize {
|
||||
guard state.size == totalSize else {
|
||||
return
|
||||
}
|
||||
if let totalSize = state.totalSize, totalSize > 0 {
|
||||
finished = finished && state.size == totalSize
|
||||
defined = true
|
||||
}
|
||||
if defined && finished {
|
||||
finish()
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
/// Sets the current tasks.
|
||||
@@ -92,9 +93,14 @@ extension ProgressBar {
|
||||
|
||||
/// Performs an addition to the current tasks.
|
||||
/// - Parameter tasks: The tasks to add to the current tasks.
|
||||
public func add(tasks toAdd: Int, render: Bool = true) {
|
||||
let newTasks = state.tasks + toAdd
|
||||
set(tasks: newTasks, render: render)
|
||||
public func add(tasks delta: Int, render: Bool = true) {
|
||||
_state.withLock {
|
||||
let newTasks = $0.tasks + delta
|
||||
$0.tasks = newTasks
|
||||
}
|
||||
if render {
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the total tasks.
|
||||
@@ -108,10 +114,15 @@ extension ProgressBar {
|
||||
|
||||
/// Performs an addition to the total tasks.
|
||||
/// - Parameter totalTasks: The tasks to add to the total tasks.
|
||||
public func add(totalTasks toAdd: Int, render: Bool = true) {
|
||||
let totalTasks = state.totalTasks ?? 0
|
||||
let newTotalTasks = totalTasks + toAdd
|
||||
set(totalTasks: newTotalTasks, render: render)
|
||||
public func add(totalTasks delta: Int, render: Bool = true) {
|
||||
_state.withLock {
|
||||
let totalTasks = $0.totalTasks ?? 0
|
||||
let newTotalTasks = totalTasks + delta
|
||||
$0.totalTasks = newTotalTasks
|
||||
}
|
||||
if render {
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the items name.
|
||||
@@ -134,9 +145,14 @@ extension ProgressBar {
|
||||
|
||||
/// Performs an addition to the current items.
|
||||
/// - Parameter items: The items to add to the current items.
|
||||
public func add(items toAdd: Int, render: Bool = true) {
|
||||
let newItems = state.items + toAdd
|
||||
set(items: newItems, render: render)
|
||||
public func add(items delta: Int, render: Bool = true) {
|
||||
_state.withLock {
|
||||
let newItems = $0.items + delta
|
||||
$0.items = newItems
|
||||
}
|
||||
if render {
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the total items.
|
||||
@@ -150,10 +166,15 @@ extension ProgressBar {
|
||||
|
||||
/// Performs an addition to the total items.
|
||||
/// - Parameter totalItems: The items to add to the total items.
|
||||
public func add(totalItems toAdd: Int, render: Bool = true) {
|
||||
let totalItems = state.totalItems ?? 0
|
||||
let newTotalItems = totalItems + toAdd
|
||||
set(totalItems: newTotalItems, render: render)
|
||||
public func add(totalItems delta: Int, render: Bool = true) {
|
||||
_state.withLock {
|
||||
let totalItems = $0.totalItems ?? 0
|
||||
let newTotalItems = totalItems + delta
|
||||
$0.totalItems = newTotalItems
|
||||
}
|
||||
if render {
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the current size.
|
||||
@@ -167,9 +188,14 @@ extension ProgressBar {
|
||||
|
||||
/// Performs an addition to the current size.
|
||||
/// - Parameter size: The size to add to the current size.
|
||||
public func add(size toAdd: Int64, render: Bool = true) {
|
||||
let newSize = state.size + toAdd
|
||||
set(size: newSize, render: render)
|
||||
public func add(size delta: Int64, render: Bool = true) {
|
||||
_state.withLock {
|
||||
let newSize = $0.size + delta
|
||||
$0.size = newSize
|
||||
}
|
||||
if render {
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the total size.
|
||||
@@ -183,9 +209,14 @@ extension ProgressBar {
|
||||
|
||||
/// Performs an addition to the total size.
|
||||
/// - Parameter totalSize: The size to add to the total size.
|
||||
public func add(totalSize toAdd: Int64, render: Bool = true) {
|
||||
let totalSize = state.totalSize ?? 0
|
||||
let newTotalSize = totalSize + toAdd
|
||||
set(totalSize: newTotalSize, render: render)
|
||||
public func add(totalSize delta: Int64, render: Bool = true) {
|
||||
_state.withLock {
|
||||
let totalSize = $0.totalSize ?? 0
|
||||
let newTotalSize = totalSize + delta
|
||||
$0.totalSize = newTotalSize
|
||||
}
|
||||
if render {
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,9 @@ import SendableProperty
|
||||
/// A progress bar that updates itself as tasks are completed.
|
||||
public final class ProgressBar: Sendable {
|
||||
let config: ProgressConfig
|
||||
// `@SendableProperty` adds `_state: Synchronized<State>`, which can be updated inside a lock using `_state.withLock()`.
|
||||
@SendableProperty
|
||||
var state: State
|
||||
var state = State()
|
||||
@SendableProperty
|
||||
var printedWidth = 0
|
||||
let term: FileHandle?
|
||||
@@ -97,7 +98,7 @@ public final class ProgressBar: Sendable {
|
||||
printFullDescription()
|
||||
}
|
||||
|
||||
while !isFinished {
|
||||
while !state.finished {
|
||||
let intervalNanoseconds = UInt64(intervalSeconds * 1_000_000_000)
|
||||
render()
|
||||
state.iteration += 1
|
||||
@@ -117,11 +118,15 @@ public final class ProgressBar: Sendable {
|
||||
|
||||
/// Finishes the progress bar.
|
||||
public func finish() {
|
||||
guard !isFinished else {
|
||||
guard !state.finished else {
|
||||
return
|
||||
}
|
||||
|
||||
state.finished = true
|
||||
|
||||
// The last render.
|
||||
render(force: true)
|
||||
|
||||
if !config.disableProgressUpdates && !config.clearOnFinish {
|
||||
displayText(state.output, terminating: "\n")
|
||||
}
|
||||
@@ -143,8 +148,8 @@ extension ProgressBar {
|
||||
return timeDifferenceSeconds
|
||||
}
|
||||
|
||||
func render() {
|
||||
guard term != nil && !config.disableProgressUpdates && !isFinished else {
|
||||
func render(force: Bool = false) {
|
||||
guard term != nil && !config.disableProgressUpdates && (force || !state.finished) else {
|
||||
return
|
||||
}
|
||||
let output = draw()
|
||||
@@ -154,8 +159,12 @@ extension ProgressBar {
|
||||
func draw() -> String {
|
||||
var components = [String]()
|
||||
if config.showSpinner && !config.showProgressBar {
|
||||
let spinnerIcon = config.theme.getSpinnerIcon(state.iteration)
|
||||
components.append("\(spinnerIcon)")
|
||||
if !state.finished {
|
||||
let spinnerIcon = config.theme.getSpinnerIcon(state.iteration)
|
||||
components.append("\(spinnerIcon)")
|
||||
} else {
|
||||
components.append("\(config.theme.done)")
|
||||
}
|
||||
}
|
||||
|
||||
if config.showTasks, let totalTasks = state.totalTasks {
|
||||
@@ -176,13 +185,13 @@ extension ProgressBar {
|
||||
let total = state.totalSize ?? Int64(state.totalItems ?? 0)
|
||||
|
||||
if config.showPercent && total > 0 && allowProgress {
|
||||
components.append("\(state.percent)")
|
||||
components.append("\(state.finished ? "100%" : state.percent)")
|
||||
}
|
||||
|
||||
if config.showProgressBar, total > 0, allowProgress {
|
||||
let usedWidth = components.joined(separator: " ").count + 45 /* the maximum number of characters we may need */
|
||||
let remainingWidth = max(config.width - usedWidth, 1 /* the minumum width of a progress bar */)
|
||||
let barLength = Int(Int64(remainingWidth) * value / total)
|
||||
let barLength = state.finished ? remainingWidth : Int(Int64(remainingWidth) * value / total)
|
||||
let barPaddingLength = remainingWidth - barLength
|
||||
let bar = "\(String(repeating: config.theme.bar, count: barLength))\(String(repeating: " ", count: barPaddingLength))"
|
||||
components.append("|\(bar)|")
|
||||
@@ -195,40 +204,56 @@ extension ProgressBar {
|
||||
if !state.itemsName.isEmpty {
|
||||
itemsName = " \(state.itemsName)"
|
||||
}
|
||||
if let totalItems = state.totalItems {
|
||||
additionalComponents.append("\(state.items.formattedNumber()) of \(totalItems.formattedNumber())\(itemsName)")
|
||||
if state.finished {
|
||||
if let totalItems = state.totalItems {
|
||||
additionalComponents.append("\(totalItems.formattedNumber())\(itemsName)")
|
||||
}
|
||||
} else {
|
||||
additionalComponents.append("\(state.items.formattedNumber())\(itemsName)")
|
||||
if let totalItems = state.totalItems {
|
||||
additionalComponents.append("\(state.items.formattedNumber()) of \(totalItems.formattedNumber())\(itemsName)")
|
||||
} else {
|
||||
additionalComponents.append("\(state.items.formattedNumber())\(itemsName)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.size > 0 && allowProgress {
|
||||
var formattedCombinedSize = ""
|
||||
if config.showSize {
|
||||
var formattedSize = state.size.formattedSize()
|
||||
formattedSize = adjustFormattedSize(formattedSize)
|
||||
if let totalSize = state.totalSize {
|
||||
var formattedTotalSize = totalSize.formattedSize()
|
||||
formattedTotalSize = adjustFormattedSize(formattedTotalSize)
|
||||
formattedCombinedSize = combineSize(size: formattedSize, totalSize: formattedTotalSize)
|
||||
} else {
|
||||
formattedCombinedSize = formattedSize
|
||||
if state.finished {
|
||||
if config.showSize {
|
||||
if let totalSize = state.totalSize {
|
||||
var formattedTotalSize = totalSize.formattedSize()
|
||||
formattedTotalSize = adjustFormattedSize(formattedTotalSize)
|
||||
additionalComponents.append(formattedTotalSize)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var formattedCombinedSize = ""
|
||||
if config.showSize {
|
||||
var formattedSize = state.size.formattedSize()
|
||||
formattedSize = adjustFormattedSize(formattedSize)
|
||||
if let totalSize = state.totalSize {
|
||||
var formattedTotalSize = totalSize.formattedSize()
|
||||
formattedTotalSize = adjustFormattedSize(formattedTotalSize)
|
||||
formattedCombinedSize = combineSize(size: formattedSize, totalSize: formattedTotalSize)
|
||||
} else {
|
||||
formattedCombinedSize = formattedSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var formattedSpeed = ""
|
||||
if config.showSpeed {
|
||||
formattedSpeed = "\(state.sizeSpeed ?? state.averageSizeSpeed)"
|
||||
formattedSpeed = adjustFormattedSize(formattedSpeed)
|
||||
}
|
||||
var formattedSpeed = ""
|
||||
if config.showSpeed {
|
||||
formattedSpeed = "\(state.sizeSpeed ?? state.averageSizeSpeed)"
|
||||
formattedSpeed = adjustFormattedSize(formattedSpeed)
|
||||
}
|
||||
|
||||
if config.showSize && config.showSpeed {
|
||||
additionalComponents.append(formattedCombinedSize)
|
||||
additionalComponents.append(formattedSpeed)
|
||||
} else if config.showSize {
|
||||
additionalComponents.append(formattedCombinedSize)
|
||||
} else if config.showSpeed {
|
||||
additionalComponents.append(formattedSpeed)
|
||||
if config.showSize && config.showSpeed {
|
||||
additionalComponents.append(formattedCombinedSize)
|
||||
additionalComponents.append(formattedSpeed)
|
||||
} else if config.showSize {
|
||||
additionalComponents.append(formattedCombinedSize)
|
||||
} else if config.showSpeed {
|
||||
additionalComponents.append(formattedSpeed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,13 +18,16 @@
|
||||
public protocol ProgressTheme: Sendable {
|
||||
/// The icons used to represent a spinner.
|
||||
var spinner: [String] { get }
|
||||
/// The icons used to represent a progress bar.
|
||||
/// The icon used to represent a progress bar.
|
||||
var bar: String { get }
|
||||
/// The icon used to indicate that a progress bar finished.
|
||||
var done: String { get }
|
||||
}
|
||||
|
||||
public struct DefaultProgressTheme: ProgressTheme {
|
||||
public let spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
||||
public let bar = "█"
|
||||
public let done = "✔"
|
||||
}
|
||||
|
||||
extension ProgressTheme {
|
||||
|
||||
Reference in New Issue
Block a user