Release Vulpes OS 2.7-preview.22
This commit is contained in:
parent
200d1fb235
commit
a138f97877
294 changed files with 515786 additions and 187 deletions
90
tundra/tools/test-beatles-fluidity.py
Normal file
90
tundra/tools/test-beatles-fluidity.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
#!/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()
|
||||
Loading…
Reference in a new issue