90 lines
4.7 KiB
Python
90 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: MPL-2.0
|
|
"""Repeatable Gaia frame-callback/scroll and system-load measurements on Nokia.
|
|
|
|
Frame callbacks measure application scheduling, not physical panel refresh.
|
|
No calls, messages, connectivity changes or flash operations.
|
|
"""
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import time
|
|
|
|
spec=importlib.util.spec_from_file_location('wifi_probe',Path(__file__).with_name('test-beatles-wifi.py'))
|
|
bridge=importlib.util.module_from_spec(spec);spec.loader.exec_module(bridge)
|
|
probe=bridge.probe
|
|
|
|
|
|
def counters():
|
|
rows=bridge.controls.shell('cat /proc/stat /proc/vmstat /proc/meminfo /proc/uptime').splitlines()
|
|
result={}
|
|
for row in rows:
|
|
fields=row.split()
|
|
if fields[0]=='cpu':result['cpu']=list(map(int,fields[1:9]))
|
|
elif fields[0] in ('processes','pgmajfault','pswpin','pswpout','MemFree:','Cached:','SwapFree:'):
|
|
result[fields[0].rstrip(':')]=int(fields[1])
|
|
result['uptime']=float(rows[-1].split()[0])
|
|
return result
|
|
|
|
|
|
def delta(a,b):
|
|
cpu=[y-x for x,y in zip(a['cpu'],b['cpu'])];duration=b['uptime']-a['uptime']
|
|
return {'seconds':duration,'cpuBusyPercent':round(100*(1-cpu[3]/sum(cpu)),2),
|
|
'taskCreationsPerSecond':round((b['processes']-a['processes'])/duration,2),
|
|
'majorFaultsPerSecond':round((b['pgmajfault']-a['pgmajfault'])/duration,2),
|
|
'swapInPagesPerSecond':round((b['pswpin']-a['pswpin'])/duration,2),
|
|
'swapFreeKiB':b['SwapFree']}
|
|
|
|
|
|
def measure(app,scroll=False):
|
|
# Find a real scroll container; preserve its position after the benchmark.
|
|
script='''const scroll=SCROLL;
|
|
const candidates=[...document.querySelectorAll('body *')].filter(e=>e.clientHeight>100 && e.scrollHeight>e.clientHeight+40 && ['auto','scroll'].includes(getComputedStyle(e).overflowY) && e.checkVisibility());
|
|
const surface=candidates.sort((a,b)=>b.clientHeight-a.clientHeight)[0];
|
|
if(scroll && !surface)throw Error('No visible scroll surface');
|
|
const old=surface?.scrollTop || 0,available=surface?surface.scrollHeight-surface.clientHeight:0;
|
|
const max=Math.min(available,500);
|
|
return await new Promise(resolve=>{const gaps=[];let previous=performance.now(),start=previous;
|
|
function frame(now){gaps.push(now-previous);previous=now;
|
|
if(scroll)surface.scrollTop=max*(1-Math.cos((now-start)*Math.PI/1500))/2;
|
|
if(now-start<6000)requestAnimationFrame(frame);
|
|
else{if(scroll)surface.scrollTop=old;const sorted=gaps.slice().sort((a,b)=>a-b);
|
|
resolve({frames:gaps.length,seconds:(now-start)/1000,callbackFps:gaps.length*1000/(now-start),
|
|
p50ms:sorted[Math.floor(sorted.length*.5)],p95ms:sorted[Math.floor(sorted.length*.95)],
|
|
stallsOver50ms:gaps.filter(n=>n>50).length,scrollRange:scroll?max:0,
|
|
surface:scroll?(surface.id || surface.tagName):null,availableScrollRange:available});}}
|
|
requestAnimationFrame(frame);
|
|
});'''.replace('SCROLL',json.dumps(scroll))
|
|
a=counters();value=probe(app,script);b=counters()
|
|
return {**value,'system':delta(a,b)}
|
|
|
|
|
|
def main():
|
|
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--output',type=Path,required=True)
|
|
args=parser.parse_args();report={'device':'beatles','metric':'requestAnimationFrame callbacks, not panel FPS',
|
|
'cpuMetric':'Non-idle CPU time, including I/O wait','measurements':{}}
|
|
try:
|
|
probe('system','dispatchEvent(new CustomEvent("home"));return true;');time.sleep(2)
|
|
report['measurements']['homeIdle']=measure('homescreen')
|
|
report['measurements']['homeScroll']=measure('homescreen',True)
|
|
probe('system','await VulpesCompat.call("apps.launch",{manifestURL:"http://settings.localhost:8765/manifest.webapp"});return true;')
|
|
deadline=time.monotonic()+45
|
|
while True:
|
|
try:
|
|
ready=probe('settings','return document.readyState === "complete" && typeof require === "function" && !!document.querySelector("#root");')
|
|
except RuntimeError as error:
|
|
if 'find(...) is null' not in str(error):raise
|
|
ready=False
|
|
if ready:break
|
|
if time.monotonic()>deadline:raise RuntimeError('Settings did not finish loading')
|
|
time.sleep(1)
|
|
probe('settings','document.querySelector("#wifi-status button[type=reset]")?.click();require(["modules/settings_service"],s=>s.navigate("root"));return true;')
|
|
time.sleep(3)
|
|
report['measurements']['settingsScroll']=measure('settings',True)
|
|
finally:
|
|
args.output.parent.mkdir(parents=True,exist_ok=True);args.output.write_text(json.dumps(report,indent=2)+'\n')
|
|
print(json.dumps(report,indent=2))
|
|
|
|
|
|
if __name__=='__main__':main()
|