Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding debugging+performance helpers #1247

Merged
merged 2 commits into from
Nov 21, 2024
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
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');
shakyShane marked this conversation as resolved.
Show resolved Hide resolved
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
Loading