luci-app-qosify: cut the cost of a poll tick

The page polls, so the work per tick matters on the targets this
package is aimed at. The tick no longer re-reads both config files and
rebuilds three fieldsets; it patches the service table cells and the
autostart button in place with dom.content.

The Status tab is the expensive one -- qosify-status forks tc twice per
active interface -- so it now fetches when the tab is opened rather
than on every page load, keeps its 10 s tick rather than picking up
L.env.pollinterval, paints the per-interface summary from ubus without
waiting on the fork, and patches the <pre> in place so a tick cannot
reset the scroll position while it is being read. Poll.step() holds the
next tick until the promise settles and an overlapping refresh is
dropped, so a fork slower than the interval skips ticks instead of
stacking up.

po/templates/qosify.pot regenerated with build/i18n-scan.pl.

Signed-off-by: Ash Clarke <clarkeaj@hotmail.co.uk>
pull/8971/merge
Ash Clarke 2026-09-13 21:57:00 +01:00 committed by Dirk Brenken
parent b562acac48
commit 3791919f3e
2 changed files with 524 additions and 236 deletions

View File

@ -342,6 +342,10 @@ return view.extend({
pane.addEventListener('cbi-tab-active',function(){
self.currentTab=t[0];
try{history.replaceState(null,'','#'+names[t[0]]);}catch(e){}
// The Status tab costs a fork per active interface, so it is fetched
// when it is opened rather than on every page load; initTabGroup fires
// this from a requestAnimationFrame, so the pane is in the DOM.
if(t[0]==='st')self.refreshStatus();
});
group.appendChild(pane);
});
@ -358,10 +362,16 @@ return view.extend({
return root;
},
// Both tabs tick at 10 s. Overview is six ubus calls and no forks; the Status
// tab forks qosify-status, which runs tc twice per active interface, so it is
// the expensive one and does not get a faster tick. Poll.step() holds
// the next tick until the promise this returns settles, and refreshStatus()
// drops an overlapping call, so a fork slower than the interval skips ticks
// instead of stacking up.
installPollers:function(){
var self=this;
poll.add(function(){if(self.currentTab!=='ov'||self._n)return;return self.refreshOverview();},10);
poll.add(function(){return self.refreshStatus();},5);
poll.add(function(){if(self.currentTab!=='st'||self._n)return;return self.refreshStatus();},10);
},
tabOverview:function(ctx){
@ -565,6 +575,17 @@ return view.extend({
return tbl;
},
updateSvcTable:function(ctx){
var n=this.svcNodes(ctx),map={init:'qos-svc-init',auto:'qos-svc-auto',run:'qos-svc-run',shaped:'qos-svc-shaped'},k,el;
for(k in map){el=$(map[k]);if(el)dom.content(el,n[k]);}
el=$('qos-btn-auto');
if(el){
el.className='cbi-button '+(ctx.enabled?'cbi-button-positive':'cbi-button-negative');
el.title=ctx.enabled?_('Click to disable autostart'):_('Click to enable autostart');
dom.content(el,ctx.enabled?_('Enabled'):_('Disabled'));
}
},
renderCfgFiles:function(ctx){
var rulesN=(ctx.rulesN!=null)?ctx.rulesN:countRules(ctx.rulesText);
var cfgOk=(ctx.cfgOk!=null)?ctx.cfgOk:((ctx.cfgRaw||'').length>10&&/(^|\n)config /.test(ctx.cfgRaw||''));
@ -962,7 +983,11 @@ return view.extend({
tabStatus:function(ctx){
var section=E('div',{'id':'qos-st'});
var fs1=E('fieldset',{'class':'cbi-section'},E('legend',{},_('qosify-status')));
var body=E('div',{'id':'qos-st-body'});
var body=E('div',{'id':'qos-st-body'},[
E('div',{'id':'qos-st-sum'}),
E('pre',{'id':'qos-st-pre','class':'qos-pre','style':'display:none'}),
E('div',{'id':'qos-st-msg'})
]);
this.fillStatus(body,ctx);
fs1.appendChild(body);
section.appendChild(fs1);
@ -982,15 +1007,51 @@ return view.extend({
return out;
},
// Runs on every poll tick and twice per open, so only the summary table is
// rebuilt: replacing the <pre> would throw away the scroll position while it
// is being read. ctx.qstatus null means the fork has not returned yet, '' means
// it returned nothing -- the two used to look the same on screen.
fillStatus:function(body,ctx){
dom.content(body,'');
var sum=body.querySelector('#qos-st-sum'),pre=body.querySelector('#qos-st-pre'),msg=body.querySelector('#qos-st-msg');
if(!sum||!pre||!msg)return;
var note=function(t){dom.content(msg,E('p',{'class':'qos-muted'},E('em',{},t)));};
if(!ctx.running){
body.appendChild(E('div',{'class':'alert-message warning'},_('qosify is not running. Start from the Overview tab.')));
}else if(!ctx.qstatus){
body.appendChild(E('p',{'style':'opacity:.7'},E('em',{},_('qosify-status returned no output.'))));
}else{
body.appendChild(E('pre',{'style':'background:#1e1e1e;color:#e0e0e0;padding:12px;border:1px solid #333;border-radius:4px;overflow-x:auto;font-size:12px;line-height:1.5;white-space:pre-wrap'},ctx.qstatus));
dom.content(sum,'');
pre.style.display='none';
dom.content(msg,E('div',{'class':'alert-message warning'},_('qosify is not running. Start from the Overview tab.')));
return;
}
dom.content(sum,this.statusSummary(ctx.status));
pre.style.display=ctx.qstatus?'':'none';
if(ctx.qstatus){
if(pre.textContent!==ctx.qstatus)pre.textContent=ctx.qstatus;
dom.content(msg,'');
}
else if(this.readonly)note(_('The detailed tc output needs write access to this page.'));
else if(ctx.qstatus==null)note(_('Reading tc output...'));
else note(_('qosify-status returned no output.'));
},
// ubus call qosify status, so the per-interface summary costs no forks
statusSummary:function(st){
var tbl=E('table',{'class':'qos-kv','width':'100%'}),b=E('tbody');
tbl.appendChild(b);
['interfaces','devices'].forEach(function(g){
var t=st&&st[g],k,e;
for(k in t){
e=t[k]||{};
b.appendChild(E('tr',{},[
E('td',{},(g==='devices'?_('device %s'):_('interface %s')).format(k)),
E('td',{},[
E('span',{'class':'qos-badge '+(e.active?'qos-green':'qos-red')},e.active?_('active'):_('inactive')),
E('span',{'class':'qos-muted','style':'margin-left:8px'},
_('device: %s, ingress: %s, egress: %s').format(e.ifname||'-',e.ingress?_('yes'):_('no'),e.egress?_('yes'):_('no')))
])
]));
}
});
if(!b.firstChild)b.appendChild(E('tr',{},E('td',{'class':'qos-muted'},E('em',{},_('qosify has no interfaces or devices configured')))));
return tbl;
},
// === Actions ===
@ -1508,18 +1569,18 @@ return view.extend({
});
},
// Poll path: six ubus calls (uci.get and gatherCtx(false)'s five), no shell
// forks, and the parts of the page that hold user input or focus are patched
// in place rather than rebuilt.
refreshOverview:function(){
var self=this;
self.lock();
uci.unload('qosify');
return uci.load('qosify').then(function(){
return self.gatherCtx();
return self.gatherCtx(false);
}).then(function(ctx){
self.fillSect('qos-svc-sect',self.buildSvcSect(ctx));
self.updateSvcTable(ctx);
self.fillSect('qos-cfg-sect',self.buildCfgSect(ctx));
self.fillSect('qos-ctl-sect',self.buildCtlSect(ctx));
var stb=$('qos-st-body');
if(stb)self.fillStatus(stb,ctx);
var bd=$('q-en-badge');
if(bd){
var sn=ifSect(),w=(sn&&uci.get('qosify',sn.id))||{};
@ -1532,22 +1593,30 @@ return view.extend({
refreshOverviewFull:function(){
var self=this;
return self.refreshOverview().then(function(ctx){
self.fillSect('qos-svc-sect',self.buildSvcSect(ctx));
self.fillSect('qos-ctl-sect',self.buildCtlSect(ctx));
self.fillSect('qos-qs-sect',self.buildQsSect(ctx));
return ctx;
});
},
refreshStatus:function(){
if(this.currentTab!=='st')return;
var self=this;
if(self.currentTab!=='st'||self._st)return Promise.resolve();
self._st=true;
var ex=self.readonly?Promise.resolve(null):L.resolveDefault(fs.exec('/usr/sbin/qosify-status',[]),null);
return Promise.all([
L.resolveDefault(callServiceList('qosify'),{}),
L.resolveDefault(fs.exec('/usr/sbin/qosify-status',[]),{stdout:''})
L.resolveDefault(callQosifyStatus(),{})
]).then(function(d){
var ctx={running:isRunning(d[0]),qstatus:(d[1]&&d[1].stdout)||''};
var ctx={running:isRunning(d[0]),status:d[1]||{},qstatus:self.readonly?'':null};
var stb=$('qos-st-body');
if(stb)self.fillStatus(stb,ctx);
});
return ex.then(function(r){
ctx.qstatus=self.readonly?'':((r&&r.stdout)||'');
if(stb)self.fillStatus(stb,ctx);
});
}).finally(function(){self._st=false;});
},
// which = 'cfg' | 'rules' | undefined: the editor for the file just written is

File diff suppressed because it is too large Load Diff