Skip to content

feat: track response time of invoke and sendSync methods on ipcRenderer #275

Merged
samuelmaddock merged 13 commits intonextfrom
feat/track-response-time
Aug 21, 2025
Merged

feat: track response time of invoke and sendSync methods on ipcRenderer #275
samuelmaddock merged 13 commits intonextfrom
feat/track-response-time

Conversation

@hitarth-gg
Copy link
Collaborator

@hitarth-gg hitarth-gg commented Jul 31, 2025

Description of Change

  • Allows Devtron to track the time (in milliseconds) it takes for ipcRenderer.invoke and ipcRenderer.sendSync to receive responses back from the main process.
  • Patches .on, .off, .once, .addListener, .removeListener, .removeAllListeners, .handle, .handleOnce, and .removeHandler methods of ipcMain to allow Devtron to track response times and detect when handlers or listeners are removed from ipcMain.

Tracking .sendSync and .invoke response times:

Request Response
image image
  • Clicking on the Linked Event field in the Details Panel will redirect the user to the linked event.
  • Internally, the two events are linked using a UUID. Renderer process generates a UUID and includes it in the arguments of .sendSync and .invoke requests. This UUID is then received by the main process, where it is extracted from the arguments. As a result, both processes use the same UUID to track the events. Events with matching UUIDs are then linked together in the frontend.

Tracking .once and .handleOnce:

image
  • ipcMain.once is tracked as two separate events: ipcMain.removeListener and ipcMain.on.
  • ipcMain.handleOnce is tracked as two separate events: ipcMain.removeHandler and ipcMain.handle.

@hitarth-gg hitarth-gg force-pushed the feat/track-response-time branch 2 times, most recently from c8f1372 to 7cb718d Compare July 31, 2025 12:14
@hitarth-gg hitarth-gg force-pushed the feat/track-response-time branch from 7cb718d to 22a4d7c Compare August 7, 2025 08:11
@hitarth-gg
Copy link
Collaborator Author

One thing that might confuse end users is that when ipcMain.once is used, it appears as if .removeListener is called before .on is registered. This happens because .on is tracked using session.defaultSession.on("-ipc-message", ...) which logs events after the patched ipcMain.removeListener is already done logging. Curious to hear what others think about this behavior.

image

@hitarth-gg hitarth-gg marked this pull request as ready for review August 7, 2025 10:23
@hitarth-gg hitarth-gg force-pushed the feat/track-response-time branch from 22a4d7c to f47bca4 Compare August 7, 2025 14:35
const cleanedListener = async (event: Electron.IpcMainInvokeEvent, ...args: any[]) => {
const newArgs = getArgsFromPayload(args);
const result = await listener(event, ...newArgs);
return result;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be interesting to track the result as well so it can be later inspected. Not blocking for this PR.

Copy link
Collaborator Author

@hitarth-gg hitarth-gg Aug 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added it to the GSoC issue. I can probably do it in a follow up PR.

src/index.ts Outdated
Comment on lines +214 to +221
const getArgsFromPayload = (payload: any[]): any[] => {
if (payload[0] && typeof payload[0] === 'object' && payload[0].__uuid__devtron) {
// If the first argument is an object with __uuid__devtron, return its args property
return payload[0].args || [];
}
// Otherwise, return the payload as is
return payload;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it would be nice if we could extend/reuse this helper function above in trackIpcEvent

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: 18adb6d
Lmk if it still needs changes.

src/index.ts Outdated
}

function patchIpcMain() {
const listenerMap = new Map<string, Map<any, any>>(); // channel -> (originalListener -> tracked/cleaned Listener)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate the comment here!
nit: It might be nice to create named types to make this code more self documenting:

Suggested change
const listenerMap = new Map<string, Map<any, any>>(); // channel -> (originalListener -> tracked/cleaned Listener)
// top level
type Channel = string
type Listener = any
...
const listenerMap = new Map<Channel, Map<Listener, Listener>>(); // channel -> (originalListener -> tracked/cleaned Listener)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: 1f049bf

Copy link
Member

@yangannyx yangannyx Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[non-blocking] Nice, though it looks like (event: Electron.IpcMainEvent, ...args: any[]) => void, might be a better type for Listener? The best way to make these named types useful is to use them across the file, so I'd also encourage you to update the typings of channel and listener beyond just listenerMap

Copy link
Member

@yangannyx yangannyx Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[non-blocking] Similar to data, types benefit from having a single source of truth. It's a good idea to define types like this at the highest relevant level in the repo so that they can be reused across all files where the type is relevant. It might be good to declare the Channel type in src/types and use it in IpcEventData.channel. Since this file is the only (I think) that deals with listeners, it makes sense to just declare the listener type in this file for now

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried fixing it here: 7d5813e
Batched some other type-safety related changes into this commit as well.

If anything isn’t up to the mark, I’ll be happy to make the necessary changes.

Comment on lines +136 to +151
const updated = [...prev, event].slice(-MAX_EVENTS_TO_DISPLAY);
// If the event with the same UUID already exists, we update it
if (event.uuid && uuidMapRef.current.has(event.uuid)) {
const index = uuidMapRef.current.get(event.uuid)!;
if (index < prev.length) {
// update the old event to include a link to the new event
const oldEvent = updated[index];
oldEvent.gotoSerialNumber = event.serialNumber;
updated[index] = oldEvent;
// add a link to the old event in the new event
updated[updated.length - 1] = { ...event, gotoSerialNumber: index + 1 };
}
uuidMapRef.current.delete(event.uuid);
} else if (event.uuid) {
// If a UUID is encountered for the first time, we store its index
uuidMapRef.current.set(event.uuid, updated.length - 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you'll run into a couple of issues when the events array is at MAX_EVENTS_TO_DISPLAY

  • uuidMapRef indices might be stale and point to shifted events
  • new entries to uuidMapRef will all have the same index value since updated.length is now constant

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switched to using serialNumber instead of indexes in f998f34 and updated the comments I had forgotten to change in 3616cf0.

@hitarth-gg
Copy link
Collaborator Author

hitarth-gg commented Aug 8, 2025

Updated the NPM Package to 0.0.0-development.13, which includes all commits up to this point.

While testing the npm package with Electron Fiddle, I noticed that the line The service-worker for Devtron is not registered yet. Cannot track IPC event. is logged 10-20 times as the IPC events sent before Devtron's SW is initialized aren't captured. I'll likely add an option to hide logs by level in a future PR, or remove this log entirely.

@hitarth-gg hitarth-gg closed this Aug 8, 2025
@hitarth-gg hitarth-gg reopened this Aug 8, 2025
Comment on lines +37 to +38
type UUID = string;
type SerialNumber = number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[non-blocking] Move these up to the top-level shared types file to tie these types with the IPCEventData shape more explicitly

Suggested change
type UUID = string;
type SerialNumber = number;
// in src/types/shared.ts
export type UUID = string;
export type SerialNumber = number;
...
export interface IpcEventData {
....
uuid?: UUID,
}
...
export interface IpcEventDataIndexed extends IpcEventData {
serialNumber: SerialNumber;
gotoSerialNumber?: SerialNumber; // For navigating to a specific event in the grid
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, this was batched in 7d5813e.

Comment on lines +179 to +182
if (lockToBottomRef.current) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
gridRef.current?.api.ensureIndexVisible(updated.length - 1, 'bottom');
scrollToRow(updated.length - 1, 'bottom');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[non-blocking] Calling side-effects from within a React useState setter's callback is nonstandard. For requestAnimationFrame, consider using useLayoutEffect.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: 9656a9b

Copy link
Member

@yangannyx yangannyx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR LGTM from a functionality standpoint! Left some nonblocking suggestions around better type reusability, but don't want to bike shed on it

@hitarth-gg hitarth-gg force-pushed the feat/track-response-time branch from 9656a9b to c2fabdf Compare August 19, 2025 12:33
@hitarth-gg
Copy link
Collaborator Author

hitarth-gg commented Aug 19, 2025

Rebased onto the next branch and resolved merge conflicts in src/index.ts.

Note: This rebase rewrote commit hashes, so all commits show up as new. In reality, the only change since the last review is resolving merge conflicts in src/index.ts. No other files were modified.

@hitarth-gg hitarth-gg force-pushed the feat/track-response-time branch from c2fabdf to 22927f9 Compare August 19, 2025 20:08
@hitarth-gg
Copy link
Collaborator Author

hitarth-gg commented Aug 19, 2025

Rebased onto the next branch and resolved merge conflicts in src/index.ts.

Note: This rebase rewrote commit hashes, so all commits show up as new. In reality, the only change since the last review is resolving merge conflicts in src/index.ts. No other files were modified.

Rebased and fixed merge conflicts again.

@samuelmaddock samuelmaddock merged commit f0a2941 into next Aug 21, 2025
@dsanders11 dsanders11 deleted the feat/track-response-time branch August 21, 2025 20:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants