Skip to content

Commit f0a2941

Browse files
feat: track response time of invoke and sendSync methods on ipcRenderer (#275)
* feat: track ipcRenderer invoke/sendSync response time * feat: link related events using UUID * fix: remove `uuid` and `node:perf_hooks` to fix preload loading issue * chore: remove duplicate type entry Co-authored-by: Sam Maddock <samuel.maddock@gmail.com> * chore: remove unused className Co-authored-by: Sam Maddock <samuel.maddock@gmail.com> * refactor: define Channel and Listener type aliases at top level * refactor: add IPC payload helpers * fix: handle stale `uuidMapRef` indices when event list is full * fix: prevent crash when service worker is undefined * fix: comments * refactor: improve type safety * chore: move scroll logic to `useLayoutEffect` * lint --------- Co-authored-by: Sam Maddock <samuel.maddock@gmail.com>
1 parent 415dfab commit f0a2941

File tree

8 files changed

+448
-88
lines changed

8 files changed

+448
-88
lines changed

package-lock.json

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/extension/react/components/DetailPanel.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ type Props = {
1010
selectedRow: IpcEventDataIndexed | null;
1111
onClose: () => void;
1212
direction?: 'right' | 'bottom';
13+
gotoRow?: (row: number) => void;
1314
};
14-
function DetailPanel({ selectedRow, onClose, direction = 'right' }: Props) {
15+
function DetailPanel({ selectedRow, onClose, direction = 'right', gotoRow }: Props) {
1516
const { theme } = useDevtronContext();
1617
if (!selectedRow) return null;
1718

@@ -92,6 +93,31 @@ function DetailPanel({ selectedRow, onClose, direction = 'right' }: Props) {
9293
</span>
9394
</div>
9495
)}
96+
97+
{/* Response Time */}
98+
{selectedRow.responseTime && (
99+
<div className="flex w-fit items-center gap-x-1">
100+
<span className="text-nowrap font-medium">Response Time: </span>
101+
<span className="block max-w-96 break-all rounded bg-gray-200 px-1 py-0.5 dark:bg-charcoal-500">
102+
{selectedRow.responseTime.toFixed(2)} ms
103+
</span>
104+
</div>
105+
)}
106+
107+
{/* Linked Event */}
108+
{selectedRow.gotoSerialNumber && (
109+
<div className="flex w-fit items-center gap-x-1">
110+
<span className="text-nowrap font-medium">Linked Event: </span>
111+
<span
112+
className="block max-w-96 cursor-pointer break-all rounded bg-gray-200 px-1 py-0.5 text-cyan-700 hover:underline dark:bg-charcoal-500 dark:text-cyan-500"
113+
onClick={() => {
114+
if (gotoRow) gotoRow(selectedRow.gotoSerialNumber!);
115+
}}
116+
>
117+
#{selectedRow.gotoSerialNumber}
118+
</span>
119+
</div>
120+
)}
95121
</div>
96122

97123
{/* Args */}

src/extension/react/components/DirectionBadge.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ export default function DirectionBadge({ direction }: Props) {
5353
labelRight: '',
5454
tooltip: 'Renderer',
5555
};
56+
case 'main':
57+
return {
58+
colorClass:
59+
'dark:bg-dark-purple dark:text-light-purple bg-purple-100 text-purple-800 border-purple-300 dark:border-light-purple',
60+
labelLeft: 'Main',
61+
labelRight: '',
62+
tooltip: 'Main',
63+
};
5664
default:
5765
return {
5866
colorClass:

src/extension/react/components/Panel.tsx

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo, useState, useEffect, useRef, useCallback } from 'react';
1+
import { useMemo, useState, useEffect, useRef, useCallback, useLayoutEffect } from 'react';
22
import { AgGridReact } from 'ag-grid-react';
33
import type {
44
ColDef,
@@ -14,11 +14,12 @@ import {
1414
themeQuartz,
1515
ScrollApiModule,
1616
TooltipModule,
17+
RowApiModule,
1718
} from 'ag-grid-community';
1819
import { Ban, Lock, LockOpen, Moon, PanelBottom, PanelRight, Sun } from 'lucide-react';
1920
import { MSG_TYPE, PORT_NAME } from '../../../common/constants';
2021
import ResizablePanel from './ResizablePanel';
21-
import type { IpcEventDataIndexed, MessagePanel } from '../../../types/shared';
22+
import type { IpcEventDataIndexed, MessagePanel, SerialNumber, UUID } from '../../../types/shared';
2223
import DirectionBadge from './DirectionBadge';
2324
import formatTimestamp from '../utils/formatTimestamp';
2425
import DetailPanel from './DetailPanel';
@@ -29,6 +30,7 @@ ModuleRegistry.registerModules([
2930
CellStyleModule,
3031
ScrollApiModule,
3132
TooltipModule,
33+
RowApiModule,
3234
]);
3335
import { useDevtronContext } from '../context/context';
3436

@@ -41,6 +43,7 @@ function Panel() {
4143
const [selectedRow, setSelectedRow] = useState<IpcEventDataIndexed | null>(null);
4244
const [showDetailPanel, setShowDetailPanel] = useState<boolean>(false);
4345
const [isPortReady, setIsPortReady] = useState<boolean>(false);
46+
const [shouldScrollToBottom, setShouldScrollToBottom] = useState<boolean>(false);
4447

4548
const {
4649
theme,
@@ -51,10 +54,47 @@ function Panel() {
5154
setLockToBottom,
5255
} = useDevtronContext();
5356

57+
/**
58+
* uuidMapRef stores the serial numbers of events that have a UUID.
59+
* If an event with the same UUID is received later on, we add a gotoSerialNumber property
60+
* to the previous event with the same UUID.
61+
* This allows us to jump between events that are related to each other.
62+
*/
63+
const uuidMapRef = useRef(new Map<UUID, SerialNumber>());
5464
const lockToBottomRef = useRef(lockToBottom);
5565
const gridRef = useRef<AgGridReact<IpcEventDataIndexed> | null>(null);
5666
const portRef = useRef<chrome.runtime.Port | null>(null);
5767

68+
// scrollToRow uses zero-based indexing
69+
const scrollToRow = useCallback(
70+
(row: number, position: 'top' | 'bottom' | 'middle' | null) =>
71+
gridRef.current?.api.ensureIndexVisible(row, position),
72+
[],
73+
);
74+
75+
// go to a row, highlight it and open the detail panel (it uses serialNumber to identify the row)
76+
const gotoRow = useCallback(
77+
(serialNumber: SerialNumber) => {
78+
if (!gridRef.current || events.length === 0) return;
79+
80+
const firstSerial = events[0].serialNumber;
81+
const index = serialNumber - firstSerial; // convert to 0-based index
82+
83+
if (index < 0 || index >= events.length) return;
84+
85+
scrollToRow(index, 'bottom');
86+
87+
const rowNode = gridRef.current.api.getRowNode(String(serialNumber));
88+
89+
if (rowNode) {
90+
rowNode.setSelected(true);
91+
setSelectedRow(rowNode.data ?? null);
92+
setShowDetailPanel(true);
93+
}
94+
},
95+
[scrollToRow, events],
96+
);
97+
5898
const clearEvents = useCallback(() => {
5999
if (isDev) {
60100
setEvents([]);
@@ -66,11 +106,22 @@ function Panel() {
66106
try {
67107
portRef.current.postMessage({ type: MSG_TYPE.CLEAR_EVENTS } satisfies MessagePanel);
68108
setEvents([]);
109+
uuidMapRef.current.clear();
69110
} catch (error) {
70111
console.error('Devtron - Error clearing events:', error);
71112
}
72113
}, [isPortReady]);
73114

115+
// used to scroll to the bottom of the grid when a new event is added
116+
useLayoutEffect(() => {
117+
if (shouldScrollToBottom && events.length > 0) {
118+
requestAnimationFrame(() => {
119+
scrollToRow(events.length - 1, 'bottom');
120+
});
121+
setShouldScrollToBottom(false);
122+
}
123+
}, [events.length, scrollToRow, shouldScrollToBottom]);
124+
74125
useEffect(() => {
75126
// Update lockToBottomRef on first render
76127
const savedLockToBottom = localStorage.getItem('lockToBottom');
@@ -99,14 +150,43 @@ function Panel() {
99150

100151
const handleOnMessage = (message: MessagePanel): void => {
101152
if (message.type === MSG_TYPE.RENDER_EVENT) {
153+
const event = message.event;
154+
102155
setEvents((prev) => {
103-
const updated = [...prev, message.event].slice(-MAX_EVENTS_TO_DISPLAY);
156+
const updated = [...prev, event].slice(-MAX_EVENTS_TO_DISPLAY);
157+
// If the event with the same UUID already exists, we update it
158+
if (event.uuid && uuidMapRef.current.has(event.uuid)) {
159+
const oldEventSerialNumber = uuidMapRef.current.get(event.uuid)!;
160+
if (
161+
oldEventSerialNumber >= updated[0].serialNumber &&
162+
oldEventSerialNumber < updated[updated.length - 1].serialNumber
163+
) {
164+
const offset = updated[0].serialNumber;
165+
166+
const oldEventIndex = oldEventSerialNumber - offset;
167+
168+
// update the old event to include a link to the new event
169+
updated[oldEventIndex] = {
170+
...updated[oldEventIndex],
171+
gotoSerialNumber: event.serialNumber,
172+
};
173+
174+
// add a link to the old event in the new event
175+
updated[updated.length - 1] = {
176+
...updated[updated.length - 1],
177+
gotoSerialNumber: oldEventSerialNumber,
178+
};
179+
}
180+
181+
uuidMapRef.current.delete(event.uuid);
182+
} else if (event.uuid) {
183+
// If a UUID is encountered for the first time, we store its serialNumber
184+
uuidMapRef.current.set(event.uuid, event.serialNumber);
185+
}
186+
104187
if (lockToBottomRef.current) {
105-
requestAnimationFrame(() => {
106-
requestAnimationFrame(() => {
107-
gridRef.current?.api.ensureIndexVisible(updated.length - 1, 'bottom');
108-
});
109-
});
188+
// If lockToBottom is true, scroll to the newly added event
189+
setShouldScrollToBottom(true);
110190
}
111191
return updated;
112192
});
@@ -130,7 +210,7 @@ function Panel() {
130210
setIsPortReady(false);
131211
if (port) port.disconnect();
132212
};
133-
}, [setLockToBottom]);
213+
}, [scrollToRow, setLockToBottom]);
134214

135215
const columnDefs: ColDef<IpcEventDataIndexed>[] = useMemo(
136216
() => [
@@ -167,8 +247,17 @@ function Panel() {
167247
flex: 1,
168248
cellClass: 'font-roboto text-[13px] !p-1 h-full flex items-center',
169249
headerClass: '!h-6',
170-
tooltipValueGetter: (params) => {
171-
return params.value; // or a custom string
250+
cellRenderer: (params: ICellRendererParams<IpcEventDataIndexed>) => {
251+
return (
252+
<div title={params.value}>
253+
{params.value}
254+
{params.data?.responseTime && (
255+
<span className="ml-2 rounded bg-gray-200 px-1.5 py-0.5 text-xs dark:bg-charcoal-400">
256+
Response
257+
</span>
258+
)}
259+
</div>
260+
);
172261
},
173262
},
174263
{
@@ -275,6 +364,7 @@ function Panel() {
275364
suppressScrollOnNewData={true}
276365
rowData={events}
277366
columnDefs={columnDefs}
367+
getRowId={(params) => String(params.data.serialNumber)}
278368
theme={themeQuartz
279369
.withParams({
280370
cellFontFamily: 'roboto, sans-serif',
@@ -308,6 +398,7 @@ function Panel() {
308398
selectedRow={selectedRow}
309399
onClose={handleCloseDetailPanel}
310400
direction={detailPanelPosition}
401+
gotoRow={gotoRow}
311402
/>
312403
</ResizablePanel>
313404
</div>

src/extension/react/test_data/test_data.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,13 @@ export const events: IpcEventDataIndexed[] = [
103103
serviceWorkerVersionId: 32,
104104
},
105105
},
106+
{
107+
serialNumber: 130,
108+
direction: 'main-to-renderer',
109+
method: 'sendSync (response)',
110+
channel: 'sendSync-check',
111+
args: ['sync response data'],
112+
timestamp: 1749114387250,
113+
responseTime: 2000.34,
114+
},
106115
];

0 commit comments

Comments
 (0)