Skip to content

perf: short-circuit no-op session refreshes via fingerprint match - #187

Merged
matt1398 merged 1 commit into
mainfrom
perf/session-refresh-fingerprint-shortcircuit
Apr 29, 2026
Merged

matt1398 merged 1 commit into
mainfrom
perf/session-refresh-fingerprint-shortcircuit

Conversation

@matt1398

@matt1398 matt1398 commented Apr 29, 2026 •

Copy link
Copy Markdown
Owner

Fixes #186. Long sessions ran at ~10fps after v0.4.13 because PR #183 shrank the refresh debounce ceiling from 60s to 5s. The shorter window exposed an existing architectural flaw: every getSessionDetail IPC returned the full payload, with the renderer-side fingerprint check happening after the expensive serialization. Frequent refreshes paid near-real-refresh cost even when nothing on disk had changed.

Move the fingerprint check to the IPC boundary. The renderer caches the opaque file-state fingerprint (mtimeMs+size) returned by main and passes it back on the next call. When it matches, the handler short-circuits to a tiny { unchanged: true } sentinel before cache lookup or parsing. The renderer detects the sentinel and exits without transforming or re-rendering — bounded to one stat() + a sub-100B IPC payload.

With no-op cost effectively eliminated, the adaptive debounce can be restored to sane values (1s ceiling for >500 groups, 500ms for >200, 300ms for >100), preserving the streaming-responsiveness intent of PR #183 without the perf cost.

Backwards compatible: knownFingerprint is optional, so initial loads and export still receive a full SessionDetail.

Summary by CodeRabbit

  • Performance Improvements

    • Session refresh now intelligently detects unchanged file state and skips redundant updates, reducing processing overhead
    • Adaptive refresh timing optimized for sessions with large datasets, increasing update frequency while maintaining smooth performance
  • Bug Fixes

    • Enhanced robustness in handling concurrent session refresh operations

Fixes #186. Long sessions ran at ~10fps after v0.4.13 because PR #183
shrank the refresh debounce ceiling from 60s to 5s. The shorter window
exposed an existing architectural flaw: every `getSessionDetail` IPC
returned the full payload, with the renderer-side fingerprint check
happening *after* the expensive serialization. Frequent refreshes paid
near-real-refresh cost even when nothing on disk had changed.

Move the fingerprint check to the IPC boundary. The renderer caches the
opaque file-state fingerprint (mtimeMs+size) returned by main and passes
it back on the next call. When it matches, the handler short-circuits
to a tiny `{ unchanged: true }` sentinel before cache lookup or parsing.
The renderer detects the sentinel and exits without transforming or
re-rendering — bounded to one stat() + a sub-100B IPC payload.

With no-op cost effectively eliminated, the adaptive debounce can be
restored to sane values (1s ceiling for >500 groups, 500ms for >200,
300ms for >100), preserving the streaming-responsiveness intent of
PR #183 without the perf cost.

Backwards compatible: knownFingerprint is optional, so initial loads
and export still receive a full SessionDetail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 29, 2026 04:06
@coderabbitai coderabbitai Bot added the bug Something isn't working label Apr 29, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements a fingerprint-based short-circuiting mechanism for session detail fetching to optimize performance by skipping IPC serialization and re-renders when the session file is unchanged. Key changes include the introduction of a SessionDetailResponse union type, fingerprint tracking in the renderer store, and updates to the IPC handler. Feedback highlights an opportunity to use the new isSessionDetailUnchanged type guard for better consistency in the main process and identifies a potential memory leak where module-level fingerprint maps are never cleared.

Comment thread src/main/ipc/sessions.ts
// below is purely for type safety.
const detail = await handleGetSessionDetail(_event, projectId, sessionId);
if (!detail) {
if (!detail || 'unchanged' in detail) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Consider using the newly introduced isSessionDetailUnchanged type guard here for consistency and better type safety, rather than checking for the 'unchanged' property manually. Note that you will need to import it from @shared/utils/sessionDetailResponse.

* on every no-op refresh. Distinct from `sessionChunkFingerprint`, which is
* a renderer-local content fingerprint used as a second-line guard.
*/
const sessionFileFingerprint = new Map<string, string>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The sessionFileFingerprint map (along with sessionChunkFingerprint, sessionRefreshGeneration, etc.) is a module-level constant that is never cleared. In a long-running application session where a user opens and closes many different projects and sessions, these maps will grow indefinitely, leading to a memory leak. Consider cleaning up these entries in cleanupTabSessionData when no other tabs are viewing the associated session.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses the post-v0.4.13 performance regression by moving the “unchanged session” fingerprint check to the main-process IPC boundary, allowing getSessionDetail to return a tiny sentinel response when the underlying session file hasn’t changed—avoiding expensive serialization + renderer transforms on no-op refreshes.

Changes:

  • Add a knownFingerprint param to getSessionDetail and return { unchanged: true, fingerprint } when the file-state fingerprint matches.
  • Cache and round-trip the file fingerprint in the renderer to enable the short-circuit fast path; restore more aggressive adaptive refresh debounce ceilings.
  • Introduce shared IPC-contract helpers + tests to keep the sentinel discriminator consistent.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/shared/utils/sessionDetailResponse.test.ts Adds IPC contract tests for identifying the “unchanged” sentinel response.
src/shared/utils/sessionDetailResponse.ts Adds shared type-guard helper for the getSessionDetail response union.
src/shared/types/api.ts Updates ElectronAPI.getSessionDetail signature + return type to the new discriminated union.
src/renderer/store/slices/sessionDetailSlice.ts Caches file fingerprints and short-circuits refresh work when main returns the sentinel.
src/renderer/store/index.ts Restores tighter adaptive debounce ceilings now that no-op refreshes are cheap.
src/renderer/components/layout/MoreMenu.tsx Updates export flow to handle SessionDetailResponse vs sentinel.
src/preload/index.ts Plumbs optional knownFingerprint through the preload IPC bridge.
src/main/types/chunks.ts Adds fingerprint to SessionDetail and introduces SessionDetailUnchanged/SessionDetailResponse.
src/main/ipc/sessions.ts Implements the IPC short-circuit based on file-state fingerprint match and includes fingerprint in full responses.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +24 to +29
return (
!!response &&
typeof response === 'object' &&
'unchanged' in response &&
(response as { unchanged: unknown }).unchanged === true
);

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

isSessionDetailUnchanged currently only checks for the literal unchanged: true flag. Since the type guard is used to narrow callers to SessionDetailUnchanged (and the docs/tests imply fingerprint is guaranteed), it should also validate that fingerprint exists and is a string; otherwise callers may get an unsound narrowing if a malformed IPC payload ever omits/changes fingerprint.

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +88
// No knownFingerprint passed, so the response is always SessionDetail | null
// at runtime. Narrow defensively for type safety.
void api
.getSessionDetail(projectId, sessionId)
.then((detail) => {
if (detail) triggerDownload(detail, format);
.then((response) => {
if (response && !('unchanged' in response)) {
triggerDownload(response, format);
}

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

This export path is manually checking !('unchanged' in response) instead of using the shared isSessionDetailUnchanged helper. Since the whole point of the helper/test is to centralize the IPC discriminator, this duplicates the contract in a second place and can drift if the sentinel shape ever changes. Consider using isSessionDetailUnchanged(response) here as well (and keep the existing null guard).

Copilot uses AI. Check for mistakes.
@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements session-detail fingerprinting to optimize refresh behavior. It adds file-state change detection via optional fingerprint fields, introduces an IPC sentinel response for unchanged sessions, and enables renderer-side short-circuiting to skip unnecessary parsing and state updates when session files remain unchanged.

Changes

Cohort / File(s) Summary
Type Definitions
src/main/types/chunks.ts, src/shared/types/api.ts
Added fingerprint field to SessionDetail, introduced SessionDetailUnchanged sentinel type, and created SessionDetailResponse union type for IPC contract.
IPC Handler & Preload
src/main/ipc/sessions.ts, src/preload/index.ts
Updated get-session-detail handler to accept optional knownFingerprint parameter and short-circuit on match; extended preload API signature to pass fingerprint through.
Utility Functions
src/shared/utils/sessionDetailResponse.ts, test/shared/utils/sessionDetailResponse.test.ts
Added type-guard utility isSessionDetailUnchanged for discriminating between full detail and unchanged sentinel responses, with comprehensive test coverage.
Renderer Store
src/renderer/store/slices/sessionDetailSlice.ts, src/renderer/store/index.ts
Enhanced session refresh logic to cache sessionFileFingerprint, supply it to IPC calls, and exit early on unchanged responses; adjusted debounce thresholds for more frequent updates on large sessions.
Renderer UI
src/renderer/components/layout/MoreMenu.tsx
Added defensive narrowing in export flow to prevent triggering download when response is unchanged sentinel or null.

Possibly related PRs

  • #89: Modifies renderer session-refresh flow and listeners that interact with the updated refresh logic.
  • #120: Modifies the session-detail IPC contract and fingerprint handling in both handler and renderer.
  • #183: Introduces file-state fingerprint and cache foundations that this PR builds upon.

Suggested labels

bug

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed All changes directly address #186 by implementing fingerprint-based short-circuiting to eliminate expensive no-op session refreshes, restoring UI responsiveness.
Out of Scope Changes check ✅ Passed All changes are narrowly scoped to fingerprint-based optimization: IPC contracts, fingerprint caching, response handling, debounce tuning, and supporting tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/renderer/components/layout/MoreMenu.tsx (1)

81-88: Consider using the shared type guard for consistency.

The inline check !('unchanged' in response) works correctly, but other places in this PR (e.g., sessionDetailSlice.ts) use the shared isSessionDetailUnchanged() type guard. Using the shared utility would be slightly more consistent:

+import { isSessionDetailUnchanged } from '@shared/utils/sessionDetailResponse';
 // ...
         .then((response) => {
-          if (response && !('unchanged' in response)) {
+          if (response && !isSessionDetailUnchanged(response)) {
             triggerDownload(response, format);
           }
         })

That said, the current approach is valid defense-in-depth since no knownFingerprint is passed, so the sentinel cannot be returned at runtime. The comment accurately explains this.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/layout/MoreMenu.tsx` around lines 81 - 88, Replace
the inline sentinel check with the shared type guard for consistency: in the
promise handler for api.getSessionDetail(projectId, sessionId) (where response
is checked before calling triggerDownload), use the
isSessionDetailUnchanged(response) guard instead of "!('unchanged' in response)"
to determine if response is a SessionDetail; keep the existing null/undefined
defensive check and ensure you import or reference isSessionDetailUnchanged from
the same module used elsewhere (e.g., sessionDetailSlice.ts).
src/main/ipc/sessions.ts (1)

389-393: Prefer a named type guard for the unchanged sentinel.

'unchanged' in detail is inline runtime type checking. Per repo rules, this should be an isXxx guard so the narrowing stays explicit and reusable.

Minimal refactor
+function isUnchangedSessionDetailResponse(
+  detail: SessionDetailResponse | null
+): detail is Extract<SessionDetailResponse, { unchanged: true }> {
+  return detail !== null && typeof detail === 'object' && 'unchanged' in detail;
+}
+
 async function handleGetWaterfallData(
   _event: IpcMainInvokeEvent,
   projectId: string,
   sessionId: string
 ): Promise<WaterfallData | null> {
   try {
@@
-    if (!detail || 'unchanged' in detail) {
+    if (!detail || isUnchangedSessionDetailResponse(detail)) {
       return null;
     }
As per coding guidelines, "Use TypeScript type guards with isXxx naming convention for runtime type checking".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/sessions.ts` around lines 389 - 393, The inline runtime check
"'unchanged' in detail" should be replaced with a named TypeScript type guard to
follow repo conventions; add an exported guard function (e.g.
isSessionDetailUnchanged or isUnchangedDetail) that accepts the
handleGetSessionDetail return type and returns boolean by checking the
'unchanged' sentinel, then use that guard in the conditional (replace
"'unchanged' in detail" with isSessionDetailUnchanged(detail)) so the narrowing
is explicit and reusable across code that inspects the detail variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/ipc/sessions.ts`:
- Around line 389-393: The inline runtime check "'unchanged' in detail" should
be replaced with a named TypeScript type guard to follow repo conventions; add
an exported guard function (e.g. isSessionDetailUnchanged or isUnchangedDetail)
that accepts the handleGetSessionDetail return type and returns boolean by
checking the 'unchanged' sentinel, then use that guard in the conditional
(replace "'unchanged' in detail" with isSessionDetailUnchanged(detail)) so the
narrowing is explicit and reusable across code that inspects the detail
variable.

In `@src/renderer/components/layout/MoreMenu.tsx`:
- Around line 81-88: Replace the inline sentinel check with the shared type
guard for consistency: in the promise handler for
api.getSessionDetail(projectId, sessionId) (where response is checked before
calling triggerDownload), use the isSessionDetailUnchanged(response) guard
instead of "!('unchanged' in response)" to determine if response is a
SessionDetail; keep the existing null/undefined defensive check and ensure you
import or reference isSessionDetailUnchanged from the same module used elsewhere
(e.g., sessionDetailSlice.ts).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d5f07907-65fc-4510-925a-8ae7c04dcf89

📥 Commits

Reviewing files that changed from the base of the PR and between 8540dbe and df300f2.

📒 Files selected for processing (9)
  • src/main/ipc/sessions.ts
  • src/main/types/chunks.ts
  • src/preload/index.ts
  • src/renderer/components/layout/MoreMenu.tsx
  • src/renderer/store/index.ts
  • src/renderer/store/slices/sessionDetailSlice.ts
  • src/shared/types/api.ts
  • src/shared/utils/sessionDetailResponse.ts
  • test/shared/utils/sessionDetailResponse.test.ts

@matt1398
matt1398 merged commit 6535fbb into main Apr 29, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Performance degradation

2 participants