Skip to content

Commit

Permalink
adding debugging+performance helpers (#1247)
Browse files Browse the repository at this point in the history
* adding debugging+performance helpers

* linting

---------

Co-authored-by: Shane Osbourne <sosbourne@duckduckgo.com>
  • Loading branch information
shakyShane and Shane Osbourne authored Nov 21, 2024
1 parent 3e17e6f commit 22e3eb0
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { h } from 'preact';
import { useContext } from 'preact/hooks';

import { useTypedTranslation } from '../../types.js';
import { useTelemetry, useTypedTranslation } from '../../types.js';
import { useVisibility } from '../../widget-list/widget-config.provider.js';
import { useCustomizer } from '../../customizer/Customizer.js';

Expand All @@ -14,8 +14,10 @@ import { FavoritesMemo } from './Favorites.js';
*/
export function FavoritesConsumer() {
const { state, toggle, favoritesDidReOrder, openContextMenu, openFavorite, add } = useContext(FavoritesContext);
const telemetry = useTelemetry();

if (state.status === 'ready') {
telemetry.measureFromPageLoad('favorites-will-render', 'time to favorites');
return (
<PragmaticDND items={state.data.favorites} itemsDidReOrder={favoritesDidReOrder}>
<FavoritesMemo
Expand Down
31 changes: 17 additions & 14 deletions special-pages/pages/new-tab/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EnvironmentProvider, UpdateEnvironment } from '../../../shared/componen
import { Fallback } from '../../../shared/components/Fallback/Fallback.jsx';
import { ErrorBoundary } from '../../../shared/components/ErrorBoundary.js';
import { SettingsProvider } from './settings.provider.js';
import { InitialSetupContext, MessagingContext } from './types';
import { InitialSetupContext, MessagingContext, TelemetryContext } from './types';
import { TranslationProvider } from '../../../shared/components/TranslationsProvider.js';
import { WidgetConfigService } from './widget-list/widget-config.service.js';
import enStrings from '../src/locales/en/newtab.json';
Expand All @@ -15,9 +15,10 @@ import { widgetEntryPoint } from './widget-list/WidgetList.js';

/**
* @param {import("../src/js").NewTabPage} messaging
* @param {import("./telemetry/telemetry.js").Telemetry} telemetry
* @param {import("../../../shared/environment").Environment} baseEnvironment
*/
export async function init(messaging, baseEnvironment) {
export async function init(messaging, telemetry, baseEnvironment) {
const init = await messaging.init();

if (!Array.isArray(init.widgets)) {
Expand Down Expand Up @@ -107,18 +108,20 @@ export async function init(messaging, baseEnvironment) {
<UpdateEnvironment search={window.location.search} />
<MessagingContext.Provider value={messaging}>
<InitialSetupContext.Provider value={init}>
<SettingsProvider settings={settings}>
<TranslationProvider translationObject={strings} fallback={strings} textLength={environment.textLength}>
<WidgetConfigProvider
api={widgetConfigAPI}
widgetConfigs={init.widgetConfigs}
widgets={init.widgets}
entryPoints={entryPoints}
>
<App />
</WidgetConfigProvider>
</TranslationProvider>
</SettingsProvider>
<TelemetryContext.Provider value={telemetry}>
<SettingsProvider settings={settings}>
<TranslationProvider translationObject={strings} fallback={strings} textLength={environment.textLength}>
<WidgetConfigProvider
api={widgetConfigAPI}
widgetConfigs={init.widgetConfigs}
widgets={init.widgets}
entryPoints={entryPoints}
>
<App />
</WidgetConfigProvider>
</TranslationProvider>
</SettingsProvider>
</TelemetryContext.Provider>
</InitialSetupContext.Provider>
</MessagingContext.Provider>
</ErrorBoundary>
Expand Down
64 changes: 64 additions & 0 deletions special-pages/pages/new-tab/app/telemetry/Debug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { h } from 'preact';
import { useEffect, useRef, useState } from 'preact/hooks';
import { useTelemetry } from '../types.js';
import { useCustomizer } from '../customizer/Customizer.js';
import { Telemetry } from './telemetry.js';

export function DebugCustomized({ index }) {
const [isOpen, setOpen] = useState(false);
const telemetry = useTelemetry();
useCustomizer({
title: '🐞 Debug',
id: 'debug',
icon: 'shield',
visibility: isOpen ? 'visible' : 'hidden',

toggle: (_id) => setOpen((prev) => !prev),
index,
});
return (
<div>
<Debug telemetry={telemetry} isOpen={isOpen} />
</div>
);
}

/**
* @param {object} props
* @param {import("./telemetry.js").Telemetry} props.telemetry
* @param {boolean} props.isOpen
*/
export function Debug({ telemetry, isOpen }) {
/** @type {import("preact").Ref<HTMLTextAreaElement>} */
const textRef = useRef(null);
useEvents(textRef, telemetry);
return (
<div hidden={!isOpen}>
<textarea style={{ width: '100%' }} rows={20} ref={textRef}></textarea>
</div>
);
}

/**
* @param {import("preact").RefObject<HTMLTextAreaElement>} ref
* @param {import("./telemetry.js").Telemetry} telemetry
*/
function useEvents(ref, telemetry) {
useEffect(() => {
if (!ref.current) return;
const elem = ref.current;
function handle(/** @type {CustomEvent<any>} */ { detail }) {
elem.value += JSON.stringify(detail, null, 2) + '\n\n';
}
for (const beforeElement of telemetry.eventStore) {
elem.value += JSON.stringify(beforeElement, null, 2) + '\n\n';
}
telemetry.eventStore = [];
telemetry.storeEnabled = false;
telemetry.eventTarget.addEventListener(Telemetry.EVENT_BROADCAST, handle);
return () => {
telemetry.eventTarget.removeEventListener(Telemetry.EVENT_BROADCAST, handle);
telemetry.storeEnabled = true;
};
}, [ref, telemetry]);
}
179 changes: 179 additions & 0 deletions special-pages/pages/new-tab/app/telemetry/telemetry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* @import { Messaging } from "@duckduckgo/messaging"
*/
export class Telemetry {
static EVENT_REQUEST = 'TELEMETRY_EVENT_REQUEST';
static EVENT_RESPONSE = 'TELEMETRY_EVENT_RESPONSE';
static EVENT_SUBSCRIPTION = 'TELEMETRY_EVENT_SUBSCRIPTION';
static EVENT_SUBSCRIPTION_DATA = 'TELEMETRY_EVENT_SUBSCRIPTION_DATA';
static EVENT_NOTIFICATION = 'TELEMETRY_EVENT_NOTIFICATION';
static EVENT_BROADCAST = 'TELEMETRY_*';

eventTarget = new EventTarget();
/** @type {any[]} */
eventStore = [];
storeEnabled = true;

/**
* @param now
*/
constructor(now = Date.now()) {
this.now = now;
performance.mark('ddg-telemetry-init');
this._setupMessagingMarkers();
}

_setupMessagingMarkers() {
this.eventTarget.addEventListener(Telemetry.EVENT_REQUEST, (/** @type {CustomEvent<any>} */ { detail }) => {
const named = `ddg request ${detail.method} ${detail.timestamp}`;
performance.mark(named);
this.broadcast(detail);
});
this.eventTarget.addEventListener(Telemetry.EVENT_RESPONSE, (/** @type {CustomEvent<any>} */ { detail }) => {
const reqNamed = `ddg request ${detail.method} ${detail.timestamp}`;
const resNamed = `ddg response ${detail.method} ${detail.timestamp}`;
performance.mark(resNamed);
performance.measure(reqNamed, reqNamed, resNamed);
this.broadcast(detail);
});
this.eventTarget.addEventListener(Telemetry.EVENT_SUBSCRIPTION, (/** @type {CustomEvent<any>} */ { detail }) => {
const named = `ddg subscription ${detail.method} ${detail.timestamp}`;
performance.mark(named);
this.broadcast(detail);
});
this.eventTarget.addEventListener(Telemetry.EVENT_SUBSCRIPTION_DATA, (/** @type {CustomEvent<any>} */ { detail }) => {
const named = `ddg subscription data ${detail.method} ${detail.timestamp}`;
performance.mark(named);
this.broadcast(detail);
});
this.eventTarget.addEventListener(Telemetry.EVENT_NOTIFICATION, (/** @type {CustomEvent<any>} */ { detail }) => {
const named = `ddg notification ${detail.method} ${detail.timestamp}`;
performance.mark(named);
this.broadcast(detail);
});
}

broadcast(payload) {
if (this.eventStore.length >= 50) {
this.eventStore = [];
}
if (this.storeEnabled) {
this.eventStore.push(structuredClone(payload));
}
this.eventTarget.dispatchEvent(new CustomEvent(Telemetry.EVENT_BROADCAST, { detail: payload }));
}

measureFromPageLoad(marker, measure = 'measure__' + Date.now()) {
if (!performance.getEntriesByName(marker).length) {
performance.mark(marker);
performance.measure(measure, 'ddg-telemetry-init', marker);
}
}
}

/**
* @implements Messaging
*/
class MessagingObserver {
/** @type {Map<string, number>} */
observed = new Map();

/**
* @param {import("@duckduckgo/messaging").Messaging} messaging
* @param {EventTarget} eventTarget
*/
constructor(messaging, eventTarget) {
this.messaging = messaging;
this.messagingContext = messaging.messagingContext;
this.transport = messaging.transport;
this.eventTarget = eventTarget;
}

/**
* @param {string} method
* @param {Record<string, any>} params
*/
request(method, params) {
const timestamp = Date.now();
const json = {
kind: 'request',
method,
params,
timestamp,
};
this.record(Telemetry.EVENT_REQUEST, json);
return (
this.messaging
.request(method, params)
// eslint-disable-next-line promise/prefer-await-to-then
.then((x) => {
const resJson = {
kind: 'response',
method,
result: x,
timestamp,
};
this.record(Telemetry.EVENT_RESPONSE, resJson);
return x;
})
);
}

/**
* @param {string} method
* @param {Record<string, any>} params
*/
notify(method, params) {
const json = {
kind: 'notification',
method,
params,
};
this.record(Telemetry.EVENT_NOTIFICATION, json);
return this.messaging.notify(method, params);
}

/**
* @param method
* @param callback
* @return {function(): void}
*/
subscribe(method, callback) {
const timestamp = Date.now();
const json = {
kind: 'subscription',
method,
timestamp,
};

this.record(Telemetry.EVENT_SUBSCRIPTION, json);
return this.messaging.subscribe(method, (params) => {
const json = {
kind: 'subscription data',
method,
timestamp,
params,
};
this.record(Telemetry.EVENT_SUBSCRIPTION_DATA, json);
callback(params);
});
}

/**
* @param {string} name
* @param {Record<string, any>} detail
*/
record(name, detail) {
this.eventTarget.dispatchEvent(new CustomEvent(name, { detail }));
}
}

/**
* @param {Messaging} messaging
* @return {{telemetry: Telemetry, messaging: MessagingObserver}}
*/
export function install(messaging) {
const telemetry = new Telemetry();
const observedMessaging = new MessagingObserver(messaging, telemetry.eventTarget);
return { telemetry, messaging: observedMessaging };
}
6 changes: 6 additions & 0 deletions special-pages/pages/new-tab/app/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export function useTypedTranslation() {

export const MessagingContext = createContext(/** @type {import("../src/js/index.js").NewTabPage} */ ({}));
export const useMessaging = () => useContext(MessagingContext);
export const TelemetryContext = createContext(
/** @type {import("./telemetry/telemetry.js").Telemetry} */ ({
measureFromPageLoad: () => {},
}),
);
export const useTelemetry = () => useContext(TelemetryContext);

export const InitialSetupContext = createContext(/** @type {InitialSetupResponse} */ ({}));
export const useInitialSetupData = () => useContext(InitialSetupContext);
4 changes: 4 additions & 0 deletions special-pages/pages/new-tab/app/widget-list/WidgetList.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { WidgetConfigContext, WidgetVisibilityProvider } from './widget-config.p
import { useContext } from 'preact/hooks';
import { Stack } from '../../../onboarding/app/components/Stack.js';
import { Customizer, CustomizerMenuPositionedFixed } from '../customizer/Customizer.js';
import { useEnv } from '../../../../shared/components/EnvironmentProvider.js';
import { DebugCustomized } from '../telemetry/Debug.js';

/**
* @param {string} id
Expand Down Expand Up @@ -36,6 +38,7 @@ export async function widgetEntryPoint(id) {

export function WidgetList() {
const { widgets, widgetConfigItems, entryPoints } = useContext(WidgetConfigContext);
const { env } = useEnv();

return (
<Stack gap={'var(--sp-8)'}>
Expand All @@ -53,6 +56,7 @@ export function WidgetList() {
</Fragment>
);
})}
{env === 'development' && <DebugCustomized index={widgets.length} />}
<CustomizerMenuPositionedFixed>
<Customizer />
</CustomizerMenuPositionedFixed>
Expand Down
6 changes: 4 additions & 2 deletions special-pages/pages/new-tab/src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createTypedMessages } from '@duckduckgo/messaging';
import { createSpecialPageMessaging } from '../../../../shared/create-special-page-messaging';
import { Environment } from '../../../../shared/environment.js';
import { mockTransport } from './mock-transport.js';
import { install } from '../../app/telemetry/telemetry.js';

export class NewTabPage {
/**
Expand Down Expand Up @@ -57,7 +58,7 @@ export class NewTabPage {

const baseEnvironment = new Environment().withInjectName(import.meta.injectName).withEnv(import.meta.env);

const messaging = createSpecialPageMessaging({
const rawMessaging = createSpecialPageMessaging({
injectName: import.meta.injectName,
env: import.meta.env,
pageName: 'newTabPage',
Expand All @@ -71,9 +72,10 @@ const messaging = createSpecialPageMessaging({
},
});

const { messaging, telemetry } = install(rawMessaging);
const newTabMessaging = new NewTabPage(messaging, import.meta.injectName);

init(newTabMessaging, baseEnvironment).catch((e) => {
init(newTabMessaging, telemetry, baseEnvironment).catch((e) => {
console.error(e);
const msg = typeof e?.message === 'string' ? e.message : 'unknown init error';
newTabMessaging.reportInitException(msg);
Expand Down

0 comments on commit 22e3eb0

Please sign in to comment.