perf(v5): Kompressions-Fortschritt als kleine Nachricht, Roh-Größen 30 s gemerkt

WAS: Ein Prozent-Tick von HandBrake (etwa einmal je Sekunde) geht jetzt
als `kompression-stand` (nur die Warteschlange) an Haupt und Fenster; der
volle Vorgangs-Stand — mit rekursivem Ordnerlauf durch jeden Roh-Ordner,
fremden Ordnern und zwei statfs — kommt nur noch bei einem Phasenwechsel
oder auf Zuruf. Roh-Größen werden 30 s gemerkt; Löschen, Aufräumen und der
Knopf „Aktualisieren" messen frisch.
WARUM: Durchsicht 12.09.2026, Fund F7 — auf einer NAS als Arbeitsordner
war das ein Netz-Ordnerlauf pro Sekunde, und das Fenster zeichnete jede
Sekunde komplett neu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-12 21:33:22 +02:00
co-authored by Claude Opus 5
parent 904e4b2ef6
commit 551f1d7b2c
6 changed files with 62 additions and 7 deletions
@@ -213,6 +213,10 @@ function kernNachricht(nachricht: KernNachricht): void {
case 'vorgaenge':
stand.vorgaenge = nachricht.stand
break
case 'kompression-stand':
// 5.7.0 (F7): nur die Warteschlange frisch — der Rest bleibt stehen.
if (stand.vorgaenge !== null) stand.vorgaenge = { ...stand.vorgaenge, kompression: nachricht.kompression }
break
case 'kompression-ergebnis':
meldung(`${nachricht.ok ? '' : '⚠ '}Kompression „${nachricht.titel}": ${nachricht.text}`)
break
@@ -414,6 +414,9 @@ export type KernNachricht =
/** Alle Vorgänge, die Roh-Belegung und die Kompressions-Warteschlange
* (5.4.0) — geht auch ans Haupt (Taskleiste, Updates warten). */
| { art: 'vorgaenge'; stand: VorgaengeStand }
/** 5.7.0 (F7): NUR die Warteschlange — jeder Prozent-Tick der Kompression
* kommt so, ohne den teuren vollen Vorgangs-Stand (Ordnerläufe, statfs). */
| { art: 'kompression-stand'; kompression: VorgaengeStand['kompression'] }
/** Eine Kompression ist zu Ende — fürs Toast und die Ereignis-Zeile. */
| { art: 'kompression-ergebnis'; vorgangId: string; titel: string; ok: boolean; text: string }
| { art: 'laufwerk-gesundheit'; id: string; bericht: LaufwerkBericht }
@@ -594,6 +597,8 @@ export function istKernNachricht(wert: unknown): wert is KernNachricht {
istObjekt((wert.stand as Record<string, unknown>).kompression) &&
istObjekt((wert.stand as Record<string, unknown>).statistik)
)
case 'kompression-stand':
return istObjekt(wert.kompression) && Array.isArray((wert.kompression as Record<string, unknown>).wartend)
case 'kompression-ergebnis':
return (
istKennung(wert.vorgangId) &&
+3
View File
@@ -121,6 +121,9 @@ function start(): void {
// 5.4.0: Die Kompression läuft OHNE Laufwerk weiter — Updates
// warten auch auf sie, und die Taskleiste zeigt ihren Fortschritt.
kompressionVerarbeiten(nachricht.stand.kompression.laeuft)
} else if (nachricht.art === 'kompression-stand') {
// 5.7.0 (F7): der leichte Fortschritts-Tick — dieselbe Wirkung.
kompressionVerarbeiten(nachricht.kompression.laeuft)
} else if (nachricht.art === 'kompression-ergebnis' && !ohneNebenwirkungen) {
toast(nachricht.ok ? 'Rippy ist fertig' : 'Rippy: Kompression fehlgeschlagen', `${nachricht.titel}: ${nachricht.text}`)
} else if (nachricht.art === 'disc-info' && nachricht.info.titel.length > 0 && !ohneNebenwirkungen) {
@@ -165,7 +165,7 @@ export function ordnerGroesse(pfad: string): number {
}
/** Ordner unter roh\, die kein Vorgang kennt — nur zeigen, nie von selbst löschen. */
export function fremdeOrdner(rohWurzel: string, bekannte: ReadonlySet<string>): FremderOrdner[] {
export function fremdeOrdner(rohWurzel: string, bekannte: ReadonlySet<string>, groesse: (pfad: string) => number = ordnerGroesse): FremderOrdner[] {
if (!existsSync(rohWurzel)) return []
const liste: FremderOrdner[] = []
let namen: string[]
@@ -182,7 +182,7 @@ export function fremdeOrdner(rohWurzel: string, bekannte: ReadonlySet<string>):
for (const name of namen) {
const voll = join(rohWurzel, name)
if (bekannte.has(voll) || bekannte.has(name)) continue
liste.push({ ordner: voll, bytes: ordnerGroesse(voll) })
liste.push({ ordner: voll, bytes: groesse(voll) })
}
return liste.sort((a, b) => b.bytes - a.bytes)
}
+47 -4
View File
@@ -115,12 +115,46 @@ export class VorgangDienst {
}
}
/** Jede Änderung der Warteschlange — Fortschritt eingeschlossen. 5.7.0
* (Durchsicht F7): Ein Prozent-Tick ist eine KLEINE Nachricht
* (kompression-stand); vorher löste jeder Tick (HandBrake: etwa einmal je
* Sekunde) den vollen Vorgangs-Stand aus — mit einem Ordnerlauf durch
* jeden Roh-Ordner, zwei statfs und einem kompletten Neuzeichnen des
* Fensters. Der volle Stand geht nur noch bei einem Phasenwechsel. */
private standGeaendert(laeuft: KompressionStand | null): void {
if (laeuft !== null && this.letztePhase.get(laeuft.vorgangId) !== laeuft.phase) {
const phasenwechsel = laeuft !== null && this.letztePhase.get(laeuft.vorgangId) !== laeuft.phase
if (laeuft !== null && phasenwechsel) {
this.letztePhase.set(laeuft.vorgangId, laeuft.phase)
this.db?.vorgangAktualisieren(laeuft.vorgangId, { phase: laeuft.phase === 'legt-ab' ? 'legt-ab' : 'komprimiert' })
}
this.senden()
if (phasenwechsel || laeuft === null) {
this.senden()
return
}
this.kontext.anHauptUndFenster({
art: 'kompression-stand',
kompression: { ...this.warteschlange.stand(), pausiert: this.warteschlange.istPausiert() },
})
}
// Roh-Größen sind teuer (rekursiver Ordnerlauf, bei einer NAS über das
// Netz) und ändern sich nur, wenn Rippy selbst etwas anlegt oder löscht —
// deshalb 30 s lang gemerkt (F7). Wer löscht, leert das Gedächtnis.
private groessen = new Map<string, { bytes: number; wann: number }>()
private static readonly GROESSEN_FRISCH_MS = 30_000
private groesseGemerkt(pfad: string): number {
const jetzt = Date.now()
const alt = this.groessen.get(pfad)
if (alt !== undefined && jetzt - alt.wann < VorgangDienst.GROESSEN_FRISCH_MS) return alt.bytes
const bytes = ordnerGroesse(pfad)
this.groessen.set(pfad, { bytes, wann: jetzt })
return bytes
}
/** Nach Löschen oder Aufräumen: frisch messen. */
private groessenVergessen(): void {
this.groessen.clear()
}
private async ergebnis(auftrag: KompressionsAuftrag, ergebnis: AblageErgebnis): Promise<void> {
@@ -220,7 +254,7 @@ export class VorgangDienst {
let rohGesamtBytes = 0
const vorgaenge: VorgangEintrag[] = liste.map((v) => {
const rohDa = v.rohGeloeschtAm === null && existsSync(v.rohOrdner)
const rohBytes = rohDa ? ordnerGroesse(v.rohOrdner) : 0
const rohBytes = rohDa ? this.groesseGemerkt(v.rohOrdner) : 0
rohGesamtBytes += rohBytes
const auftrag = auftragAusJson(v.auftrag)
const ruhig = v.phase === 'fertig' || v.phase === 'fehler' || v.phase === 'abgebrochen'
@@ -247,7 +281,7 @@ export class VorgangDienst {
}
})
const bekannte = new Set(liste.map((v) => v.rohOrdner))
const fremde = fremdeOrdner(this.einstellungen.rohWurzel(), bekannte)
const fremde = fremdeOrdner(this.einstellungen.rohWurzel(), bekannte, (pfad) => this.groesseGemerkt(pfad))
for (const f of fremde) rohGesamtBytes += f.bytes
return {
vorgaenge,
@@ -259,6 +293,12 @@ export class VorgangDienst {
}
}
/** Der Knopf „Aktualisieren": frisch messen, nicht aus dem Gedächtnis. */
neuMessen(): void {
this.groessenVergessen()
this.senden()
}
senden(): void {
try {
this.kontext.anHauptUndFenster({ art: 'vorgaenge', stand: this.stand() })
@@ -290,6 +330,7 @@ export class VorgangDienst {
fertigeDateien: dateienAusJson(v.fertigeDateien, (g) => this.kontext.protokoll.schreiben(`${v.id}: ${g}`)),
}))
const ergebnis = aufraeumen(kandidaten, regel, new Date())
if (ergebnis.geloescht.length > 0) this.groessenVergessen()
const jetzt = new Date().toISOString()
for (const id of ergebnis.geloescht) {
this.db.vorgangAktualisieren(id, { rohGeloeschtAm: jetzt, rohBytes: this.db.vorgangLesen(id)?.rohBytes ?? 0 })
@@ -321,6 +362,7 @@ export class VorgangDienst {
}
try {
rmSync(v.rohOrdner, { recursive: true, force: true })
this.groessenVergessen()
this.db?.vorgangAktualisieren(vorgangId, { rohGeloeschtAm: new Date().toISOString() })
this.kontext.hinweis(`Roh-Dateien von „${v.titel}" gelöscht (von Hand).`)
} catch (fehler) {
@@ -341,6 +383,7 @@ export class VorgangDienst {
}
try {
rmSync(ordner, { recursive: true, force: true })
this.groessenVergessen()
this.kontext.hinweis(`Ordner „${ordner}" gelöscht (von Hand).`)
} catch (fehler) {
this.kontext.fehler(`${ordner}" nicht gelöscht: ${String(fehler)}`)
+1 -1
View File
@@ -289,7 +289,7 @@ function fensterNachricht(frage: unknown): void {
void werkzeuge.schluesselLauf().then(() => werkzeuge.senden())
return
case 'vorgaenge-laden':
vorgaenge.senden()
vorgaenge.neuMessen()
return
case 'vorgang-roh-loeschen':
vorgaenge.rohLoeschen(frage.vorgangId)