Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 68 additions & 3 deletions Sources/NXLibrary/ViewModels/AppViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,11 @@ public final class AppViewModel: ObservableObject {
private var environmentCheckGeneration = UUID()
private var buildStartedAt: Date?
private var buildTempDirectory: URL?
private var stageElapsedTimer: Timer?
private var currentStageStartedAt: Date?
private var currentBuildCacheURL: URL?
private let pipelineArchiveProgress = PipelineArchiveProgress()
private var isPollingPipelineArchive = false

public init(settingsStore: AppSettingsStore = AppSettingsStore()) {
self.settingsStore = settingsStore
Expand Down Expand Up @@ -2501,9 +2506,13 @@ public final class AppViewModel: ObservableObject {
)
lastPortingCommand = [command.executable.path] + command.arguments

stopStageElapsedTimer()
buildStartedAt = Date()
buildTempDirectory = tempRoot
buildLogURL = logURL
currentBuildCacheURL = workspace.buildCache
pipelineArchiveProgress.reset()
isPollingPipelineArchive = false
buildWizardStep = 3
isBuilding = true
isCancellingBuild = false
Expand Down Expand Up @@ -2557,6 +2566,7 @@ public final class AppViewModel: ObservableObject {

public func cancelBuild() {
guard isBuilding, let buildService else { return }
stopStageElapsedTimer()
isCancellingBuild = true
currentStageName = "Cancelling build..."
logs.append(LogEntry(level: .warning, message: "Cancelling build..."))
Expand Down Expand Up @@ -2601,6 +2611,8 @@ public final class AppViewModel: ObservableObject {
portingProgress?.apply(event)
switch event {
case .stage(let k, let n, let name, let step):
let now = Date()
currentStageStartedAt = now
currentStageName = name
stageIndex = k
totalStages = n
Expand All @@ -2612,10 +2624,15 @@ public final class AppViewModel: ObservableObject {
if let index = buildStages.firstIndex(where: { $0.id == k }) {
buildStages[index].isRunning = true
buildStages[index].step = step ?? ""
if buildStages[index].elapsedSeconds == nil {
buildStages[index].elapsedSeconds = 0
}
} else {
buildStages.append(BuildStageItem(id: k, name: name,
step: step ?? "", isRunning: true))
step: step ?? "", isRunning: true,
elapsedSeconds: 0))
}
startStageElapsedTimerIfNeeded()
if let step, !step.isEmpty { currentStepName = step }
logs.append(LogEntry(level: .step, message: "[\(k)/\(n)] \(name)"))

Expand All @@ -2626,9 +2643,14 @@ public final class AppViewModel: ObservableObject {
}
logs.append(LogEntry(level: .info, message: "↳ \(step)"))

case .progress(let fraction, let done, let total, let unit, let eta):
case .progress(let fraction, let done, let total, let unit, let eta, let elapsed):
progressFraction = min(max(fraction, 0), 1)
etaSeconds = eta
if let elapsed,
let index = buildStages.firstIndex(where: { $0.id == stageIndex && $0.isRunning }) {
buildStages[index].elapsedSeconds = elapsed
currentStageStartedAt = Date().addingTimeInterval(-elapsed)
}
if let done, let total, total > 0 {
if let unit, !unit.isEmpty,
unit.lowercased() != "b", unit.lowercased() != "bytes" {
Expand All @@ -2650,7 +2672,7 @@ public final class AppViewModel: ObservableObject {
buildStages[index].isCompleted = ok
buildStages[index].isFailed = !ok
buildStages[index].hasWarning = warned
buildStages[index].elapsedSeconds = elapsed
buildStages[index].elapsedSeconds = elapsed ?? buildStages[index].elapsedSeconds
buildStages[index].summary = summary
}
logs.append(LogEntry(
Expand All @@ -2669,8 +2691,51 @@ public final class AppViewModel: ObservableObject {
}
}

private func startStageElapsedTimerIfNeeded() {
guard stageElapsedTimer == nil else { return }
stageElapsedTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.tickStageElapsed()
}
}

private func stopStageElapsedTimer() {
stageElapsedTimer?.invalidate()
stageElapsedTimer = nil
currentStageStartedAt = nil
isPollingPipelineArchive = false
}

private func tickStageElapsed() {
guard let started = currentStageStartedAt,
let index = buildStages.firstIndex(where: { $0.id == stageIndex && $0.isRunning }) else { return }
let now = Date()
let elapsed = max(0, now.timeIntervalSince(started))
buildStages[index].elapsedSeconds = elapsed

if currentStageName == PortingProgress.pipelineArchiveStageName,
let cacheURL = currentBuildCacheURL,
!isPollingPipelineArchive {
isPollingPipelineArchive = true
let psoWorkDir = cacheURL.appendingPathComponent("pso/mac-work")
let progress = pipelineArchiveProgress
DispatchQueue.global(qos: .utility).async { [weak self] in
let event = progress.poll(psoWorkDirectory: psoWorkDir, stageStartedAt: started, now: Date())
DispatchQueue.main.async {
guard let self else { return }
self.isPollingPipelineArchive = false
if let event, self.isBuilding, self.currentStageName == PortingProgress.pipelineArchiveStageName {
self.handleBuildEvent(event)
}
}
}
}
}

private func finishBuild(_ result: BuildResult, gameID: UUID,
finalApp: URL, tempDir: URL) {
stopStageElapsedTimer()
currentBuildCacheURL = nil
pipelineArchiveProgress.reset()
portingProgress?.finish(success: result.success)
isBuilding = false
isCancellingBuild = false
Expand Down
13 changes: 1 addition & 12 deletions Sources/NXLibrary/Views/AddGame/AddGameStepThreeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ struct AddGameStepThreeView: View {
}
Spacer(minLength: canvas.u(12))
if let elapsed = stage.elapsedSeconds {
Text(elapsedText(elapsed))
Text(PortingETA.elapsed(elapsed))
.seleneFont(14, .regular, design: .monospaced)
.foregroundColor(SeleneTheme.ink60)
}
Expand Down Expand Up @@ -167,17 +167,6 @@ struct AddGameStepThreeView: View {
}
}

private func elapsedText(_ seconds: Double) -> String {
let total = Int(seconds.rounded())
if total < 60 { return "\(total) s" }
return String(format: "%d:%02d", total / 60, total % 60)
}

private func formatted(_ seconds: Double) -> String {
let total = Int(seconds.rounded())
return String(format: "%02d:%02d", total / 60, total % 60)
}

// MARK: - Success

/// The game whose new app waits for it to quit, if this porting's did.
Expand Down
29 changes: 26 additions & 3 deletions Sources/NXLibraryCore/Models/PortingPresentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,42 @@ public enum PortingETA {
switch progress.phase {
case .shaders:
guard let eta = progress.shaderETA else { return nil }
if let last = lastFinishingSeconds { return "About \(minutes(eta + last)) min left" }
return "Shaders: about \(minutes(eta)) min"
if let last = lastFinishingSeconds { return "About \(duration(eta + last)) left" }
return "Shaders: about \(duration(eta))"
case .finishing:
if let eta = progress.finishingETA { return "About \(duration(eta)) left" }
guard let last = lastFinishingSeconds else { return "Finishing up" }
return "About \(minutes(max(0, last - progress.finishingElapsed))) min left"
return "About \(duration(max(0, last - progress.finishingElapsed))) left"
case .preparing, .done, .failed:
return nil
}
}

/// Stage time in the Porting list: seconds under a minute, m:ss under an hour, h:mm:ss from an hour on.
public static func elapsed(_ seconds: Double) -> String {
let total = max(0, Int(seconds.rounded()))
if total < 60 { return "\(total) s" }
let hours = total / 3600
let minutes = (total % 3600) / 60
let secs = total % 60
if hours > 0 {
return String(format: "%d:%02d:%02d", hours, minutes, secs)
}
return String(format: "%d:%02d", minutes, secs)
}

static func minutes(_ seconds: Double) -> Int {
max(1, Int((seconds / 60).rounded()))
}

static func duration(_ seconds: Double) -> String {
let total = minutes(seconds)
let hours = total / 60
let mins = total % 60
if hours == 0 { return "\(mins) min" }
if mins == 0 { return "\(hours) hr" }
return "\(hours) hr \(mins) min"
}
}

/// What a game's art tile shows.
Expand Down
14 changes: 13 additions & 1 deletion Sources/NXLibraryCore/Models/PortingProgress.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public struct PortingProgress: Equatable {
}

public static let shaderStageName = "Mac shaders"
public static let pipelineArchiveStageName = "Pipeline archive"
public static let shaderShare = 0.9
/// The stages after the shaders when the engine has not counted them yet.
public static let expectedFinishingStages = 3
Expand All @@ -31,6 +32,8 @@ public struct PortingProgress: Equatable {
public private(set) var stageName = ""
public private(set) var stepName = ""
public private(set) var shaderETA: Double?
/// ETA during finishing stages (e.g. Pipeline archive).
public private(set) var finishingETA: Double?
/// Cache maintenance has no count or reliable ETA; show its activity instead.
public private(set) var isUpdatingShaderCache = false
/// Seconds the stages after the shaders took so far.
Expand All @@ -46,6 +49,7 @@ public struct PortingProgress: Equatable {
switch event {
case .stage(let k, let n, let name, let stepTitle):
isUpdatingShaderCache = false
finishingETA = nil
step = k
count = n
stageName = name
Expand All @@ -59,12 +63,20 @@ public struct PortingProgress: Equatable {
case .step(let title):
isUpdatingShaderCache = false
stepName = title
case .progress(let fraction, let done, _, _, let eta):
case .progress(let fraction, let done, _, _, let eta, _):
if phase == .finishing, stageName == Self.pipelineArchiveStageName, done != nil {
finishingETA = eta
let stages = max(Self.expectedFinishingStages, count - (shaderStep ?? count))
let clamped = min(max(fraction, 0), 0.99)
raise(to: Self.shaderShare + (1 - Self.shaderShare) * (Double(finishingEnded) + clamped) / Double(stages))
return
}
guard countsAsShaders(hasCount: done != nil) else { return }
phase = .shaders
raise(to: Self.shaderShare * min(max(fraction, 0), 1))
shaderETA = eta
case .stageEnd(let name, let ok, let elapsed, _, _):
finishingETA = nil
if name == Self.shaderStageName {
raise(to: Self.shaderShare)
phase = .finishing
Expand Down
2 changes: 1 addition & 1 deletion Sources/NXLibraryCore/Services/BuildEvent.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
public enum BuildEvent: Equatable {
case stage(k: Int, n: Int, name: String, step: String?)
case step(String)
case progress(fraction: Double, done: Double?, total: Double?, unit: String?, eta: Double?)
case progress(fraction: Double, done: Double?, total: Double?, unit: String?, eta: Double?, elapsed: Double? = nil)
/// `warn`: the stage finished without what it makes, though `ok` (a pipeline archive that failed, so the app is
/// packed without one). Engines before `warn` existed never send it.
case stageEnd(name: String, ok: Bool, elapsed: Double?, summary: String?, warn: Bool = false)
Expand Down
2 changes: 1 addition & 1 deletion Sources/NXLibraryCore/Services/BuildEventParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public enum BuildEventParser {
return wire.step.map { .step($0) }
case "progress":
return .progress(fraction: wire.frac ?? 0, done: wire.done,
total: wire.total, unit: wire.unit, eta: wire.eta)
total: wire.total, unit: wire.unit, eta: wire.eta, elapsed: wire.elapsed)
case "stage_end":
return .stageEnd(name: wire.name ?? "", ok: wire.ok ?? true,
elapsed: wire.elapsed, summary: wire.summary, warn: wire.warn ?? false)
Expand Down
Loading