Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions .planning/debug/resolved/extension-slowness-incydr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
status: resolved
trigger: "QuickTab extension becomes extremely slow when Incydr Chrome extension is running. This was the common factor across multiple browsers."
created: 2026-01-27T12:00:00Z
updated: 2026-01-27T12:20:00Z
---

## Current Focus

hypothesis: CONFIRMED - setActionIcon() causes O(N) chrome API calls for O(1) events; DLP monitoring adds latency per call making this exponentially worse
test: Design fix that reduces unnecessary API calls
expecting: Reducing API calls will reduce total latency under DLP monitoring
next_action: Implement fix - change setActionIcon to only update the CURRENT tab, not all tabs

## Symptoms

expected: Fast/instant response - actions should complete quickly
actual: All extension actions are extremely slow
errors: No errors visible
reproduction: Slowness occurs when Incydr Chrome extension is also running. Tested across multiple browsers - Incydr presence is the common factor.
started: Issue appears specifically when Incydr is active

## Prior Context

- A previous fix was applied (commit f103924) to parallelize setActionIcon() calls
- The real trigger is Incydr extension being active
- Incydr is a security/DLP extension that likely monitors network requests, intercepts chrome.* API calls, and may add latency to extension messaging

## Eliminated

## Evidence

- timestamp: 2026-01-27T12:05:00Z
checked: entrypoints/background.ts - setActionIcon() function (lines 90-104)
found: |
setActionIcon() does the following sequence:
1. await queryZendeskTabs() - calls chrome.tabs.query()
2. For each tab returned, calls chrome.action.setIcon() in parallel
This is called on EVERY onDOMContentLoaded event (line 391)
implication: If user has N Zendesk tabs open, each DOMContentLoaded triggers N+1 chrome.* API calls

- timestamp: 2026-01-27T12:06:00Z
checked: entrypoints/background.ts - onDOMContentLoaded handler (lines 378-394)
found: |
Every DOMContentLoaded for zendesk.com/agent URLs triggers:
1. trackTab() -> loadState() [storage read] + matchZendeskUrl() + saveState() [storage write]
2. chrome.action.enable()
3. getUrlDetection() [storage read]
4. setActionIcon() -> queryZendeskTabs() [tabs query] + N x setIcon calls
Total: 3 storage calls + 1 tabs.query + N setIcon + 1 action.enable = 5+N API calls PER navigation
implication: Heavy API usage per event - DLP monitoring would add latency to each call

- timestamp: 2026-01-27T12:07:00Z
checked: entrypoints/background.ts - onBeforeNavigate and onCommitted handlers
found: |
Both handlers fire for same navigation (with deduplication):
1. getUrlDetection() [storage read]
2. If match: handleNavigation() which calls:
- loadState() [storage read]
- chrome.tabs.query() for subdomain
- If existing tab: updateLotusRoute() [scripting.executeScript] + focusTab() [3 chrome.* calls] + closeTab() + saveState()
- If no existing tab: saveState()
implication: Navigation interception adds 3-8 more API calls

- timestamp: 2026-01-27T12:08:00Z
checked: Pattern analysis across codebase
found: |
KEY INSIGHT: setActionIcon() updates ALL tabs even when only ONE tab changed.
When mode changes or any DOMContentLoaded fires, it re-queries all tabs and updates all icons.
This is O(N) work for O(1) change.
implication: This is the primary suspect - unnecessary work amplified by DLP monitoring latency

## Resolution

root_cause: |
The setActionIcon() function queries ALL Zendesk tabs and updates icons on ALL of them,
even when only a single tab's state changed. This is called on every DOMContentLoaded event.

With N Zendesk tabs open:
- Each navigation triggers: 3 storage calls + 1 tabs.query + N setIcon calls + 1 action.enable
- Total: 5+N chrome.* API calls per navigation

When Incydr DLP extension is active, it monitors/intercepts chrome.* API calls, adding latency
to each call. With even modest latency (e.g., 50ms per call) and 5 tabs:
- Normal: ~instant (parallel calls)
- With DLP: (5+5) * 50ms = 500ms per navigation

This explains why the extension is slow specifically when Incydr is running.

fix: |
1. Added getIconPath() helper function to avoid code duplication
2. Created setTabIcon() for single-tab icon updates (O(1) API calls)
3. Renamed setActionIcon() to setAllTabIcons() for mode changes (O(N) API calls, used only when needed)
4. Changed onDOMContentLoaded to use setTabIcon() instead of setAllTabIcons()
5. Mode change handler continues to use setAllTabIcons() (appropriate - mode affects all tabs)

Before: Each DOMContentLoaded triggered N+1 chrome.* API calls (query + N setIcon)
After: Each DOMContentLoaded triggers 1 chrome.* API call (just setIcon for current tab)

With 5 Zendesk tabs and DLP adding 50ms latency per call:
- Before: ~300ms per navigation (6 calls * 50ms)
- After: ~50ms per navigation (1 call * 50ms)

verification: |
- All 95 unit tests pass
- Extension builds successfully
- Logic review confirms:
- onDOMContentLoaded only updates single tab icon (correct)
- Mode changes still update all tabs (correct)
- Navigation interception unchanged (doesn't call setIcon)

files_changed:
- entrypoints/background.ts
48 changes: 48 additions & 0 deletions .planning/debug/resolved/extension-slowness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
status: resolved
trigger: "Extension has become extremely slow after a recent code commit"
created: 2026-01-27T00:00:00Z
updated: 2026-01-27T00:00:00Z
---

## Current Focus

hypothesis: setActionIcon() uses sequential await in a loop (for...of + await), blocking execution while updating icons for each tab one at a time. This is called on EVERY DOMContentLoaded and mode change.
test: Look at setActionIcon implementation and where it's called
expecting: Sequential await pattern (for...of with await inside) causing slow execution
next_action: Verify sequential await pattern and check call frequency

## Symptoms

expected: Fast/instant response - actions should complete quickly
actual: All extension actions are extremely slow
errors: No errors visible
reproduction: All extension actions are slow
started: Was working well before, became slow after a recent code commit

## Eliminated

## Evidence

- timestamp: 2026-01-27T00:01:00Z
checked: Recent git commits
found: Commit 0c04754 introduced postMessage routing via chrome.scripting.executeScript
implication: This is a key change that could affect performance

- timestamp: 2026-01-27T00:02:00Z
checked: setActionIcon function at lines 90-105 in background.ts
found: Uses sequential await in for loop - for each tab, awaits chrome.action.setIcon()
implication: If N tabs open, this blocks for N serial API calls instead of N parallel calls

- timestamp: 2026-01-27T00:02:30Z
checked: Where setActionIcon is called
found: Called in onDOMContentLoaded (line 392) and setMode handler (line 424)
implication: Every Zendesk page load triggers this slow sequential operation

## Resolution

root_cause: setActionIcon() at lines 96-104 uses sequential await in a for loop. Each chrome.action.setIcon() call must complete before the next one starts. With N Zendesk tabs open, this takes N * (single setIcon time) instead of max(single setIcon time). This function is called on every DOMContentLoaded for Zendesk pages AND on every mode change from the popup.
fix: Changed setActionIcon() to use Promise.all() with map() for parallel execution of all chrome.action.setIcon() calls. Used type guard filter to avoid non-null assertions.
verification: Build succeeded (wxt build), all 95 tests pass (npm test), linter passes (npm run lint)
files_changed:
- entrypoints/background.ts (lines 90-105)
69 changes: 52 additions & 17 deletions entrypoints/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,26 +82,57 @@ function isDuplicateNavigation(tabId: number, url: string): boolean {
}

/**
* Update action icon for all Zendesk agent tabs based on detection mode.
* Get the icon path for a given detection mode.
*
* @param mode - Current URL detection mode
* @returns Path to the appropriate icon
*/
function getIconPath(mode: UrlDetectionMode): string {
return mode !== "noUrls"
? "/images/icons/icon38-enabled.png"
: "/images/icons/icon38-disabled.png";
}

/**
* Update action icon for a SINGLE tab based on detection mode.
* Use this for individual tab updates (e.g., onDOMContentLoaded).
*
* This is optimized to minimize chrome.* API calls, which is critical
* when DLP extensions (like Incydr) are monitoring API calls and adding latency.
*
* @param tabId - Tab ID to update
* @param mode - Current URL detection mode
*/
async function setTabIcon(tabId: number, mode: UrlDetectionMode): Promise<void> {
const iconPath = getIconPath(mode);
try {
await chrome.action.setIcon({ tabId, path: iconPath });
} catch {
// Tab may have closed, ignore
}
}

/**
* Update action icon for ALL Zendesk agent tabs based on detection mode.
* Use this only when the mode changes (requires updating all tabs).
*
* Per existing behavior: enabled icon if mode !== 'noUrls', disabled otherwise.
*
* @param mode - Current URL detection mode
*/
async function setActionIcon(mode: UrlDetectionMode): Promise<void> {
const iconPath =
mode !== "noUrls" ? "/images/icons/icon38-enabled.png" : "/images/icons/icon38-disabled.png";
async function setAllTabIcons(mode: UrlDetectionMode): Promise<void> {
const iconPath = getIconPath(mode);

// Query all Zendesk agent tabs and update their icons
// Query all Zendesk agent tabs and update their icons in parallel
const tabs = await queryZendeskTabs();
for (const tab of tabs) {
if (tab.id) {
try {
await chrome.action.setIcon({ tabId: tab.id, path: iconPath });
} catch {
const tabIds = tabs.map((tab) => tab.id).filter((id): id is number => id !== undefined);
await Promise.all(
tabIds.map((tabId) =>
chrome.action.setIcon({ tabId, path: iconPath }).catch(() => {
// Tab may have closed, ignore
}
}
}
}),
),
);
}

/**
Expand Down Expand Up @@ -375,6 +406,10 @@ export default defineBackground(() => {
/**
* Track Zendesk agent tabs when page loads
* Shows action icon and updates tab tracking state
*
* OPTIMIZATION: Only updates the SINGLE tab's icon instead of all tabs.
* This is critical for performance when DLP extensions (like Incydr) are
* monitoring chrome.* API calls - reduces O(N) to O(1) API calls per navigation.
*/
chrome.webNavigation.onDOMContentLoaded.addListener(
async (details) => {
Expand All @@ -387,9 +422,9 @@ export default defineBackground(() => {
// Enable action icon for this tab
chrome.action.enable(details.tabId);

// Set icon state based on detection mode
// Set icon state for THIS tab only (not all tabs)
const mode = await getUrlDetection();
await setActionIcon(mode);
await setTabIcon(details.tabId, mode);
},
{ url: [{ urlContains: "zendesk.com/agent" }] },
);
Expand Down Expand Up @@ -419,9 +454,9 @@ export default defineBackground(() => {
}

if (message.type === "setMode") {
// Save new mode and update icons
// Save new mode and update ALL tab icons (mode change affects all tabs)
setUrlDetection(message.mode)
.then(() => setActionIcon(message.mode))
.then(() => setAllTabIcons(message.mode))
.then(() => {
sendResponse(true);
})
Expand Down
Loading