Skip to content

Commit 068186e

Browse files
authored
Utilize react-window for the rendering log lines (#92)
This will allow us to handle very large verifier logs and not affect performance. This involved moving a lot of state back into the individual react components instead of utilizing `useMemo` for side effects on the dom to adjust css styles. Closes: #91
1 parent 62b59d6 commit 068186e

File tree

9 files changed

+758
-420
lines changed

9 files changed

+758
-420
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"private": true,
55
"dependencies": {
66
"react": "^19.1.0",
7-
"react-dom": "^19.1.0"
7+
"react-dom": "^19.1.0",
8+
"react-window": "^2.1.1"
89
},
910
"scripts": {
1011
"start": "vite",

src/App.test.tsx

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,26 @@
44
import "@testing-library/jest-dom";
55
import { render, createEvent, fireEvent } from "@testing-library/react";
66
import App from "./App";
7-
import { SAMPLE_LOG_DATA_1, SAMPLE_LOG_DATA_2 } from "./test-data";
7+
import {
8+
SAMPLE_LOG_DATA_1,
9+
SAMPLE_LOG_DATA_2,
10+
SAMPLE_LOG_DATA_ERORR,
11+
} from "./test-data";
12+
13+
// use screen.debug(); to log the whole DOM
814

915
const DOM_EL_FAIL = "DOM Element missing";
1016

1117
describe("App", () => {
18+
beforeEach(() => {
19+
const mockObserverInstance = {
20+
observe: jest.fn(),
21+
unobserve: jest.fn(),
22+
disconnect: jest.fn(),
23+
};
24+
global.ResizeObserver = jest.fn(() => mockObserverInstance);
25+
});
26+
1227
it("renders the correct starting elements", () => {
1328
const { container } = render(<App />);
1429
expect(container).toMatchSnapshot();
@@ -36,7 +51,7 @@ describe("App", () => {
3651
expect(container).toMatchSnapshot();
3752

3853
expect(document.getElementById("c-source-container")).toBeTruthy();
39-
expect(document.getElementById("formatted-log-lines")).toBeTruthy();
54+
expect(document.getElementById("log-content")).toBeTruthy();
4055
expect(document.getElementById("state-panel")).toBeTruthy();
4156

4257
// Hit the clear button and make sure we go back to the intial state
@@ -50,7 +65,7 @@ describe("App", () => {
5065
expect(container).toMatchSnapshot();
5166

5267
expect(document.getElementById("c-source-container")).toBeFalsy();
53-
expect(document.getElementById("formatted-log-lines")).toBeFalsy();
68+
expect(document.getElementById("log-content")).toBeFalsy();
5469
expect(document.getElementById("state-panel")).toBeFalsy();
5570
});
5671

@@ -80,41 +95,43 @@ describe("App", () => {
8095

8196
const logContainerEl = document.getElementById("log-container");
8297

83-
const line5 = document.getElementById("line-5");
84-
const line6 = document.getElementById("line-6");
85-
const line7 = document.getElementById("line-7");
98+
const line1 = document.getElementById("line-1");
99+
const line2 = document.getElementById("line-2");
100+
const line3 = document.getElementById("line-3");
86101

87-
if (!line5 || !line7 || !line6 || !logContainerEl) {
102+
if (!line1 || !line3 || !line2 || !logContainerEl) {
88103
throw new Error(DOM_EL_FAIL);
89104
}
90105

91-
expect(line5.innerHTML).toBe(
92-
'<span id="mem-slot-r7-line-5" class="mem-slot" data-id="r7">r7</span>&nbsp;=&nbsp;1',
106+
expect(line1.innerHTML).toBe(
107+
'<div class="pc-number">0</div><div class="dep-arrow" line-id="1" id="dep-arrow-line-1"></div><div class="log-line-content"><span id="mem-slot-r1-line-1" class="mem-slot" data-id="r1">r1</span>&nbsp;=&nbsp;0x11</div>',
93108
);
94109

95110
// Show that the next line is not an instruction
96-
expect(line6.innerHTML).toBe("; if (!n) @ rbtree.c:199");
111+
expect(line2.innerHTML).toBe(
112+
'<div class="pc-number">\n</div><div class="dep-arrow" line-id="2" id="dep-arrow-line-2"></div><div class="log-line-content">; if (!n) @ rbtree.c:199</div>',
113+
);
97114

98115
// Show the one after IS an instruction
99-
expect(line7.innerHTML).toBe(
100-
'if (<span id="mem-slot-r6-line-7" class="mem-slot" data-id="r6">r6</span>&nbsp;==&nbsp;0x0)&nbsp;goto&nbsp;pc+104',
116+
expect(line3.innerHTML).toBe(
117+
'<div class="pc-number">2</div><div class="dep-arrow" line-id="3" id="dep-arrow-line-3"></div><div class="log-line-content"><span id="mem-slot-r2-line-3" class="mem-slot" data-id="r2">r2</span>&nbsp;=&nbsp;0</div>',
101118
);
102119

103-
// Click on Line 5
104-
fireEvent(line5, createEvent.click(line5));
105-
expect(line5.classList.contains("selected-line")).toBeTruthy();
120+
// Click on Line 1
121+
fireEvent(line1, createEvent.click(line1));
122+
expect(line1.classList.contains("selected-line")).toBeTruthy();
106123

107124
// Keyboard Down Arrow
108125
fireEvent.keyDown(logContainerEl, { key: "ArrowDown", code: "ArrowDown" });
109-
expect(line5.classList.contains("selected-line")).toBeFalsy();
110-
expect(line6.classList.contains("selected-line")).toBeFalsy();
111-
expect(line7.classList.contains("selected-line")).toBeTruthy();
126+
expect(line1.classList.contains("selected-line")).toBeFalsy();
127+
expect(line2.classList.contains("selected-line")).toBeFalsy();
128+
expect(line3.classList.contains("selected-line")).toBeTruthy();
112129

113130
// Keyboard Up Arrow
114131
fireEvent.keyDown(logContainerEl, { key: "ArrowUp", code: "ArrowUp" });
115-
expect(line5.classList.contains("selected-line")).toBeTruthy();
116-
expect(line6.classList.contains("selected-line")).toBeFalsy();
117-
expect(line7.classList.contains("selected-line")).toBeFalsy();
132+
expect(line1.classList.contains("selected-line")).toBeTruthy();
133+
expect(line2.classList.contains("selected-line")).toBeFalsy();
134+
expect(line3.classList.contains("selected-line")).toBeFalsy();
118135
});
119136

120137
it("c lines and state panel containers are collapsible", async () => {
@@ -164,7 +181,9 @@ describe("App", () => {
164181
});
165182

166183
it("highlights the associated c source or log line(s) when the other is clicked ", async () => {
167-
render(<App />);
184+
// Note: I haven't figured out how to get react-window to scroll in jest dom tests
185+
// so just make the list height static so it renders the lines we care about
186+
render(<App testListHeight={1000} />);
168187

169188
const inputEl = document.getElementById("input-text");
170189
if (!inputEl) {
@@ -235,17 +254,19 @@ describe("App", () => {
235254
inputEl,
236255
createEvent.paste(inputEl, {
237256
clipboardData: {
238-
getData: () => SAMPLE_LOG_DATA_2,
257+
getData: () => SAMPLE_LOG_DATA_ERORR,
239258
},
240259
}),
241260
);
242261

243-
const line117El = document.getElementById("line-117");
244-
if (!line117El) {
262+
const line1El = document.getElementById("line-1");
263+
if (!line1El) {
245264
throw new Error(DOM_EL_FAIL);
246265
}
247266

248-
expect(line117El.classList).toContain("error-message");
249-
expect(line117El.innerHTML).toBe("R6 invalid mem access 'scalar'");
267+
expect(line1El.classList).toContain("error-message");
268+
expect(line1El.innerHTML).toBe(
269+
'<div class="pc-number">\n</div><div class="dep-arrow" line-id="1" id="dep-arrow-line-1"></div><div class="log-line-content">R6 invalid mem access \'scalar\'</div>',
270+
);
250271
});
251272
});

src/App.tsx

Lines changed: 66 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import React from "react";
2-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1+
import React, { RefObject } from "react";
2+
import { useCallback, useEffect, useRef, useState } from "react";
3+
import { ListImperativeAPI, useListRef } from "react-window";
34

45
import {
56
VerifierLogState,
@@ -11,7 +12,6 @@ import {
1112
import {
1213
fetchLogFromUrl,
1314
getVisibleLogLineRange,
14-
scrollToLogLine,
1515
scrollToCLine,
1616
siblingInsLine,
1717
getVisibleLogLines,
@@ -70,9 +70,7 @@ function getVisualLogState(
7070
const ContentRaw = ({
7171
loadError,
7272
visualLogState,
73-
selectedLine,
74-
selectedMemSlotId,
75-
selectedCLine,
73+
selectedState,
7674
handlePaste,
7775
handleMainContentClick,
7876
handleCLinesClick,
@@ -83,12 +81,15 @@ const ContentRaw = ({
8381
handleFullLogToggle,
8482
onGotoStart,
8583
onGotoEnd,
84+
logListRef,
85+
visualLogStart,
86+
visualLogEnd,
87+
onLogRowsRendered,
88+
testListHeight,
8689
}: {
8790
loadError: string | null;
8891
visualLogState: VisualLogState;
89-
selectedLine: number;
90-
selectedMemSlotId: string;
91-
selectedCLine: number;
92+
selectedState: LogLineState;
9293
handlePaste: (event: React.ClipboardEvent) => void;
9394
handleMainContentClick: (event: React.MouseEvent<HTMLDivElement>) => void;
9495
handleCLinesClick: (event: React.MouseEvent<HTMLDivElement>) => void;
@@ -99,16 +100,19 @@ const ContentRaw = ({
99100
handleFullLogToggle: () => void;
100101
onGotoStart: () => void;
101102
onGotoEnd: () => void;
103+
logListRef: RefObject<ListImperativeAPI | null>;
104+
visualLogStart: number;
105+
visualLogEnd: number;
106+
onLogRowsRendered: (start: number, end: number) => void;
107+
testListHeight: number | undefined;
102108
}) => {
103109
if (loadError) {
104110
return <div>{loadError}</div>;
105111
} else if (visualLogState.logLines.length > 0) {
106112
return (
107113
<MainContent
108114
visualLogState={visualLogState}
109-
selectedLine={selectedLine}
110-
selectedMemSlotId={selectedMemSlotId}
111-
selectedCLine={selectedCLine}
115+
selectedState={selectedState}
112116
handleCLinesClick={handleCLinesClick}
113117
handleMainContentClick={handleMainContentClick}
114118
handleLogLinesClick={handleLogLinesClick}
@@ -118,6 +122,11 @@ const ContentRaw = ({
118122
handleFullLogToggle={handleFullLogToggle}
119123
onGotoStart={onGotoStart}
120124
onGotoEnd={onGotoEnd}
125+
logListRef={logListRef}
126+
visualLogStart={visualLogStart}
127+
visualLogEnd={visualLogEnd}
128+
onLogRowsRendered={onLogRowsRendered}
129+
testListHeight={testListHeight}
121130
/>
122131
);
123132
} else {
@@ -133,7 +142,10 @@ const ContentRaw = ({
133142

134143
const Content = React.memo(ContentRaw);
135144

136-
function App() {
145+
// testListHeight is only used in our unit tests because react-window
146+
// doesn't seem to respond to scroll events in the virtual dom
147+
// so we set the height manually to include more log lines
148+
function App({ testListHeight }: { testListHeight?: number }) {
137149
const [visualLogState, setVisualLogState] = useState<VisualLogState>(
138150
getEmptyVisualLogState(),
139151
);
@@ -145,8 +157,16 @@ function App() {
145157
);
146158
const [loadError, setLoadError] = useState<string | null>(null);
147159
const [isLoading, setIsLoading] = useState<boolean>(false);
160+
const [visualIndexRange, setVisualIndexRange] = useState<{
161+
visualLogStart: number;
162+
visualLogEnd: number;
163+
}>({ visualLogStart: 0, visualLogEnd: 0 });
164+
const onLogRowsRendered = useCallback((start: number, end: number) => {
165+
setVisualIndexRange({ visualLogStart: start, visualLogEnd: end });
166+
}, []);
148167

149168
const fileInputRef = useRef<HTMLInputElement>(null);
169+
const logListRef = useListRef(null);
150170

151171
const {
152172
verifierLogState,
@@ -156,29 +176,24 @@ function App() {
156176
logLineIdxToVisualIdx,
157177
} = visualLogState;
158178

159-
const { line: selectedLine, memSlotId: selectedMemSlotId } = selectedState;
179+
const { line: selectedLine } = selectedState;
160180
const selectedLineVisualIdx = logLineIdxToVisualIdx.get(selectedLine) || 0;
161181
const hoveredLineVisualIdx =
162182
logLineIdxToVisualIdx.get(hoveredState.line) || 0;
163-
const selectedCLine = useMemo(() => {
164-
let clineId = "";
165-
if (selectedState.cLine) {
166-
clineId = selectedState.cLine;
167-
} else {
168-
const parsedLine = verifierLogState.lines[selectedState.line];
169-
if (!parsedLine) {
170-
return 0;
171-
}
172-
if (parsedLine.type === ParsedLineType.C_SOURCE) {
173-
clineId = parsedLine.id;
174-
} else {
175-
clineId =
176-
verifierLogState.cSourceMap.logLineToCLine.get(selectedState.line) ||
177-
"";
183+
184+
const scrollToLogLine = useCallback(
185+
(index: number) => {
186+
if (index < 0) {
187+
return;
178188
}
179-
}
180-
return verifierLogState.cSourceMap.cSourceLines.get(clineId)?.lineNum || 0;
181-
}, [verifierLogState, selectedState]);
189+
const list = logListRef.current;
190+
list?.scrollToRow({
191+
index,
192+
align: "center",
193+
});
194+
},
195+
[logListRef],
196+
);
182197

183198
const setSelectedAndScroll = useCallback(
184199
(
@@ -188,7 +203,7 @@ function App() {
188203
nextCLineVisualIdx: number,
189204
memSlotId: string = "",
190205
) => {
191-
scrollToLogLine(nextInsLineVisualIdx, logLines.length);
206+
scrollToLogLine(nextInsLineVisualIdx);
192207
scrollToCLine(nextCLineVisualIdx, cLines.length);
193208
setSelectedState({ line: nextInsLineId, memSlotId, cLine: nextCLineId });
194209
},
@@ -248,9 +263,17 @@ function App() {
248263
const handleKeyDown = (e: KeyboardEvent) => {
249264
let delta = 0;
250265
let areCLinesInFocus = selectedState.cLine !== "";
251-
let { min, max } = getVisibleLogLineRange(
252-
areCLinesInFocus ? cLines.length : logLines.length,
253-
);
266+
let min = 0;
267+
let max = 0;
268+
269+
if (areCLinesInFocus) {
270+
const range = getVisibleLogLineRange(cLines.length);
271+
min = range.min;
272+
max = range.max;
273+
} else {
274+
min = visualIndexRange.visualLogStart;
275+
max = visualIndexRange.visualLogEnd;
276+
}
254277
let page = max - min + 1;
255278
switch (e.key) {
256279
case "ArrowDown":
@@ -528,7 +551,7 @@ function App() {
528551
const maxIdx = arr[arr.length - 1];
529552
const visualIdx = logLineIdxToVisualIdx.get(maxIdx);
530553
if (visualIdx !== undefined) {
531-
scrollToLogLine(visualIdx, logLines.length);
554+
scrollToLogLine(visualIdx);
532555
}
533556
}
534557

@@ -677,9 +700,7 @@ function App() {
677700
<Content
678701
loadError={loadError}
679702
visualLogState={visualLogState}
680-
selectedLine={selectedLine}
681-
selectedMemSlotId={selectedMemSlotId}
682-
selectedCLine={selectedCLine}
703+
selectedState={selectedState}
683704
handlePaste={handlePaste}
684705
handleMainContentClick={handleMainContentClick}
685706
handleCLinesClick={handleCLinesClick}
@@ -690,6 +711,11 @@ function App() {
690711
handleFullLogToggle={handleFullLogToggle}
691712
onGotoStart={onGotoStart}
692713
onGotoEnd={onGotoEnd}
714+
logListRef={logListRef}
715+
visualLogStart={visualIndexRange.visualLogStart}
716+
visualLogEnd={visualIndexRange.visualLogEnd}
717+
onLogRowsRendered={onLogRowsRendered}
718+
testListHeight={testListHeight}
693719
/>
694720
<div id="hint">
695721
<SelectedLineHint

0 commit comments

Comments
 (0)