Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions clients/web/store.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
// Cap each history buffer so a long session can't grow the arrays (and the
// per-render join/DOM rebuild) without bound. Older lines drop off the front.
// This is the main guard against the mobile "taps get slower over time" lag.
const HISTORY_BUFFER_LIMIT = 500;

function pushCappedHistory(lines, text) {
lines.push(text);
const overflow = lines.length - HISTORY_BUFFER_LIMIT;
if (overflow > 0) {
lines.splice(0, overflow);
}
}

export function createStore() {
const state = {
connection: {
Expand Down Expand Up @@ -58,9 +71,9 @@ export function createStore() {
if (!state.historyBuffers[buffer]) {
state.historyBuffers[buffer] = [];
}
state.historyBuffers[buffer].push(text);
pushCappedHistory(state.historyBuffers[buffer], text);
if (buffer !== "all") {
state.historyBuffers.all.push(text);
pushCappedHistory(state.historyBuffers.all, text);
}
notify();
},
Expand Down
65 changes: 42 additions & 23 deletions clients/web/ui/history.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export function createHistoryView({
const isMobileLike = window.matchMedia("(pointer: coarse)").matches;
let mobileCollapsed = isMobileLike;
let renderedLogBuffer = "";
let renderedLogCount = 0;
let renderedLogValue = null;
let renderScheduled = false;

function ensureBufferPosition(bufferName) {
if (!Object.hasOwn(bufferPositions, bufferName)) {
Expand Down Expand Up @@ -72,41 +73,59 @@ export function createHistoryView({
}
}

function render() {
function flushRender() {
renderScheduled = false;
for (const name of getBufferNames()) {
ensureBufferPosition(name);
}
const bufferName = store.state.historyBuffer;
const lines = store.state.historyBuffers[bufferName] || [];
historyEl.value = lines.join("\n");
const joined = lines.join("\n");

// Skip the DOM work entirely when nothing in the visible buffer changed.
// notify() fires on every state change (menu navigation, audio, etc.), so
// most flushes here are no-ops — this keeps arrow-key menu moves cheap.
if (bufferName === renderedLogBuffer && joined === renderedLogValue) {
return;
}
renderedLogBuffer = bufferName;
renderedLogValue = joined;

historyEl.value = joined;
historyEl.scrollTop = historyEl.scrollHeight;

if (historyLogEl) {
const needsRebuild = renderedLogBuffer !== bufferName || renderedLogCount > lines.length;
if (needsRebuild) {
historyLogEl.replaceChildren();
for (const line of lines) {
const row = document.createElement("p");
row.className = "history-line";
row.textContent = line;
historyLogEl.appendChild(row);
}
renderedLogBuffer = bufferName;
renderedLogCount = lines.length;
} else if (lines.length > renderedLogCount) {
for (let i = renderedLogCount; i < lines.length; i += 1) {
const row = document.createElement("p");
row.className = "history-line";
row.textContent = lines[i];
historyLogEl.appendChild(row);
}
renderedLogBuffer = bufferName;
renderedLogCount = lines.length;
// Rebuild from scratch (bounded by the store's per-buffer cap). A full
// rebuild is correct even when the cap trims lines off the front, which
// the previous incremental count-based append could not detect.
const fragment = document.createDocumentFragment();
for (const line of lines) {
const row = document.createElement("p");
row.className = "history-line";
row.textContent = line;
fragment.appendChild(row);
}
historyLogEl.replaceChildren(fragment);
historyLogEl.scrollTop = historyLogEl.scrollHeight;
}
}

// Coalesce bursts of notify() calls (a single tap can append many lines, each
// firing notify()) into one render per animation frame instead of one render
// per line. This is what turns the old O(lines added * buffer size) churn per
// tap into a single bounded render.
function render() {
if (renderScheduled) {
return;
}
renderScheduled = true;
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(flushRender);
} else {
flushRender();
}
}

function renderMobileVisibility() {
if (!historyContentEl || !historyToggleEl || !historyLogEl) {
return;
Expand Down
Loading