@microsoft/windows-admin-center-sdk
Version:
Microsoft - Windows Admin Center Shell
1 lines • 30.5 kB
Source Map (JSON)
{"version":3,"sources":["../../../packages/core/performance/performance-tracker.ts"],"names":[],"mappings":"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","file":"performance-tracker.d.ts","sourcesContent":["import { getCLS, getFCP, getFID, getLCP, getTTFB } from 'web-vitals';\r\nimport { Logging } from '../diagnostics/logging';\r\nimport { SmeWebTelemetry } from '../diagnostics/sme-web-telemetry';\r\nimport { LighthousePerformanceMetrics, SmePerformanceData } from '../diagnostics/sme-web-telemetry-models';\r\n\r\n/**\r\n * Summarizes a sequence of measurements\r\n */\r\nexport class SmePerformanceSummary {\r\n label: string;\r\n startTime: number;\r\n totalLoadTime: number;\r\n timestamps: { [index: string]: number };\r\n}\r\n\r\n/**\r\n * Web Vitals metrics to track. See more details at https://web.dev/vitals/\r\n */\r\nexport enum WebVitalFields {\r\n CLS = 'CumulativeLayoutShift',\r\n LCP = 'LargestContentfulPaint',\r\n FCP = 'FirstContentfulPaint',\r\n FID = 'FirstInputDelay',\r\n TTFB = 'TimeToFirstByte',\r\n TBT = 'TotalBlockingTime',\r\n TTI = 'TimeToInteractive',\r\n}\r\n\r\n/**\r\n * List performance entry types that are relevant for our metrics.\r\n */\r\nexport enum PerformanceEntryType {\r\n LongTask = 'longtask',\r\n Resource = 'resource',\r\n Mark = 'mark',\r\n Navigation = 'navigation'\r\n}\r\n\r\n/**\r\n * Simplified performance entry type with only a start time, end time, and entryType.\r\n */\r\nexport class TaskTiming {\r\n startTime: number;\r\n endTime: number;\r\n entryType: PerformanceEntryType;\r\n}\r\n\r\n/**\r\n * Internal further simplification for a range of time with only start and end time.\r\n */\r\nclass TimeWindow {\r\n start: number;\r\n end: number;\r\n}\r\n\r\n/**\r\n * Performance tracker class handles two things:\r\n * 1) Tracking lighthouse metrics (see https://web.dev/vitals/)\r\n * 2) Providing a wrapper to mark various times in code for performance measurement.\r\n * This is used primarily for shell and various tools to determine load times.\r\n */\r\nexport class PerformanceTracker {\r\n public static get smePrefix(): string {\r\n return 'SME:';\r\n }\r\n\r\n /**\r\n * The source name to use when logging about this service.\r\n */\r\n private static get logSourceName() {\r\n return 'PerformanceTracker';\r\n }\r\n\r\n // indexLoaded is logged by each extension when there index.html file starts executing code\r\n // The value itself is used, not the variable. This is kept here to have a complete list of sme marks used.\r\n private static markIndexLoaded = PerformanceTracker.smePrefix + 'IndexLoaded';\r\n private static markCoreEnvironmentInitStarted = PerformanceTracker.smePrefix + 'CoreEnvironmentInitializeStarted';\r\n private static markCoreEnvironmentInitCompleted = PerformanceTracker.smePrefix + 'CoreEnvironmentInitializeCompleted';\r\n private static markManifestLoadStarted = PerformanceTracker.smePrefix + 'ManifestLoadStarted';\r\n private static markLocalizationStarted = PerformanceTracker.smePrefix + 'LocalizationStarted';\r\n private static markAccessibilityManagerStarted = PerformanceTracker.smePrefix + 'AccessibilityManagerStarted';\r\n private static markAppContextServiceStarted = PerformanceTracker.smePrefix + 'AppContextServiceStarted';\r\n private static markAppContextRpcInitStarted = PerformanceTracker.smePrefix + 'AppContextRpcInitStarted';\r\n private static markAppContextRpcInitComplete = PerformanceTracker.smePrefix + 'AppContextRpcInitComplete';\r\n private static markAppModuleInitialized = PerformanceTracker.smePrefix + 'AppModuleInitialized';\r\n private static markAppComponentInitialized = PerformanceTracker.smePrefix + 'AppComponentInitialized';\r\n\r\n private static markCriticalDataLoaded = PerformanceTracker.smePrefix + 'CriticalDataLoaded';\r\n private static markNavigationInitialized = PerformanceTracker.smePrefix + 'NavigationInitialized';\r\n private static markNavigationStarted = PerformanceTracker.smePrefix + 'NavigationStarted';\r\n private static markNavigationCompleted = PerformanceTracker.smePrefix + 'NavigationCompleted';\r\n private static markNavigationMeasure = PerformanceTracker.smePrefix + 'NavigationMeasure';\r\n\r\n private static moduleOpening = true;\r\n\r\n /**\r\n * Used to keep track of various marks when measuring arbitrary timings.\r\n */\r\n private static dataLoadMap: {[index: string]: PerformanceEntry[]} = {};\r\n\r\n /**\r\n * Counter for total blocking time on initial loadup. This should be reset every time a new page is initialized.\r\n */\r\n private static totalBlockingTime = 0;\r\n\r\n // initialize totalBlockingTime as null so we can distinguish when majority of lighthouse metrics are collected\r\n private static lighthouseMetrics: LighthousePerformanceMetrics = {TotalBlockingTime: null};\r\n private static ttiIntervalCheck: NodeJS.Timeout;\r\n private static ttiInProgress = false;\r\n private static wacPo: PerformanceObserver;\r\n private static fcp = 0;\r\n private static tasks: TaskTiming[] = [];\r\n\r\n public static readonly timeToInteractiveNotFound = -1;\r\n\r\n // Under TTI guidelines, a quiet window is defined to be a 5 second period where no long tasks run and no more than\r\n // 2 network requests at any given point.\r\n public static readonly quietWindowLength = 5000;\r\n\r\n // Time to interactive has to be calculated retroactively - every 10 seconds, we'll calculate and see if we can find TTI.\r\n // After 20 seconds, cancel and just find current tti (assume a quiet window will appear somewhere in the future)\r\n // These windows were selected arbitrarily - can be tweaked if necessary.\r\n public static readonly ttiTimeout = 10000;\r\n public static readonly ttiTimeoutThreshold = 2;\r\n\r\n // Long events (events in excess of 50ms) qualify for total blocking time - any duration in excess of 50ms,\r\n // the excess time will be added to total blocking time.\r\n public static readonly totalBlockingTimeThresholdRequirement = 50;\r\n\r\n private static setLighthouseField(field: string, value: number) {\r\n this.lighthouseMetrics[field] = value;\r\n if (this.lighthouseMetrics.TotalBlockingTime !== null) {\r\n // if TBT is calculated, we (should) have all required fields, log this in telemetry.\r\n // It is possible once TBT is calculated other fields can be triggered afterwards (eg CLS or LCP).\r\n // We will simply update those fields within lighthouse object and send the whole thing.\r\n // On telemetry end we can decide how to handle (ie take first or last instance).\r\n SmeWebTelemetry.traceLighthouseData(this.lighthouseMetrics);\r\n }\r\n }\r\n\r\n /**\r\n * Calculate Time to Interactive using list of longtask and resource events. Time to Interactive calculation\r\n * method can be found at https://web.dev/tti#what-is-tti\r\n * @param taskList List of all longtask and resource events from current time.\r\n * @returns Returns time to interactive if it exists, TTI_NOT_FOUND if not.\r\n */\r\n public static tryFindTti(taskList: TaskTiming[]): number {\r\n const timestamps: {entryType: PerformanceEntryType, time: number, isStart?: boolean}[] = [];\r\n\r\n taskList.forEach((entry) => {\r\n timestamps.push({entryType: entry.entryType, time: entry.startTime, isStart: true});\r\n timestamps.push({entryType: entry.entryType, time: entry.endTime});\r\n });\r\n // push an entry for the current timestamp, so that if the quiet window is at the end\r\n // (eg mass entries early on, and emptiness later), we can capture the end of the window with this event.\r\n timestamps.push({entryType: PerformanceEntryType.LongTask, time: window.performance.now(), isStart: true});\r\n timestamps.sort((a, b) => a.time < b.time ? -1 : 1);\r\n\r\n const quietWindows: TimeWindow[] = [];\r\n let startTime = this.fcp;\r\n let longTaskCount = 0;\r\n let networkRequestCount = 0;\r\n // Split ranges into starts and ends, iterate through all timestamps of start/end\r\n // for each start/end, identify and keep track of long tasks and network requests to identify potential windows\r\n // isStart means the timestamp is a startTime of a performance entry (long task or resource).\r\n for (const {entryType, time, isStart} of timestamps) {\r\n if (isStart) {\r\n // check if ends current window\r\n if ((entryType === PerformanceEntryType.LongTask && longTaskCount === 0 && networkRequestCount <= 2) ||\r\n (entryType === PerformanceEntryType.Resource && longTaskCount === 0 && networkRequestCount === 2)) {\r\n const newStartTime = startTime ? Math.max(startTime, this.fcp) : this.fcp;\r\n const endTime = time;\r\n if (endTime > newStartTime) {\r\n quietWindows.push({ start: newStartTime, end: endTime });\r\n }\r\n }\r\n } else {\r\n if ((entryType === PerformanceEntryType.LongTask && longTaskCount === 1 && networkRequestCount <= 2) ||\r\n (entryType === PerformanceEntryType.Resource && longTaskCount === 0 && networkRequestCount === 3)) {\r\n startTime = time;\r\n }\r\n }\r\n // For both cases, if we just hit an isStart timestamp, we are starting a new task - increment count depending on type.\r\n // if not isStart, we are ending a task, decrement count.\r\n if (entryType === PerformanceEntryType.LongTask) {\r\n longTaskCount += isStart ? 1 : -1;\r\n } else if (entryType === PerformanceEntryType.Resource) {\r\n networkRequestCount += isStart ? 1 : -1;\r\n }\r\n }\r\n\r\n const firstValidWindow = quietWindows.find((window) => (window.end - window.start) >= this.quietWindowLength);\r\n const relevantLongTasks = taskList.filter((task) => firstValidWindow && task.entryType === PerformanceEntryType.LongTask\r\n && task.startTime >= this.fcp && task.endTime <= firstValidWindow.start);\r\n const timeToInteractiveTimestamp = relevantLongTasks.length > 0\r\n ? relevantLongTasks[relevantLongTasks.length - 1].endTime : this.fcp;\r\n // if no valid window found, return timeToInteractiveNotFound\r\n return firstValidWindow ? timeToInteractiveTimestamp : this.timeToInteractiveNotFound;\r\n }\r\n\r\n /**\r\n * Initialize interval to calculate time to interactive. TTI will only be calculated if WAC is in non-production mode;\r\n * b/c calculation itself is non-performant. In production, we will use FID as a proxy for TTI.\r\n */\r\n public static tryStartCalculatingTimeToInteractive(): void {\r\n if (SmeWebTelemetry.isProduction || this.ttiInProgress) {\r\n return;\r\n }\r\n this.ttiInProgress = true;\r\n let ttiTrackerCount = 0;\r\n this.ttiIntervalCheck = setInterval(() => {\r\n const tti = this.tryFindTti(this.tasks);\r\n if (tti !== this.timeToInteractiveNotFound) {\r\n this.setLighthouseField(WebVitalFields.TTI, tti);\r\n // need to recalculate here because this is dependent on where the TTI is.\r\n const recalculatedTotalBlockingTime =\r\n this.tasks.filter((entry) => entry.entryType === PerformanceEntryType.LongTask).reduce((totalBlockTime, nextEntry) => {\r\n const duration = nextEntry.endTime - nextEntry.startTime;\r\n const entrySatisfiesBlockingCriteria = duration > this.totalBlockingTimeThresholdRequirement\r\n && nextEntry.endTime < tti\r\n && nextEntry.startTime > this.fcp;\r\n return totalBlockTime + (entrySatisfiesBlockingCriteria ? duration : 0 );\r\n }, 0);\r\n this.setLighthouseField(WebVitalFields.TBT, recalculatedTotalBlockingTime);\r\n clearInterval(this.ttiIntervalCheck);\r\n this.wacPo.disconnect();\r\n } else if (ttiTrackerCount > this.ttiTimeoutThreshold) {\r\n // if we have gone past threshold and no TTI found,\r\n // give up on TTI and just search for last long task, we will already have bad score\r\n const lastLongTask = this.tasks.filter((entry) => entry.entryType === PerformanceEntryType.LongTask).slice(-1)[0];\r\n this.setLighthouseField(WebVitalFields.TTI,\r\n lastLongTask ? lastLongTask.endTime : this.ttiTimeout * this.ttiTimeoutThreshold);\r\n this.setLighthouseField(WebVitalFields.TBT, this.totalBlockingTime);\r\n this.wacPo.disconnect();\r\n clearInterval(this.ttiIntervalCheck);\r\n }\r\n ++ttiTrackerCount;\r\n }, this.ttiTimeout);\r\n }\r\n\r\n /**\r\n * Initialize web-vitals trackers for all lighthouse metrics. If function not supported (eg due to browser limitations)\r\n * log warning and return.\r\n */\r\n public static initializeLighthouseMetricsTrackers(): void {\r\n if (!getLCP) {\r\n Logging.logWarning(this.logSourceName, 'Web Vitals not supported in current environment');\r\n return;\r\n }\r\n\r\n getCLS((item) => this.setLighthouseField(WebVitalFields.CLS, item.value), true);\r\n getFCP((item) => {\r\n this.setLighthouseField(WebVitalFields.FCP, item.value);\r\n this.fcp = item.value;\r\n this.tryStartCalculatingTimeToInteractive();\r\n }, true);\r\n getLCP((item) => this.setLighthouseField(WebVitalFields.LCP, item.value), true);\r\n getFID((item) => {\r\n this.setLighthouseField(WebVitalFields.FID, item.value);\r\n if (!this.ttiInProgress) {\r\n this.setLighthouseField(WebVitalFields.TBT, this.totalBlockingTime);\r\n }\r\n });\r\n getTTFB((item) => this.setLighthouseField(WebVitalFields.TTFB, item.value));\r\n\r\n // Set a performance observer to collect all the long task and resource events we need to calculate lighthouse metrics.\r\n this.wacPo = new PerformanceObserver((list) => {\r\n const entries = list.getEntries();\r\n\r\n for (let i = 0; i < entries.length; ++i) {\r\n if (entries[i].entryType === PerformanceEntryType.LongTask) {\r\n // measure TBT as we collect long task events - if using FID, we can just send this accumulated value as is,\r\n // otherwise can recalculate this later when we find TTI.\r\n const entrySatisfiesBlockingCriteria = entries[i].duration > this.totalBlockingTimeThresholdRequirement\r\n && entries[i].startTime > this.fcp;\r\n this.totalBlockingTime += entrySatisfiesBlockingCriteria ? entries[i].duration : 0;\r\n this.tasks.push({startTime: entries[i].startTime,\r\n endTime: entries[i].startTime + entries[i].duration,\r\n entryType: PerformanceEntryType.LongTask} as TaskTiming);\r\n } else {\r\n this.tasks.push({startTime: entries[i].startTime,\r\n endTime: entries[i].startTime + entries[i].duration,\r\n entryType: PerformanceEntryType.Resource} as TaskTiming);\r\n }\r\n }\r\n });\r\n\r\n this.wacPo.observe({entryTypes: [PerformanceEntryType.LongTask, PerformanceEntryType.Resource]});\r\n }\r\n\r\n public static coreEnvironmentInitStarted(): void {\r\n performance.mark(PerformanceTracker.markCoreEnvironmentInitStarted);\r\n }\r\n\r\n public static coreEnvironmentInitCompleted(): void {\r\n performance.mark(PerformanceTracker.markCoreEnvironmentInitCompleted);\r\n }\r\n\r\n public static manifestLoadStarted(): void {\r\n performance.mark(PerformanceTracker.markManifestLoadStarted);\r\n }\r\n\r\n public static localizationStarted(): void {\r\n performance.mark(PerformanceTracker.markLocalizationStarted);\r\n }\r\n\r\n public static accessibilityManagerStarted(): void {\r\n performance.mark(PerformanceTracker.markAccessibilityManagerStarted);\r\n }\r\n\r\n public static appContextServiceStarted(): void {\r\n performance.mark(PerformanceTracker.markAppContextServiceStarted);\r\n }\r\n\r\n public static appContextRpcInitStarted(): void {\r\n performance.mark(PerformanceTracker.markAppContextRpcInitStarted);\r\n }\r\n\r\n public static appContextRpcInitComplete(): void {\r\n performance.mark(PerformanceTracker.markAppContextRpcInitComplete);\r\n }\r\n\r\n public static appModuleInitialized(): void {\r\n performance.mark(PerformanceTracker.markAppModuleInitialized);\r\n }\r\n\r\n public static appComponentInitialized(): void {\r\n performance.mark(PerformanceTracker.markAppComponentInitialized);\r\n PerformanceTracker.logAppComponentLoadTime();\r\n }\r\n\r\n public static navigationInitialized(): void {\r\n const navigationMarks = performance.getEntriesByType(PerformanceEntryType.Mark)\r\n .filter(e => e.name.startsWith(PerformanceTracker.smePrefix + 'Navigation'));\r\n // If we reinitialize navigation, clear all previous navigation marks.\r\n navigationMarks.forEach(m => {\r\n performance.clearMarks(m.name);\r\n });\r\n performance.mark(PerformanceTracker.markNavigationInitialized);\r\n }\r\n\r\n public static navigationStarted(): void {\r\n // clear previous marks prior to starting new navigation\r\n performance.clearMarks(PerformanceTracker.markNavigationStarted);\r\n performance.clearMarks(PerformanceTracker.markNavigationCompleted);\r\n\r\n performance.mark(PerformanceTracker.markNavigationStarted);\r\n }\r\n\r\n public static navigationCompleted(moduleOpened: boolean): void {\r\n // clear last measure b/c we are replacing.\r\n performance.clearMeasures(PerformanceTracker.markNavigationMeasure);\r\n\r\n // If the navigation via module open, set this flag so we can treat the criticalLoad differently than normal. We want to\r\n // separate this data b/c some modules have pivot tabs that can also trigger critical data loads multiple times.\r\n this.moduleOpening = moduleOpened;\r\n\r\n performance.mark(PerformanceTracker.markNavigationCompleted);\r\n performance.measure(PerformanceTracker.markNavigationMeasure,\r\n PerformanceTracker.markNavigationStarted,\r\n PerformanceTracker.markNavigationCompleted);\r\n\r\n // clear navigation start/end marks after we measure - only maintain one set of start/end to keep simple.\r\n performance.clearMarks(PerformanceTracker.markNavigationCompleted);\r\n performance.clearMarks(PerformanceTracker.markNavigationStarted);\r\n }\r\n\r\n public static criticalDataLoaded(): void {\r\n performance.mark(PerformanceTracker.markCriticalDataLoaded);\r\n PerformanceTracker.logCriticalDataLoadTime();\r\n }\r\n\r\n /**\r\n * Mark an arbitrary measurement with a label. Clear the previous mark(s) prior to starting new one - allow only one at\r\n * all times.\r\n * @param label The label for the mark and the index for the entry in the internal map.\r\n */\r\n public static markDataLoadStart(label: string) {\r\n const smeLabelString = `${PerformanceTracker.smePrefix}${label}`;\r\n\r\n // If we start, overwrite and clear existing entries.\r\n if (smeLabelString in this.dataLoadMap) {\r\n this.dataLoadMap[smeLabelString].forEach((mark) => {\r\n performance.clearMarks(mark.name);\r\n });\r\n }\r\n performance.mark(smeLabelString);\r\n // get latest one\r\n const startMark = performance.getEntriesByName(smeLabelString).slice(-1)[0];\r\n\r\n this.dataLoadMap[smeLabelString] = [startMark];\r\n }\r\n\r\n /**\r\n * Mark an arbitrary measurement with a label. Intended to be a midpoint between a start and end.\r\n * @param label The label for the entry in the data map.\r\n * @param specificLabel The label for the mark\r\n */\r\n public static markDataIntermediary(label: string, specificLabel?: string) {\r\n const smeLabelString = `${PerformanceTracker.smePrefix}${label}`;\r\n\r\n if (!(smeLabelString in this.dataLoadMap)) {\r\n return;\r\n }\r\n\r\n const intermediaryLabel = specificLabel ? `${PerformanceTracker.smePrefix}${specificLabel}`\r\n : `${PerformanceTracker.smePrefix}${label}-${this.dataLoadMap[smeLabelString].length + 1}`;\r\n\r\n performance.mark(intermediaryLabel);\r\n\r\n // get latest one\r\n const mark = performance.getEntriesByName(intermediaryLabel).slice(-1)[0];\r\n\r\n this.dataLoadMap[smeLabelString].push(mark);\r\n }\r\n\r\n /**\r\n * Get a summary object from the aggregate data points marked previously.\r\n * @param label The label for the entry in the data map dictionary\r\n * @param specificLabel The label for the new measurement\r\n * @returns The performance summary of the multiple marks.\r\n */\r\n public static markDataLoadEnd(label: string, specificLabel?: string): SmePerformanceSummary {\r\n const smeLabelString = `${PerformanceTracker.smePrefix}${label}`;\r\n\r\n if (!(smeLabelString in this.dataLoadMap)) {\r\n return null;\r\n }\r\n const dataEntry = this.dataLoadMap[smeLabelString];\r\n\r\n const intermediaryLabel = specificLabel ? `${PerformanceTracker.smePrefix}${specificLabel}`\r\n : `${PerformanceTracker.smePrefix}${label}-${dataEntry.length + 1}`;\r\n\r\n performance.mark(intermediaryLabel);\r\n // get latest one\r\n const endMark = performance.getEntriesByName(intermediaryLabel).slice(-1)[0];\r\n dataEntry.push(endMark);\r\n\r\n const startEntry = dataEntry[0];\r\n\r\n const totalLoadTime = endMark.startTime - startEntry.startTime;\r\n\r\n // remove SME prefix here\r\n const summary: SmePerformanceSummary = {\r\n label: label,\r\n timestamps: {},\r\n totalLoadTime: totalLoadTime,\r\n startTime: startEntry.startTime\r\n };\r\n\r\n dataEntry.forEach((mark: PerformanceEntry) => {\r\n performance.clearMarks(mark.name);\r\n summary.timestamps[mark.name.replace(PerformanceTracker.smePrefix, '')] = mark.startTime - startEntry.startTime;\r\n });\r\n\r\n delete this.dataLoadMap[smeLabelString];\r\n return summary;\r\n }\r\n\r\n /**\r\n * Log app component load time - this contains timings up to when the appComponent is loaded.\r\n */\r\n private static logAppComponentLoadTime(): void {\r\n const data: SmePerformanceData = {\r\n sme: {},\r\n resources: {},\r\n navigation: {},\r\n totalLoadTime: 0\r\n };\r\n\r\n /// dont include sme angular navigation marks in the generic data load\r\n const smeMarks = performance.getEntriesByType(PerformanceEntryType.Mark).filter(e => e.name.startsWith(PerformanceTracker.smePrefix)\r\n && !e.name.startsWith(PerformanceTracker.smePrefix + 'Navigation'));\r\n const resourceMarks = performance.getEntriesByType(PerformanceEntryType.Resource);\r\n smeMarks.forEach(m => {\r\n data.sme[m.name.replace(PerformanceTracker.smePrefix, '')] = m.startTime;\r\n performance.clearMarks(m.name);\r\n });\r\n resourceMarks.forEach((m: PerformanceResourceTiming) => {\r\n try {\r\n const url = new URL(m.name);\r\n data.resources[url.pathname] = m.duration;\r\n } catch (e) {\r\n data.resources[m.name] = m.duration;\r\n }\r\n performance.clearMarks(m.name);\r\n });\r\n data.totalLoadTime = data.sme[this.markAppComponentInitialized.replace(PerformanceTracker.smePrefix, '')];\r\n const navigationEntries = performance.getEntriesByType(PerformanceEntryType.Navigation);\r\n if (navigationEntries && navigationEntries.length) {\r\n data.navigation = navigationEntries[0];\r\n }\r\n\r\n SmeWebTelemetry.traceModuleOpenPerformance(data);\r\n }\r\n\r\n /**\r\n * Log critical data load time - this measures the time from angular's startNavigation event to a module to the point at\r\n * which criticalDataLoad() is called, usually in the ngInit of a module\r\n */\r\n private static logCriticalDataLoadTime(): void {\r\n const data: SmePerformanceData = {\r\n sme: {}, totalLoadTime: 0\r\n };\r\n\r\n // If there are multiple somehow, take the latest one\r\n const navigationMeasure = performance.getEntriesByName(PerformanceTracker.markNavigationMeasure).slice(-1)[0];\r\n const criticalData = performance.getEntriesByName(PerformanceTracker.markCriticalDataLoaded).slice(-1)[0];\r\n\r\n // clear performance marks either way - if they exist, we clear them. If not, clear b/c it's faulty data.\r\n performance.clearMeasures(PerformanceTracker.markNavigationMeasure);\r\n performance.clearMarks(PerformanceTracker.markCriticalDataLoaded);\r\n\r\n if (!navigationMeasure || !criticalData) {\r\n Logging.logInformational('PerformanceTracker',\r\n 'Critical data loaded without either criticalData call or navigationMeasure call');\r\n return;\r\n }\r\n data.sme.criticalDataLoadTime = criticalData.startTime - navigationMeasure.startTime;\r\n data.sme.navigationTime = navigationMeasure.duration;\r\n data.totalLoadTime = data.sme.criticalDataLoadTime;\r\n\r\n // If there are multiple somehow, take the latest one\r\n const navInitialized = performance.getEntriesByName(PerformanceTracker.markNavigationInitialized).slice(-1)[0];\r\n if (navInitialized) {\r\n data.sme.timeToFirstNavigateInit = navInitialized.startTime;\r\n data.sme.timeToFirstNavigateStart = navigationMeasure.startTime;\r\n data.sme.timeToFirstNavigateEnd = navigationMeasure.startTime + navigationMeasure.duration;\r\n data.sme.timeToFirstCriticalData = criticalData.startTime;\r\n\r\n data.totalLoadTime = criticalData.startTime; // overwrite if navInit is true.\r\n\r\n performance.clearMarks(navInitialized.name);\r\n }\r\n\r\n if (this.moduleOpening) {\r\n SmeWebTelemetry.traceModuleOpenPerformance(data);\r\n } else {\r\n SmeWebTelemetry.tracePerformanceData(data);\r\n }\r\n }\r\n}\r\n"]}