{"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"]}