webui v162: API timings in the phone status sheet

A message typed on a phone far from the core takes ten to twenty seconds
to show up, and nothing on the phone says where the seconds go: a slow
request on its own points at the core, many requests pending at once
points at the browser's six sockets -- one of them held by the event
stream -- or at the link in between.

rswebui.js now counts requests in flight, times every request, keeps the
last /rsChats/sendChat round trip and the five slowest requests, and
stamps the event stream (bytes received, last event, reconnections). The
phone status sheet shows them under the version label.
This commit is contained in:
jolavillette 2026-08-29 12:37:17 +02:00
parent e9d9db3021
commit 62a3ced094
4 changed files with 78 additions and 3 deletions

View File

@ -137,7 +137,7 @@ const navbar = () => {
? 'Connected to RetroShare Core'
: 'Connection Lost',
}),
m('span.webui-version', { style: { fontSize: '0.7em' } }, 'v161'),
m('span.webui-version', { style: { fontSize: '0.7em' } }, 'v162'),
m('i.fas.fa-sync-alt.refresh-icon', {
style: { cursor: 'pointer', fontSize: '0.8em' },
onclick: () => window.location.reload(true),
@ -260,7 +260,24 @@ const MobileStatus = () => {
m('small', statusbar.formatBytes(state.totalOut)),
]),
]),
m('.mobile-status-sheet__version', 'WebUI v161'),
// Where the seconds go, seen from this phone. A request slow on its
// own points at the core; many pending at once points at the
// browser's six sockets, one of them held by the event stream.
(() => {
const s = rs.apiStats;
const ago = (t) => (t ? Math.round((Date.now() - t) / 1000) + 's ago' : 'never');
const short = (p) => String(p || '').replace(/^\/rs/, '');
return m('.mobile-status-sheet__diag', [
m('h4', 'API from this browser'),
m('div', `pending ${s.pending} · total ${s.total} · up ${Math.round((Date.now() - s.startedAt) / 1000)}s`),
m('div', s.lastSend
? `last sendChat ${s.lastSend.ms} ms (${ago(s.lastSend.at)})`
: 'no sendChat yet'),
m('div', `events: ${statusbar.formatBytes(s.eventsBytes)}, last ${ago(s.lastEventAt)}, restarts ${s.eventsRestarts}`),
s.slowest.length > 0 && m('div', 'slowest: ' + s.slowest.map((e) => `${short(e.path)} ${e.ms}ms`).join(', ')),
]);
})(),
m('.mobile-status-sheet__version', 'WebUI v162'),
])),
];
},

View File

@ -96,6 +96,33 @@ function logout() {
m.route.set('/');
}
// What the API is doing, seen from this browser. Read by the phone status
// sheet: a request that takes ten seconds shows here, and whether it was slow
// on its own or queued behind others (pending) is what tells the two apart.
const apiStats = {
pending: 0,
total: 0,
// Last /rsChats/sendChat: the one round trip the user feels directly.
lastSend: null,
// The five slowest requests since load, newest first on a tie.
slowest: [],
// Event stream: bytes received since (re)connection, last event time,
// number of reconnections.
eventsBytes: 0,
lastEventAt: 0,
eventsRestarts: 0,
startedAt: Date.now(),
};
function recordRequestTime(path, ms) {
apiStats.pending = Math.max(0, apiStats.pending - 1);
const entry = { path, ms: Math.round(ms), at: Date.now() };
if (path === '/rsChats/sendChat') apiStats.lastSend = entry;
apiStats.slowest.push(entry);
apiStats.slowest.sort((a, b) => b.ms - a.ms);
if (apiStats.slowest.length > 5) apiStats.slowest.length = 5;
}
const connectionState = {
status: true,
// Status of the last HTTP response, or 0 when the request never reached the
@ -120,6 +147,9 @@ function rsJsonApiRequest(
headers['Authorization'] = 'Basic ' + btoa(loginKey.username + ':' + loginKey.passwd);
}
}
apiStats.pending += 1;
apiStats.total += 1;
const startedAt = performance.now();
// NOTE: After upgrading to mithrilv2, options.extract is no longer required
// since the status will become part of return value and then
// handleDeserialize can also be simply passed as options.deserialize
@ -145,6 +175,7 @@ function rsJsonApiRequest(
xhr: config,
})
.then((result) => {
recordRequestTime(path, performance.now() - startedAt);
if (result.status === 200) {
connectionState.status = true;
try {
@ -176,6 +207,7 @@ function rsJsonApiRequest(
return result;
})
.catch(function (e) {
recordRequestTime(path, performance.now() - startedAt);
// Reaching here after a valid 200 means the body could not be parsed,
// i.e. the response was cut short. The core answered and is still there;
// it is the answer that did not survive the trip.
@ -424,6 +456,8 @@ function startEventQueue(
xhr.onprogress = (ev) => {
const currIndex = xhr.responseText.length;
apiStats.eventsBytes = currIndex;
apiStats.lastEventAt = Date.now();
if (currIndex > lastIndex) {
const parts = xhr.responseText.substring(lastIndex, currIndex);
lastIndex = currIndex;
@ -467,6 +501,7 @@ function startEventQueue(
xhr.onload = () => { };
xhr.onerror = (err) => {
apiStats.eventsRestarts += 1;
console.error('[RS] Event Queue XHR error occurred:', err);
// Retry after 5 seconds to avoid silent event loss
setTimeout(() => {
@ -541,6 +576,7 @@ module.exports = {
rsJsonApiRequest,
idToHex: hexId,
connectionState,
apiStats,
setKeys,
setBackgroundTask,
logon,

View File

@ -615,3 +615,25 @@
}
}
}
/* API diagnostics in the phone status sheet (main.js MobileStatus): request
* timings seen from this browser, so a slow phone can be measured without a
* console. */
.mobile-status-sheet__diag {
margin-top: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
background: rgba(15, 23, 42, 0.06);
font-size: 0.75rem;
line-height: 1.4;
color: #334155;
word-break: break-word;
h4 {
margin: 0 0 0.25rem;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
}
}

File diff suppressed because one or more lines are too long