Skip to content

Commit

Permalink
Dynamically load BrowserPerformanceMeasurement to capture browser p…
Browse files Browse the repository at this point in the history
…erf measurements if session storage flag is set (#6748)

- Dynamically load `BrowserPerformanceMeasurement` to capture browser
perf measurements if session storage flag is set.
- Calculate telemetry event duration using Unix epoch if browser
performance API is not available.
- Update browser performance doc.
  • Loading branch information
konstantin-msft authored Dec 14, 2023
1 parent c2dfe4e commit 4aacf4d
Show file tree
Hide file tree
Showing 12 changed files with 227 additions and 179 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Dynamically load BrowserPerformanceMeasurement to capture browser perf measurements if session storage flag is set #6748",
"packageName": "@azure/msal-browser",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Dynamically load BrowserPerformanceMeasurement to capture browser perf measurements if session storage flag is set #6748",
"packageName": "@azure/msal-common",
"email": "[email protected]",
"dependentChangeType": "patch"
}
16 changes: 16 additions & 0 deletions lib/msal-browser/docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,19 @@ const callbackId: string = msalInstance.addPerformanceCallback((events: Performa

const removed: boolean = msalInstance.removePerformanceCallback(callbackId);
```

### Measuring browser performance

Browser performance measurements are disabled by default due to significant performance overhead they impose.
Applications that want to enable performance measurements reported to the browser's performance timeline should:

1. Open browser developer tools
- Edge, Chrome and Firefox browsers: press F12
- Safari: go into Safari's preferences (`Safari Menu` > `Preferences`), select the `Advanced Tab` and enable `Show features for web developers`. Once that menu is enabled, you will find the developer console by clicking on `Develop` > `Show Javascript Console`
2. Navigate to `Session Storage`:
- [Edge](https://learn.microsoft.com/en-us/microsoft-edge/devtools-guide-chromium/storage/sessionstorage)
- [Chrome](https://developer.chrome.com/docs/devtools/storage/sessionstorage)
- [Firefox](https://firefox-source-docs.mozilla.org/devtools-user/storage_inspector/local_storage_session_storage)
- Safari: navigate to `Storage` tab and expand `Session Storage`
3. Select target domain
4. Add `msal.browser.performance.enabled` key to `Session Storage`, set it's value to `1`, refresh the page and check the browser's performance timeline.
137 changes: 95 additions & 42 deletions lib/msal-browser/src/telemetry/BrowserPerformanceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,65 @@
*/

import {
Constants,
InProgressPerformanceEvent,
IPerformanceClient,
Logger,
PerformanceClient,
PerformanceEvent,
PerformanceEvents,
IPerformanceClient,
PerformanceClient,
IPerformanceMeasurement,
InProgressPerformanceEvent,
SubMeasurement,
PreQueueEvent,
Constants,
SubMeasurement,
} from "@azure/msal-common";
import { BrowserPerformanceMeasurement } from "./BrowserPerformanceMeasurement";
import { Configuration } from "../config/Configuration";
import { name, version } from "../packageMetadata";
import {
BROWSER_PERF_ENABLED_KEY,
BrowserCacheLocation,
} from "../utils/BrowserConstants";

/**
* Returns browser performance measurement module if session flag is enabled. Returns undefined otherwise.
*/
function getPerfMeasurementModule() {
let sessionStorage: Storage | undefined;
try {
sessionStorage = window[BrowserCacheLocation.SessionStorage];
const perfEnabled = sessionStorage?.getItem(BROWSER_PERF_ENABLED_KEY);
if (Number(perfEnabled) === 1) {
return import("./BrowserPerformanceMeasurement");
}
// Mute errors if it's a non-browser environment or cookies are blocked.
} catch (e) {}

return undefined;
}

/**
* Returns boolean, indicating whether browser supports window.performance.now() function.
*/
function supportsBrowserPerformanceNow(): boolean {
return (
typeof window !== "undefined" &&
typeof window.performance !== "undefined" &&
typeof window.performance.now === "function"
);
}

/**
* Returns event duration in milliseconds using window performance API if available. Returns undefined otherwise.
* @param startTime {DOMHighResTimeStamp | undefined}
* @returns {number | undefined}
*/
function getPerfDurationMs(
startTime: DOMHighResTimeStamp | undefined
): number | undefined {
if (!startTime || !supportsBrowserPerformanceNow()) {
return undefined;
}

return Math.round(window.performance.now() - startTime);
}

export class BrowserPerformanceClient
extends PerformanceClient
Expand All @@ -42,13 +87,6 @@ export class BrowserPerformanceClient
);
}

startPerformanceMeasurement(
measureName: string,
correlationId: string
): IPerformanceMeasurement {
return new BrowserPerformanceMeasurement(measureName, correlationId);
}

generateId(): string {
return window.crypto.randomUUID();
}
Expand All @@ -60,32 +98,27 @@ export class BrowserPerformanceClient
private deleteIncompleteSubMeasurements(
inProgressEvent: InProgressPerformanceEvent
): void {
const rootEvent = this.eventsByCorrelationId.get(
inProgressEvent.event.correlationId
);
const isRootEvent =
rootEvent && rootEvent.eventId === inProgressEvent.event.eventId;
const incompleteMeasurements: SubMeasurement[] = [];
if (isRootEvent && rootEvent?.incompleteSubMeasurements) {
rootEvent.incompleteSubMeasurements.forEach((subMeasurement) => {
incompleteMeasurements.push({ ...subMeasurement });
});
}
// Clean up remaining marks for incomplete sub-measurements
if (incompleteMeasurements.length > 0) {
BrowserPerformanceMeasurement.flushMeasurements(
void getPerfMeasurementModule()?.then((module) => {
const rootEvent = this.eventsByCorrelationId.get(
inProgressEvent.event.correlationId
);
const isRootEvent =
rootEvent &&
rootEvent.eventId === inProgressEvent.event.eventId;
const incompleteMeasurements: SubMeasurement[] = [];
if (isRootEvent && rootEvent?.incompleteSubMeasurements) {
rootEvent.incompleteSubMeasurements.forEach(
(subMeasurement: SubMeasurement) => {
incompleteMeasurements.push({ ...subMeasurement });
}
);
}
// Clean up remaining marks for incomplete sub-measurements
module.BrowserPerformanceMeasurement.flushMeasurements(
inProgressEvent.event.correlationId,
incompleteMeasurements
);
}
}

supportsBrowserPerformanceNow(): boolean {
return (
typeof window !== "undefined" &&
typeof window.performance !== "undefined" &&
typeof window.performance.now === "function"
);
});
}

/**
Expand All @@ -102,30 +135,50 @@ export class BrowserPerformanceClient
): InProgressPerformanceEvent {
// Capture page visibilityState and then invoke start/end measurement
const startPageVisibility = this.getPageVisibility();

const inProgressEvent = super.startMeasurement(
measureName,
correlationId
);
const startTime: number | undefined = supportsBrowserPerformanceNow()
? window.performance.now()
: undefined;

const browserMeasurement = getPerfMeasurementModule()?.then(
(module) => {
return new module.BrowserPerformanceMeasurement(
measureName,
inProgressEvent.event.correlationId
);
}
);
void browserMeasurement?.then((measurement) =>
measurement.startMeasurement()
);

return {
...inProgressEvent,
end: (
event?: Partial<PerformanceEvent>
): PerformanceEvent | null => {
const res = inProgressEvent.end({
...event,
startPageVisibility,
endPageVisibility: this.getPageVisibility(),
...event,
durationMs: getPerfDurationMs(startTime),
});
void browserMeasurement?.then((measurement) =>
measurement.endMeasurement()
);
this.deleteIncompleteSubMeasurements(inProgressEvent);

return res;
},
discard: () => {
inProgressEvent.discard();
void browserMeasurement?.then((measurement) =>
measurement.flushMeasurement()
);
this.deleteIncompleteSubMeasurements(inProgressEvent);
inProgressEvent.measurement.flushMeasurement();
},
};
}
Expand All @@ -140,7 +193,7 @@ export class BrowserPerformanceClient
eventName: PerformanceEvents,
correlationId?: string
): void {
if (!this.supportsBrowserPerformanceNow()) {
if (!supportsBrowserPerformanceNow()) {
this.logger.trace(
`BrowserPerformanceClient: window performance API not available, unable to set telemetry queue time for ${eventName}`
);
Expand Down Expand Up @@ -193,7 +246,7 @@ export class BrowserPerformanceClient
queueTime?: number,
manuallyCompleted?: boolean
): void {
if (!this.supportsBrowserPerformanceNow()) {
if (!supportsBrowserPerformanceNow()) {
this.logger.trace(
`BrowserPerformanceClient: window performance API not available, unable to add queue measurement for ${eventName}`
);
Expand Down
2 changes: 2 additions & 0 deletions lib/msal-browser/src/utils/BrowserConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,5 @@ export const iFrameRenewalPolicies: CacheLookupPolicy[] = [

export const LOG_LEVEL_CACHE_KEY = "msal.browser.log.level";
export const LOG_PII_CACHE_KEY = "msal.browser.log.pii";

export const BROWSER_PERF_ENABLED_KEY = "msal.browser.performance.enabled";
17 changes: 0 additions & 17 deletions lib/msal-browser/test/app/PublicClientApplication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ import { NativeInteractionClient } from "../../src/interaction_client/NativeInte
import { NativeTokenRequest } from "../../src/broker/nativeBroker/NativeRequest";
import { NativeAuthError } from "../../src/error/NativeAuthError";
import { StandardController } from "../../src/controllers/StandardController";
import { BrowserPerformanceMeasurement } from "../../src/telemetry/BrowserPerformanceMeasurement";
import { AuthenticationResult } from "../../src/response/AuthenticationResult";
import { BrowserPerformanceClient } from "../../src/telemetry/BrowserPerformanceClient";
import {
Expand Down Expand Up @@ -130,18 +129,6 @@ let testAppConfig = {
},
};

jest.mock("../../src/telemetry/BrowserPerformanceMeasurement", () => {
return {
BrowserPerformanceMeasurement: jest.fn().mockImplementation(() => {
return {
startMeasurement: () => {},
endMeasurement: () => {},
flushMeasurement: () => 50,
};
}),
};
});

function stubProvider(config: Configuration) {
const browserEnvironment = typeof window !== "undefined";

Expand Down Expand Up @@ -188,10 +175,6 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {

await pca.initialize();

BrowserPerformanceMeasurement.flushMeasurements = jest
.fn()
.mockReturnValue(null);

// Navigation not allowed in tests
jest.spyOn(
NavigationClient.prototype,
Expand Down
12 changes: 0 additions & 12 deletions lib/msal-browser/test/broker/NativeMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,6 @@ import { CryptoOps } from "../../src/crypto/CryptoOps";

let performanceClient: IPerformanceClient;

jest.mock("../../src/telemetry/BrowserPerformanceMeasurement", () => {
return {
BrowserPerformanceMeasurement: jest.fn().mockImplementation(() => {
return {
startMeasurement: () => {},
endMeasurement: () => {},
flushMeasurement: () => 50,
};
}),
};
});

describe("NativeMessageHandler Tests", () => {
let postMessageSpy: sinon.SinonSpy;
let mcPort: MessagePort;
Expand Down
Loading

0 comments on commit 4aacf4d

Please sign in to comment.