Search code examples
google-chrome-extensionservice-workerchrome-extension-manifest-v3

Debugging and performance profiling ManifestV3 extension Service worker


I'm learning how to build chrome extensions with manifest v3, what I'm trying to do is the following

In my extension background.js (service worker) I want to do this:

  • connect to WebSocket to get data updates
  • reconnect to the Websocket when service-worker wake up
  • those are tasks to get data updates from a WebSocket and update the badge text and send notifications.

I need to do these tasks while not relying on having a port open with the popup or a content script.

I'm using Chrome Alarms to wake up the service worker

it may sound weird that I have to reconnect every time the service worker wakes up considering chrome is shutting the service worker down like every 15s or less once I close the extensions dev tools (which makes me cry blood) but it is better than sending XHR periodically using Chrome alarms, which would result in a lot more requests being sent to an API, so reconnecting to the Websocket seems less problematic.

I'm having a super hard time debugging my service worker (background script) in my chrome extension. The problem is when I have dev tools open the service worker will NEVER go inactive, and what I'm trying to do is identify when the SW wakes up to perform tasks, super-duper weird because I need dev tools open to debugging...

how do you debug an extension SW without devtools open?

do you/anyone reading this have any recommendations/thoughts on what I want to do with this extension and the pain process for debugging the SW?

here is the code I have for the background.js


const extension = {
  count: 0,
  disconnected: false,
  port: {} as any,
  ws: null,
};

const fetchData = () => {
  return fetch(
    'https://api.coingecko.com/api/v3/coins/ethereum?localization=incididuntelit&tickers=false&market_data=true&community_data=true&developer_data=true&sparkline=true'
  ).then((res) => res.json());
};

// Chrome Alarms
const setupAlarms = () => {
  console.log('###ALARMS-SETUP');
  chrome.alarms.get('data-fetch', (alarm) => {
    if (!alarm) {
      chrome.alarms.create('data-fetch', { periodInMinutes: 0.1 });
    }
  });
  chrome.alarms.get('client-pinging-server', (alarm) => {
    if (!alarm) {
      chrome.alarms.create('client-pinging-server', { periodInMinutes: 0.1 });
    }
  });

  chrome.alarms.onAlarm.addListener((e) => {
    if (e.name === 'data-fetch') {
      fetchData()
        .then((res) => {
          // console.log('###ETHEREUM:', res.market_data.current_price.cad);
          chrome.action.setBadgeText({ text: `${Math.round(Math.random() * 100)}` });
        })
        .catch((error) => console.error('###ERROR', error));
    }

    if (e.name === 'client-pinging-server') {
      if (!extension.ws || !extension.ws.getInstance()) {
        console.log('\n');
        console.log('###reconnecting...', extension.ws);
        console.log('\n');
        extension.ws = WebSocketClient();
        extension.ws.connect();
      }
      if (extension.ws.getInstance()) {
        console.log('###sending-client-ping');
        extension.ws.getInstance().send(JSON.stringify({ message: 'client ping - keep alive' }));
      }
    }
  });
};

// ON INSTALL
chrome.runtime.onInstalled.addListener(async (e) => {
  const API_URL = 'ws://localhost:8080';
  chrome.storage.local.set({ apiUrl: API_URL, count: 0 });
  console.info('###Extension installed', e);

  chrome.action.setBadgeText({ text: '0' });
  chrome.action.setBadgeBackgroundColor({ color: '#FF9900' });
});

// ON SUSPEND
chrome.runtime.onSuspend.addListener(() => {
  console.log('Unloading.');
  chrome.action.setBadgeText({ text: `off` });
});

function WebSocketClient() {
  let instance = null;
  const connect = () => {
    return new Promise((resolve, reject) => {
      const ws = new WebSocket('ws://localhost:8080');

      const onOpen = () => {
        instance = ws;
        console.log('###websocket:connected', instance);
        return resolve(ws);
      };

      const onError = (event) => {
        console.log('###INIT-FAILED', event);
        ws.close(1000, 'closing due to unknown error');
        return reject('failed to connect to websocket');
      };

      const onClose = () => {
        console.log('###websocket:disconnected');
        instance = null;
        // reconnect is happening in the alarm callback
      };

      ws.onopen = onOpen;
      ws.onerror = onError;
      ws.onclose = onClose;
    });
  };

  const getInstance = () => {
    return instance;
  };

  return {
    connect,
    getInstance,
  };
}

self.addEventListener('install', async (event) => {
  console.log('====install', event);
  chrome.action.setBadgeBackgroundColor({ color: '#a6e22e' });
});

self.addEventListener('activate', async (event) => {
  console.log('====activate', event);
  chrome.action.setBadgeBackgroundColor({ color: '#FF9900' });
  extension.ws = WebSocketClient();
  extension.ws.connect();
  setupAlarms();
});

self.addEventListener('push', function (event) {
  // Keep the service worker alive until the notification is created.
  event.waitUntil(
    self.registration.showNotification('Testing PUSH API', {
      body: 'coming from push event',
    })
  );
});

Solution

  • Since Devtools can attach to multiple contexts at once, you can open it for another context so the SW will be secondary and thus will be able to unload normally.

    Debugging

    1. Open any visible page of the extension or, if there are none, its manifest.json URL:
      chrome-extension://ID/manifest.json where ID is the extension's id
    2. Open devtools and switch to its Application tab, then choose Service worker on the left.
    3. Click start (if shown) to start the service worker, click the background script name to open it in the Sources panel, set breakpoints, etc.
    4. Click stop to stop the service worker, optionally click Update at the top, and skip waiting in the middle (if shown) to force an update.
    5. Click start again - your breakpoints will trigger.

    Performance profiling

    1. Open any visible page of the extension or, if there are none, its manifest.json URL:
      chrome-extension://ID/manifest.json where ID is the extension's id
    2. Open devtools and switch to its Application tab, then choose Service worker on the left.
    3. Duplicate the tab, open devtools there, go to Performance tab, click "Start" or press Ctrl-E
    4. Switch back to the first tab and click the start button (or stop first, then start). In certain cases you may also see skip waiting in the middle, click it then.
    5. Switch to the second tab, wait for a while and click the recording button again or press Ctrl-E.

    Notes

    When the service worker is started you can see its context in the Sources panel on the left (in the files panel), on the top-right (in the threads panel), in the console toolbar (the context selector).

    This may seem unwieldy at first, but once you try and get the knack of it, it's quite trivial and can even beat devtools that's shown when clicking the "service worker" link in chrome://extensions page because this one a) shows extension's localStorage/IndexedDB in devtools, b) provides control over service worker lifetime/execution, c) supports performance profiling of SW startup.

    Note that the ManifestV3 documentation's claims about benefits provided by service workers for extensions are largely exaggerated or completely false, e.g. in your case it's clear that restarting the service worker is bad, so you should use a port to prolong the SW's lifetime as much as possible.