Files
qbz/scripts/bench_qt_artwork.py
T
vicrodh 2fc03d325d fix(qt): restore window lifecycle and improve grid artwork
Restore tray-aware close and quit confirmation, circular label covers, and missing label artist portraits. Publish artwork progressively, preload two grid rows, and skip redundant motion reports while preserving data refreshes.

Include regression tests and isolated artwork benchmarks. Verified Linux build, QML checks, and scrolling smoke tests.

cx
2026-09-09 21:58:25 -06:00

156 lines
9.0 KiB
Python

#!/usr/bin/env python3
"""Isolated A/B microbenchmark of the real RoundedImage/Skeleton components.
Measures the reveal/decode path, NOT the complete application's memory budget
or the Rust thumbnail pipeline. Uses existing thumbs read-only; fixtures and
logs are temporary. Run without a Cargo build or screen recording in progress.
"""
import argparse
import json
import os
import re
from pathlib import Path
import subprocess
import tempfile
import time
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
QML = '/usr/lib64/qt6/bin/qml'
def run(root, candidate, dpr, display, repeat):
env = dict(os.environ, DISPLAY=display, QT_QPA_PLATFORM='xcb',
QSG_RHI_BACKEND='opengl', LIBGL_ALWAYS_SOFTWARE='1', QT_SCALE_FACTOR=str(dpr))
env.pop('WAYLAND_DISPLAY', None)
log = root / f'run-{dpr}-{repeat}-{candidate}.log'
bench = root / 'Bench.qml'
bench.write_text(re.sub(r'property bool candidate: (?:true|false)',
'property bool candidate: ' + str(candidate).lower(), bench.read_text()))
with log.open('w') as stream:
p = subprocess.Popen([QML, '-I', str(root / 'imports'), '-f', str(root / 'Bench.qml')],
env=env, stdout=stream, stderr=subprocess.STDOUT)
rss_peak = cpu = rss_last = 0
try:
deadline = time.monotonic() + 25
while p.poll() is None and time.monotonic() < deadline:
try:
stat = Path(f'/proc/{p.pid}/stat').read_text().split(') ', 1)[1].split()
cpu = (int(stat[11]) + int(stat[12])) / os.sysconf('SC_CLK_TCK')
rss = Path(f'/proc/{p.pid}/smaps_rollup').read_text()
rss_last = int(next(line.split()[1] for line in rss.splitlines() if line.startswith('Rss:')))
rss_peak = max(rss_peak, rss_last)
except (FileNotFoundError, ProcessLookupError):
pass
time.sleep(.05)
if p.poll() is None:
raise RuntimeError('benchmark did not finish')
finally:
if p.poll() is None:
p.kill(); p.wait()
content = log.read_text()
if p.returncode or any(s in content for s in ('ReferenceError', 'TypeError', 'is not a type', 'Cannot open', 'failed to create')):
raise RuntimeError(content)
match = re.search(r'BENCH_RESULT (\d+) (\d+) (\d+) (\d+) (\d+)', content)
if not match or int(match[1]) < 100 or int(match[2]) < 100 or int(match[3]) < 100 or int(match[4]) < 100 or int(match[5]) != 0:
raise RuntimeError('benchmark did not render enough images/frames: '+content)
return dict(ready_events=int(match[1]), frames=int(match[2]), revealed=int(match[3]), handed_over=int(match[4]), candidate=candidate, dpr=dpr, cpu_s=cpu, peak_mib=rss_peak/1024,
end_mib=rss_last/1024, log=str(log))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--thumbs', type=Path, default=Path.home()/'.local/share/qbz/thumbnails')
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--repeats', type=int, default=3)
args = parser.parse_args()
root = Path(tempfile.mkdtemp(prefix='qbz-artwork-bench-'))
module = root/'imports/com/blitzfc/qbz'; module.mkdir(parents=True)
derivatives = {}; sources = []
for src in sorted(args.thumbs.iterdir()):
if len(sources) >= 96: break
try:
im = Image.open(src).convert('RGBA')
except (OSError, ValueError):
continue
url = src.resolve().as_uri(); sources.append(url)
for edge in (200, 400):
scale = max(edge/im.width, edge/im.height)
w, h = int(im.width*scale+.5), int(im.height*scale+.5)
out = root/f'{len(sources)}-{w}x{h}.png'
im.resize((w,h), Image.Resampling.LANCZOS).save(out)
derivatives[f'{url}|{w}x{h}'] = out.as_uri()
if len(sources) < 30: raise RuntimeError('need at least 30 existing thumbnails')
(module/'qmldir').write_text('module com.blitzfc.qbz\nsingleton QbzShell 1.0 QbzShell.qml\nsingleton QbzPlayer 1.0 QbzPlayer.qml\nsingleton QbzLocal 1.0 QbzLocal.qml\nsingleton QbzSession 1.0 QbzSession.qml\n')
(module/'QbzShell.qml').write_text('pragma Singleton\nimport QtQuick\nQtObject { property string themeJson: ""; property int ambientMode: 0; property bool reduceMotion: false; property bool forceCanvasArt: false }')
(module/'QbzLocal.qml').write_text('pragma Singleton\nimport QtQuick\nQtObject { function artworkImmediateEnabled() { return true } }')
(module/'QbzPlayer.qml').write_text('pragma Singleton\nimport QtQuick\nQtObject { property bool npHasTrack: false }')
(module/'QbzSession.qml').write_text('pragma Singleton\nimport QtQuick\nQtObject { property var paths: '+json.dumps(derivatives)+'; signal artScaledReady(string path, string scaled, int w, int h); function artScaledCached(p,w,h) { return paths[p+"|"+w+"x"+h] || "" } function artScaled(p,w,h) {} }')
(root/'Bench.qml').write_text('''import QtQuick
import QtQuick.Window
import "'''+(ROOT/'crates/qbz-qt/qml/theme').as_uri()+'''" as Theme
import "'''+(ROOT/'crates/qbz-qt/qml/controls').as_uri()+'''" as Controls
Window {
id: benchWindow; width: 1280; height: 900; visible: true; color: "#121212"
property bool candidate: false
property int readyEvents: 0
property int revealEvents: 0
property int handoverEvents: 0
property int modeErrors: 0
property int frames: 0
onFrameSwapped: frames++
Component.onCompleted: grid.model = 200
property var sources: '''+json.dumps(sources)+'''
ListView {
id: grid; anchors.fill: parent; model: 0; cacheBuffer: 0; reuseItems: true
delegate: Row {
id: row; required property int index; height: 246; spacing: 10
Repeater {
model: 6
delegate: Item {
required property int index; width: 200; height: 246
property bool candidate: false
property bool immediateReveal: false
Component.onCompleted: {
candidate = benchWindow.candidate && benchWindow.screen.devicePixelRatio <= 1
immediateReveal = candidate && benchWindow.screen.devicePixelRatio <= 1
artSource = Qt.binding(function () {
return benchWindow ? benchWindow.sources[(row.index*6+index)%benchWindow.sources.length] : ""
})
}
property string artSource: benchWindow ? benchWindow.sources[(row.index*6+index)%benchWindow.sources.length] : ""
Theme.RoundedImage { id: art; width: 200; height: 200; radius: 8; source: parent.artSource; gridArtwork: parent.candidate; onReadyChanged: if (ready && benchWindow) benchWindow.readyEvents++
onRevealedChanged: if (revealed && benchWindow) { benchWindow.revealEvents++; if (fadeMs !== (benchWindow.candidate && benchWindow.screen.devicePixelRatio <= 1 ? 0 : 200)) benchWindow.modeErrors++ }
}
Controls.QbzSkeleton { width: 200; height: 200; variant: "art";
coverSource: parent.candidate ? "" : parent.artSource;
coverReady: parent.candidate && art.ready;
handoverFadeMs: parent.immediateReveal ? 0 : 180; animated: false
onHandedOverChanged: if (handedOver && benchWindow) { benchWindow.handoverEvents++; if (handoverFadeMs !== (benchWindow.candidate && benchWindow.screen.devicePixelRatio <= 1 ? 0 : 180)) benchWindow.modeErrors++ }
}
}
}
}
SequentialAnimation on contentY {
running: true
PauseAnimation { duration: 500 }
NumberAnimation { to: 18000; duration: 6000 }
NumberAnimation { to: 0; duration: 6000 }
PauseAnimation { duration: 1500 }
ScriptAction { script: { console.log("BENCH_RESULT", benchWindow.readyEvents, benchWindow.frames, benchWindow.revealEvents, benchWindow.handoverEvents, benchWindow.modeErrors); grid.model = 0; Qt.callLater(function() { Qt.exit(0) }) } }
}
}
}''')
xvfb = subprocess.Popen(['Xvfb','-displayfd','1','-screen','0','2560x1800x24','-nolisten','tcp'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
try:
display = ':'+xvfb.stdout.readline().decode().strip()
rows=[]
for dpr in (1,2):
for repeat in range(args.repeats):
for candidate in ([False,True] if repeat%2==0 else [True,False]):
result=run(root,candidate,dpr,display,repeat); rows.append(result)
print(json.dumps(result), flush=True)
args.output.write_text(json.dumps({'scope':'RoundedImage + QbzSkeleton microbenchmark, Xvfb/llvmpipe; not whole QBZ', 'fixtures':str(root), 'runs':rows},indent=2)+'\n')
finally:
xvfb.terminate(); xvfb.wait(timeout=5)
if __name__ == '__main__': main()