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
156 lines
9.0 KiB
Python
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()
|