From f103924191519af75cf006cb16a3429e2d4c1f6a Mon Sep 17 00:00:00 2001 From: Justin Carlson <40642470+justcarlson@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:06:34 -0500 Subject: [PATCH 1/2] fix: parallelize setActionIcon to prevent extension slowness Changed setActionIcon() from sequential await in a for loop to Promise.all() with parallel execution. Previously, each chrome.action.setIcon() call blocked until completion before the next started - with N tabs, this took N * (single call time). Now all icon updates run in parallel. Root cause: Sequential await in for loop blocked service worker Debug session: .planning/debug/resolved/extension-slowness.md --- .../debug/resolved/extension-slowness.md | 48 +++++++++++++++++++ entrypoints/background.ts | 17 ++++--- 2 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 .planning/debug/resolved/extension-slowness.md diff --git a/.planning/debug/resolved/extension-slowness.md b/.planning/debug/resolved/extension-slowness.md new file mode 100644 index 0000000..07c5441 --- /dev/null +++ b/.planning/debug/resolved/extension-slowness.md @@ -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) diff --git a/entrypoints/background.ts b/entrypoints/background.ts index 9369a4e..cd68a07 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -91,17 +91,16 @@ async function setActionIcon(mode: UrlDetectionMode): Promise { const iconPath = mode !== "noUrls" ? "/images/icons/icon38-enabled.png" : "/images/icons/icon38-disabled.png"; - // 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 - } - } - } + }), + ), + ); } /** From 7eeb0d5a54e583490e7ede7f5b56bbeb140afff0 Mon Sep 17 00:00:00 2001 From: Justin Carlson <40642470+justcarlson@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:14:48 -0500 Subject: [PATCH 2/2] fix: reduce chrome API calls to prevent slowness with DLP extensions When DLP extensions like Incydr monitor chrome.* API calls, each call adds latency. The previous setActionIcon() queried all tabs and updated all icons on every DOMContentLoaded event - O(N) API calls for O(1) change. This fix: - Adds setTabIcon() for single-tab updates (O(1) API calls) - Renames setActionIcon() to setAllTabIcons() for mode changes only - Changes onDOMContentLoaded to update only the current tab's icon With 5 Zendesk tabs and 50ms DLP latency per call: - Before: ~300ms per navigation (6 calls) - After: ~50ms per navigation (1 call) Root cause: O(N) chrome.* API calls per event amplified by DLP monitoring Debug session: .planning/debug/resolved/extension-slowness-incydr.md --- .../resolved/extension-slowness-incydr.md | 113 ++++++++++++++++++ entrypoints/background.ts | 52 ++++++-- 2 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 .planning/debug/resolved/extension-slowness-incydr.md diff --git a/.planning/debug/resolved/extension-slowness-incydr.md b/.planning/debug/resolved/extension-slowness-incydr.md new file mode 100644 index 0000000..bd8818c --- /dev/null +++ b/.planning/debug/resolved/extension-slowness-incydr.md @@ -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 diff --git a/entrypoints/background.ts b/entrypoints/background.ts index cd68a07..19f6b42 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -82,14 +82,46 @@ 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 { + 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 { - const iconPath = - mode !== "noUrls" ? "/images/icons/icon38-enabled.png" : "/images/icons/icon38-disabled.png"; +async function setAllTabIcons(mode: UrlDetectionMode): Promise { + const iconPath = getIconPath(mode); // Query all Zendesk agent tabs and update their icons in parallel const tabs = await queryZendeskTabs(); @@ -374,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) => { @@ -386,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" }] }, ); @@ -418,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); })