Skip to content

Commit 98b26c9

Browse files
committed
fix: improve performance of the log view when many log lines are present
1 parent 6c5211c commit 98b26c9

File tree

4 files changed

+133
-50
lines changed

4 files changed

+133
-50
lines changed

client/App.svelte

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
</div>
4848

4949
<script lang="ts">
50-
import { onMount } from 'svelte';
50+
import { onMount, tick } from 'svelte';
5151
import SpatialNavigation from '@smart-powers/js-spatial-navigation';
5252
import {
5353
mdiTabletDashboard,
@@ -86,7 +86,7 @@
8686
readonly: false,
8787
});
8888
let logging: boolean | undefined = $state();
89-
let logs: [string, string][] = $state([]);
89+
let logs: string[] = $state([]);
9090
let gamepadUI = $state(false);
9191
let approot: HTMLElement;
9292
let tabs = $state([
@@ -141,43 +141,12 @@
141141
});
142142
143143
onMount(() => {
144-
function calculateColor(line: string) {
145-
const match = line.match(/^\[([^\]]+)\]/);
146-
if (!match) {
147-
return '#fff';
148-
}
149-
const redId = match[1];
150-
// Shuffle the chars for the other colors.
151-
const greenId = match[1].slice(-1) + match[1].slice(0, 1);
152-
const blueId = match[1].slice(-2) + match[1].slice(0, 2);
153-
154-
// Convert IDs to numbers.
155-
const redNum = [...redId].reduce<number>(
156-
(tot, char) => tot + char.charCodeAt(0),
157-
0,
158-
);
159-
const greenNum = [...greenId].reduce<number>(
160-
(tot, char) => tot + char.charCodeAt(0),
161-
0,
162-
);
163-
const blueNum = [...blueId].reduce<number>(
164-
(tot, char) => tot + char.charCodeAt(0),
165-
0,
166-
);
167-
168-
// Convert numbers to light colors.
169-
const red = ((redNum % 156) + 100).toString(16);
170-
const green = ((greenNum % 156) + 100).toString(16);
171-
const blue = ((blueNum % 156) + 100).toString(16);
172-
173-
// And convert RGB to a hex color.
174-
return `#${('00' + red).slice(-2)}${('00' + green).slice(-2)}${(
175-
'00' + blue
176-
).slice(-2)}`;
177-
}
178-
const unlisten = electronAPI.onLog((value) => {
179-
logs.push([calculateColor(value), value]);
144+
const unlisten = electronAPI.onLog(async (value) => {
145+
logs.push(...value.split('\n'));
146+
await tick();
147+
electronAPI.readyForLog();
180148
});
149+
electronAPI.readyForLog();
181150
electronAPI.getInfo();
182151
183152
return unlisten;

client/Log.svelte

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
<svelte:window onresize={render} />
2+
13
<div
24
style="display: flex; flex-direction: column; justify-content: space-between; height: 100%; gap: 1em"
35
>
@@ -9,10 +11,23 @@
911
variant="outlined"
1012
style="flex-basis: 0; flex-grow: 1; overflow: auto; background-color: #111; color-scheme: dark;"
1113
bind:this={output}
14+
onscroll={render}
1215
>
13-
<Content>
14-
<pre style="margin: 0;">{#each logs as log, i (i)}<div
15-
style="color: {log[0]};">{log[1]}</div>{/each}</pre>
16+
<Content style="position: relative;">
17+
<div
18+
style="height: {logSpacerHeight}px; width: {logSpacerWidth}px;"
19+
></div>
20+
<div
21+
bind:this={logViewer}
22+
class="mdc-typography--body2"
23+
style="position: absolute; font-family: monospace; margin: 0; top: {start *
24+
lineHeight}px;"
25+
>
26+
{#each logs.slice(start, end) as line}
27+
<!-- prettier-ignore -->
28+
<div style="white-space: pre; color: {calculateColor(line)};">{line}</div>
29+
{/each}
30+
</div>
1631
</Content>
1732
</Paper>
1833
</div>
@@ -23,7 +38,13 @@
2338
<Button variant="outlined" onclick={scrollToBottom}>
2439
<Label>Scroll to Bottom</Label>
2540
</Button>
26-
<Button variant="outlined" onclick={() => (logs = [])}>
41+
<Button
42+
variant="outlined"
43+
onclick={() => {
44+
logs = [];
45+
logSpacerWidth = 1;
46+
}}
47+
>
2748
<Label>Clear Log</Label>
2849
</Button>
2950
<Button
@@ -37,7 +58,7 @@
3758
</div>
3859

3960
<script lang="ts">
40-
import { tick } from 'svelte';
61+
import { onMount, tick } from 'svelte';
4162
import Paper, { Content } from '@smui/paper';
4263
import Button, { Label } from '@smui/button';
4364
import type { ElectronAPI } from '../server/preload.js';
@@ -49,26 +70,56 @@
4970
}: {
5071
electronAPI: ElectronAPI;
5172
logging: boolean | undefined;
52-
logs: [string, string][];
73+
logs: string[];
5374
} = $props();
5475
5576
let output: Paper;
77+
let logViewer: HTMLDivElement;
78+
let logSpacerHeight = $state(0);
79+
let logSpacerWidth = $state(0);
80+
let lineHeight = $state(20);
81+
let start = $state(0);
82+
let end = $state(0);
5683
57-
$effect(() => {
84+
onMount(() => {
85+
lineHeight = parseFloat(window.getComputedStyle(logViewer).lineHeight);
86+
if (isNaN(lineHeight)) {
87+
lineHeight = 20;
88+
}
89+
});
90+
91+
$effect.pre(() => {
5892
if (logs.length && output) {
5993
const el = output.getElement();
6094
const isHidden = el.clientHeight < 60;
6195
const isScrolledDown =
6296
el.scrollTop >= el.scrollHeight - el.clientHeight - 48;
6397
98+
logSpacerHeight = logs.length * lineHeight;
99+
64100
if (isHidden || isScrolledDown) {
65101
tick().then(() => {
66102
scrollToBottom();
103+
render();
67104
});
68105
}
106+
} else {
107+
logSpacerHeight = 0;
69108
}
70109
});
71110
111+
function render() {
112+
const el = output.getElement();
113+
114+
const visibleLines = el.clientHeight / lineHeight;
115+
start = Math.max(Math.floor(el.scrollTop / lineHeight) - 4, 0);
116+
end = Math.min(start + visibleLines + 8, logs.length);
117+
118+
tick().then(() => {
119+
logSpacerWidth = Math.max(logViewer.clientWidth, logSpacerWidth);
120+
});
121+
}
122+
72123
function scrollToBottom() {
73124
if (output) {
74125
const el = output.getElement();
@@ -77,4 +128,39 @@
77128
}
78129
}
79130
}
131+
132+
function calculateColor(line: string) {
133+
const match = line.match(/^\[([^\]]+)\]/);
134+
if (!match) {
135+
return '#fff';
136+
}
137+
const redId = match[1];
138+
// Shuffle the chars for the other colors.
139+
const greenId = match[1].slice(-1) + match[1].slice(0, 1);
140+
const blueId = match[1].slice(-2) + match[1].slice(0, 2);
141+
142+
// Convert IDs to numbers.
143+
const redNum = [...redId].reduce<number>(
144+
(tot, char) => tot + char.charCodeAt(0),
145+
0,
146+
);
147+
const greenNum = [...greenId].reduce<number>(
148+
(tot, char) => tot + char.charCodeAt(0),
149+
0,
150+
);
151+
const blueNum = [...blueId].reduce<number>(
152+
(tot, char) => tot + char.charCodeAt(0),
153+
0,
154+
);
155+
156+
// Convert numbers to light colors.
157+
const red = ((redNum % 156) + 100).toString(16);
158+
const green = ((greenNum % 156) + 100).toString(16);
159+
const blue = ((blueNum % 156) + 100).toString(16);
160+
161+
// And convert RGB to a hex color.
162+
return `#${('00' + red).slice(-2)}${('00' + green).slice(-2)}${(
163+
'00' + blue
164+
).slice(-2)}`;
165+
}
80166
</script>

server/main.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
shell,
1313
BrowserWindow,
1414
Menu,
15+
globalShortcut,
1516
} from 'electron';
1617
import { autoUpdater } from 'electron-updater';
1718

@@ -217,19 +218,38 @@ try {
217218
});
218219

219220
let logging = LOGGING;
221+
let logLines: string[] = [];
222+
let logInterval: NodeJS.Timeout | undefined = undefined;
223+
let readyForLog = false;
224+
225+
ipcMain.on('readyForLog', (event) => {
226+
readyForLog = true;
227+
});
220228

221229
ipcMain.on('stopLogging', async (event) => {
222230
logging = false;
231+
logLines = [];
223232
setLogFunction((_line: string) => {});
233+
clearInterval(logInterval);
234+
logInterval = undefined;
224235
event.sender.send('log', 'Logging disabled.');
225236
event.sender.send('logging', logging);
226237
});
227238

228239
ipcMain.on('startLogging', async (event) => {
229240
logging = true;
230241
setLogFunction((line: string) => {
231-
event.sender.send('log', line);
242+
logLines.push(line);
232243
});
244+
logInterval = setInterval(() => {
245+
if (!logLines.length || !readyForLog) {
246+
return;
247+
}
248+
const log = logLines.join('\n');
249+
logLines = [];
250+
event.sender.send('log', log);
251+
readyForLog = false;
252+
}, 250);
233253
event.sender.send('log', 'Logging enabled.');
234254
event.sender.send('logging', logging);
235255
});
@@ -290,10 +310,16 @@ try {
290310
);
291311
});
292312

293-
// win.webContents.openDevTools({
294-
// mode: 'detach',
295-
// activate: true,
296-
// });
313+
if (process.env.NODE_ENV === 'development') {
314+
globalShortcut.register('F12', () => {
315+
console.log('opening dev tools');
316+
if (win.webContents.isDevToolsOpened()) {
317+
win.webContents.closeDevTools();
318+
} else {
319+
win.webContents.openDevTools({ mode: 'bottom' });
320+
}
321+
});
322+
}
297323

298324
win.on('close', () => {
299325
setLogFunction((_line: string) => {});

server/preload.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export type ElectronAPI = {
3333
onOpenedFolders: (callback: (folders: string[]) => void) => () => void;
3434
stopLogging: () => void;
3535
startLogging: () => void;
36+
readyForLog: () => void;
3637
onLog: (callback: (line: string) => void) => () => void;
3738
getLogging: () => void;
3839
onLogging: (callback: (value: boolean) => void) => () => void;
@@ -83,6 +84,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
8384
},
8485
stopLogging: () => ipcRenderer.send('stopLogging'),
8586
startLogging: () => ipcRenderer.send('startLogging'),
87+
readyForLog: () => ipcRenderer.send('readyForLog'),
8688
onLog: (callback) => {
8789
const listener = (_event: IpcRendererEvent, line: string) => callback(line);
8890
ipcRenderer.on('log', listener);

0 commit comments

Comments
 (0)