Performance
This commit is contained in:
parent
923835203a
commit
44656c81d5
3 changed files with 45 additions and 10 deletions
|
|
@ -17,12 +17,29 @@ class BackupEntry(
|
||||||
val sizeBytes: Long by lazy { sizeComputer() }
|
val sizeBytes: Long by lazy { sizeComputer() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class BackupMetrics(val fileCount: Int, val sizeBytes: Long)
|
||||||
|
|
||||||
object BackupHistory {
|
object BackupHistory {
|
||||||
|
|
||||||
// Used internally for parsing — java.time formatter since kotlinx.datetime
|
// Used internally for parsing — java.time formatter since kotlinx.datetime
|
||||||
// doesn't have custom format patterns built in
|
// doesn't have custom format patterns built in
|
||||||
private val JAVA_TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")
|
private val JAVA_TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts backups without parsing timestamps or sorting — just matches the naming pattern.
|
||||||
|
*/
|
||||||
|
fun countBackups(backupDir: File): Int {
|
||||||
|
if (!backupDir.exists() || !backupDir.isDirectory) return 0
|
||||||
|
return backupDir.listFiles()?.count { file ->
|
||||||
|
val name = file.nameWithoutExtension
|
||||||
|
val isZip = file.extension.equals("zip", ignoreCase = true) && file.isFile
|
||||||
|
val isDir = file.isDirectory
|
||||||
|
if (!isZip && !isDir) return@count false
|
||||||
|
try { java.time.LocalDateTime.parse(name, JAVA_TIMESTAMP_FORMAT); true }
|
||||||
|
catch (_: DateTimeParseException) { false }
|
||||||
|
} ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists all existing backups in the backup directory, sorted newest first.
|
* Lists all existing backups in the backup directory, sorted newest first.
|
||||||
*/
|
*/
|
||||||
|
|
@ -37,13 +54,18 @@ object BackupHistory {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the file count for a backup entry without computing its full size.
|
* Computes file count and size in a single pass over the backup tree.
|
||||||
*/
|
*/
|
||||||
fun fileCount(entry: BackupEntry): Int {
|
fun computeMetrics(entry: BackupEntry): BackupMetrics {
|
||||||
return if (entry.isCompressed) {
|
return if (entry.isCompressed) {
|
||||||
java.util.zip.ZipFile(entry.path).use { it.size() }
|
java.util.zip.ZipFile(entry.path).use { zip ->
|
||||||
|
BackupMetrics(zip.size(), entry.path.length())
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
entry.path.walkTopDown().count { it.isFile }
|
var count = 0
|
||||||
|
var size = 0L
|
||||||
|
entry.path.walkTopDown().filter { it.isFile }.forEach { count++; size += it.length() }
|
||||||
|
BackupMetrics(count, size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,19 @@ object BackupScheduler {
|
||||||
if (schedulerJob?.isActive == true) return
|
if (schedulerJob?.isActive == true) return
|
||||||
logger.info { "Backup scheduler started" }
|
logger.info { "Backup scheduler started" }
|
||||||
|
|
||||||
|
// Eagerly set timestamps so StatusScreen has data immediately.
|
||||||
|
// Only set lastBackupTime and nextBackupTime here (cheap).
|
||||||
|
// The full lastBackupResult (fileCount, sizeBytes) is filled by the async path
|
||||||
|
// since fileCount() walks the directory tree and would block the main thread.
|
||||||
|
val config = ConfigManager.config.value
|
||||||
|
if (config.isConfigured && config.backupPath != null) {
|
||||||
|
val backups = BackupHistory.listBackups(File(config.backupPath))
|
||||||
|
if (backups.isNotEmpty()) {
|
||||||
|
_state.update { it.copy(lastBackupTime = backups.first().timestamp) }
|
||||||
|
}
|
||||||
|
updateNextBackupTime(TimeZone.currentSystemDefault())
|
||||||
|
}
|
||||||
|
|
||||||
schedulerJob = scope.launch {
|
schedulerJob = scope.launch {
|
||||||
// Collect progress from BackupEngine
|
// Collect progress from BackupEngine
|
||||||
launch {
|
launch {
|
||||||
|
|
@ -65,23 +78,23 @@ object BackupScheduler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// On first config emission, restore last backup info from disk
|
// On first config emission, restore last backup info from disk
|
||||||
if (_state.value.lastBackupTime == null && config.backupPath != null) {
|
if (_state.value.lastBackupResult == null && config.backupPath != null) {
|
||||||
val backups = BackupHistory.listBackups(File(config.backupPath))
|
val backups = BackupHistory.listBackups(File(config.backupPath))
|
||||||
if (backups.isNotEmpty()) {
|
if (backups.isNotEmpty()) {
|
||||||
val latest = backups.first()
|
val latest = backups.first()
|
||||||
val fileCount = BackupHistory.fileCount(latest)
|
val metrics = BackupHistory.computeMetrics(latest)
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
lastBackupTime = latest.timestamp,
|
lastBackupTime = latest.timestamp,
|
||||||
lastBackupResult = BackupResult.Success(
|
lastBackupResult = BackupResult.Success(
|
||||||
backupPath = latest.path,
|
backupPath = latest.path,
|
||||||
fileCount = fileCount,
|
fileCount = metrics.fileCount,
|
||||||
sizeBytes = latest.sizeBytes,
|
sizeBytes = metrics.sizeBytes,
|
||||||
durationMs = 0,
|
durationMs = 0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
logger.info { "Restored last backup info from disk: ${latest.path.name} ($fileCount files)" }
|
logger.info { "Restored last backup info from disk: ${latest.path.name} (${metrics.fileCount} files)" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class StatusViewModel : ViewModel() {
|
||||||
BackupScheduler.state.map { it.lastBackupResult }.distinctUntilChanged(),
|
BackupScheduler.state.map { it.lastBackupResult }.distinctUntilChanged(),
|
||||||
) { backupPath, _ ->
|
) { backupPath, _ ->
|
||||||
if (backupPath != null) {
|
if (backupPath != null) {
|
||||||
BackupHistory.listBackups(File(backupPath)).size
|
BackupHistory.countBackups(File(backupPath))
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue