/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/service/performance-impl.js
1 006 строк
29 KB
Viktor Szépe
Fix typos in src/ (#39606)
12 авг 2025, 21:35
Не верифицирован
12 авг 2025, 21:35
fca692e
Код
Авторство
О чём код?
import {TickLabel_Enum} from '#core/constants/enums'; import {VisibilityState_Enum} from '#core/constants/visibility-state'; import {Signals} from '#core/data-structures/signals'; import {whenDocumentComplete, whenDocumentReady} from '#core/document/ready'; import {layoutRectLtwh} from '#core/dom/layout/rect'; import {computedStyle} from '#core/dom/style'; import {debounce} from '#core/types/function'; import {map} from '#core/types/object'; import {base64UrlEncodeFromBytes} from '#core/types/string/base64'; import {getCryptoRandomBytesArray} from '#core/types/string/bytes'; import {isExperimentOn} from '#experiments'; import {Services} from '#service'; import {createCustomEvent} from '#utils/event-helper'; import {dev, devAssert} from '#utils/log'; import {isStoryDocument} from '#utils/story'; import {whenContentIniLoad} from '../ini-load'; import {getMode} from '../mode'; import {getService, registerServiceBuilder} from '../service-helpers'; /** * Maximum number of tick events we allow to accumulate in the performance * instance's queue before we start dropping those events and can no longer * be forwarded to the actual `tick` function when it is set. */ const QUEUE_LIMIT = 50; const CLS_SESSION_GAP = 1000; const CLS_SESSION_MAX = 5000; const INP_REPORTING_THRESHOLD = 40; const TAG = 'Performance'; /** * Fields: * {{ * label: string, * delta: (number|null|undefined), * value: (number|null|undefined) * }} * @typedef {!JsonObject} */ let TickEventDef; /** * @enum {number} */ export const ELEMENT_TYPE_ENUM = { other: 0, image: 1 << 0, video: 1 << 1, ad: 1 << 2, carousel: 1 << 3, bcarousel: 1 << 4, text: 1 << 5, }; /** * @param {?Node} node * @return {ELEMENT_TYPE_ENUM} */ function getElementType(node) { if (node == null) { return ELEMENT_TYPE_ENUM.other; } const outer = getOutermostAmpElement(node); const {nodeName} = outer; if (nodeName === 'IMG' || nodeName === 'AMP-IMG') { return ELEMENT_TYPE_ENUM.image; } if (nodeName === 'VIDEO' || nodeName === 'AMP-VIDEO') { return ELEMENT_TYPE_ENUM.video; } if (nodeName === 'AMP-CAROUSEL') { return ELEMENT_TYPE_ENUM.carousel; } if (nodeName === 'AMP-BASE-CAROUSEL') { return ELEMENT_TYPE_ENUM.bcarousel; } if (nodeName === 'AMP-AD') { return ELEMENT_TYPE_ENUM.ad; } if (!nodeName.startsWith('AMP-') && outer.textContent) { return ELEMENT_TYPE_ENUM.text; } return ELEMENT_TYPE_ENUM.other; } /** * Performance holds the mechanism to call `tick` to stamp out important * events in the lifecycle of the AMP runtime. It can hold a small amount * of tick events to forward to the external `tick` function when it is set. */ export class Performance { /** * @param {!Window} win */ constructor(win) { /** @const {!Window} */ this.win = win; /** @const {string} */ this.eventid_ = base64UrlEncodeFromBytes( getCryptoRandomBytesArray(win, 16) ); /** @const @private {!Array<TickEventDef>} */ this.events_ = []; /** @const @private {number} */ this.timeOrigin_ = win.performance.timeOrigin || win.performance.timing.navigationStart; /** @private {?./ampdoc-impl.AmpDoc} */ this.ampdoc_ = null; /** @private {?./viewer-interface.ViewerInterface} */ this.viewer_ = null; /** @private {?./resources-interface.ResourcesInterface} */ this.resources_ = null; /** @private {?./document-info-impl.DocumentInfoDef} */ this.documentInfo_ = null; /** @private {boolean} */ this.isMessagingReady_ = false; /** @private {boolean} */ this.isPerformanceTrackingOn_ = false; /** @private {!{[key: string]: boolean}} */ this.enabledExperiments_ = map(); /** @private {string|undefined} */ this.ampexp_ = undefined; /** @private {Signals} */ this.metrics_ = new Signals(); /** * How many times a layout shift metric has been ticked. * * @private {number} */ this.shiftScoresTicked_ = 0; /** * The collection of layout shift events from the Layout Instability API, * used for normalized windowed sessions according to the latest CWV * implementation. This uses 5s max window with a 1s session gap as the * session size. * See https://github.com/GoogleChrome/web-vitals/blob/main/src/getCLS.ts * @private {Array<LayoutShift>} */ this.layoutShiftEntries_ = []; /** * The sum of all layout shifts. * @private {number} */ this.layoutShiftSum_ = 0; const supportedEntryTypes = (this.win.PerformanceObserver && this.win.PerformanceObserver.supportedEntryTypes) || []; // If Paint Timing API is not supported, cannot determine first contentful paint if (!supportedEntryTypes.includes('paint')) { this.metrics_.rejectSignal( TickLabel_Enum.FIRST_CONTENTFUL_PAINT, dev().createExpectedError('First Contentful Paint not supported') ); } /** * Whether the user agent supports the Layout Instability API that shipped * with Chromium 77. * * @private {boolean} */ this.supportsLayoutShift_ = supportedEntryTypes.includes('layout-shift'); if (!this.supportsLayoutShift_) { const e = dev().createExpectedError( 'Cumulative Layout Shift not supported' ); this.metrics_.rejectSignal(TickLabel_Enum.CUMULATIVE_LAYOUT_SHIFT, e); this.metrics_.rejectSignal(TickLabel_Enum.CUMULATIVE_LAYOUT_SHIFT_1, e); } /** * Whether the user agent supports the Event Timing API that shipped * with Chromium 77. * * @private {boolean} */ this.supportsEventTiming_ = supportedEntryTypes.includes('first-input'); if (!this.supportsEventTiming_) { this.metrics_.rejectSignal( TickLabel_Enum.FIRST_INPUT_DELAY, dev().createExpectedError('First Input Delay not supported') ); } /** * Whether the user agent supports the Largest Contentful Paint metric. * * @private {boolean} */ this.supportsLargestContentfulPaint_ = supportedEntryTypes.includes( 'largest-contentful-paint' ); if (!this.supportsLargestContentfulPaint_) { this.metrics_.rejectSignal( TickLabel_Enum.LARGEST_CONTENTFUL_PAINT, dev().createExpectedError('Largest Contentful Paint not supported') ); } /** * Whether the user agent supports the navigation timing API * * @private {boolean} */ this.supportsNavigation_ = supportedEntryTypes.includes('navigation'); /** * Whether the user agent supports the interaction to next paint metric. */ this.supportsEvents_ = supportedEntryTypes.includes('event') && isExperimentOn(win, 'interaction-to-next-paint'); if (!this.supportsEvents_) { this.metrics_.rejectSignal( TickLabel_Enum.INTERACTION_TO_NEXT_PAINT, dev().createExpectedError('Interaction to next paint not supported') ); } this.onAmpDocVisibilityChange_ = this.onAmpDocVisibilityChange_.bind(this); // Add RTV version as experiment ID, so we can slice the data by version. this.addEnabledExperiment('rtv-' + getMode(this.win).rtvVersion); // Tick document ready event. whenDocumentReady(win.document).then(() => { this.tick(TickLabel_Enum.DOCUMENT_READY); this.flush(); }); // Tick window.onload event. whenDocumentComplete(win.document).then(() => this.onload_()); whenDocumentComplete(win.document).then(() => this.tickInteractionToNextPaint_(INP_REPORTING_THRESHOLD) ); this.registerPerformanceObserver_(); /** * @private {boolean} */ this.googleFontExpRecorded_ = false; /** * This is called to ensure we'll report the current cls window's value * after the window closes. Its debounce time is intentionally longer than * the max session time so that we're certain the session has closed * (since a PerfOb is async, entries that belong in the current window may * arrive later). */ this.debouncedFlushLayoutShiftScore_ = debounce( win, () => { this.flushLayoutShiftScore_(); }, CLS_SESSION_MAX + 1000 ); } /** * Listens to viewer and resource events. * @return {!Promise} */ coreServicesAvailable() { const {documentElement} = this.win.document; this.ampdoc_ = Services.ampdoc(documentElement); this.viewer_ = Services.viewerForDoc(documentElement); this.resources_ = Services.resourcesForDoc(documentElement); this.documentInfo_ = Services.documentInfoForDoc(this.ampdoc_); this.isPerformanceTrackingOn_ = this.viewer_.isEmbedded() && this.viewer_.getParam('csi') === '1'; // This is for redundancy. Call flush on any visibility change. this.ampdoc_.onVisibilityChanged(this.flush.bind(this)); // Does not need to wait for messaging ready since it will be queued // if it isn't ready. this.measureUserPerceivedVisualCompletenessTime_(); // Can be null which would mean this AMP page is not embedded // and has no messaging channel. const channelPromise = this.viewer_.whenMessagingReady(); this.ampdoc_.whenFirstVisible().then(() => { this.tick(TickLabel_Enum.ON_FIRST_VISIBLE); this.flush(); }); const registerVisibilityChangeListener = this.supportsLargestContentfulPaint_ || this.supportsLayoutShift_; // Register a handler to record metrics when the page enters the hidden // lifecycle state. if (registerVisibilityChangeListener) { this.ampdoc_.onVisibilityChanged(this.onAmpDocVisibilityChange_); } // We don't check `isPerformanceTrackingOn` here since there are some // events that we call on the viewer even though performance tracking // is off we only need to know if the AMP page has a messaging // channel or not. if (!channelPromise) { return Promise.resolve(); } return channelPromise .then(() => { // Tick the "messaging ready" signal. this.tickDelta( TickLabel_Enum.MESSAGING_READY, this.win.performance.now() ); // Tick timeOrigin so that epoch time can be calculated by consumers. this.tick(TickLabel_Enum.TIME_ORIGIN, undefined, this.timeOrigin_); const usqp = this.ampdoc_.getMetaByName('amp-usqp'); if (usqp) { usqp.split(',').forEach((exp) => { this.addEnabledExperiment('ssr-' + exp); }); } return this.maybeAddStoryExperimentId_(); }) .then(() => { this.isMessagingReady_ = true; // Forward all queued ticks to the viewer since messaging // is now ready. this.flushQueuedTicks_(); // Send all csi ticks through. this.flush(); }); } /** * Add a story experiment ID in order to slice the data for amp-story. * @return {!Promise} * @private */ maybeAddStoryExperimentId_() { const ampdoc = Services.ampdocServiceFor(this.win).getSingleDoc(); return isStoryDocument(ampdoc).then((isStory) => { if (isStory) { this.addEnabledExperiment('story'); } }); } /** * Callback for onload. */ onload_() { this.tick(TickLabel_Enum.ON_LOAD); this.flush(); } /** * Reports performance metrics first paint, first contentful paint, * and first input delay. * See https://github.com/WICG/paint-timing */ registerPerformanceObserver_() { // Turn off performanceObserver derived metrics for inabox as there // will never be a viewer to report to. // TODO(ccordry): we are still doing some other unnecessary measurements for // the inabox case, but would need a larger refactor. if (getMode(this.win).runtime === 'inabox') { return; } // These state vars ensure that we only report a given value once, because // the backend doesn't support updates. let recordedFirstPaint = false; let recordedFirstContentfulPaint = false; let recordedFirstInputDelay = false; let recordedNavigation = false; const processEntry = (entry) => { if (entry.name == 'first-paint' && !recordedFirstPaint) { this.tickDelta( TickLabel_Enum.FIRST_PAINT, entry.startTime + entry.duration ); recordedFirstPaint = true; } else if ( entry.name == 'first-contentful-paint' && !recordedFirstContentfulPaint ) { const value = entry.startTime + entry.duration; this.tickDelta(TickLabel_Enum.FIRST_CONTENTFUL_PAINT, value); this.tickSinceVisible( TickLabel_Enum.FIRST_CONTENTFUL_PAINT_VISIBLE, value ); recordedFirstContentfulPaint = true; } else if ( entry.entryType === 'first-input' && !recordedFirstInputDelay ) { const value = entry.processingStart - entry.startTime; this.tickDelta(TickLabel_Enum.FIRST_INPUT_DELAY, value); recordedFirstInputDelay = true; } else if (entry.entryType === 'layout-shift') { // Ignore layout shift that occurs within 500ms of user input, as it is // likely in response to the user's action. if (!entry.hadRecentInput) { this.tickLayoutShiftScore_(entry); this.layoutShiftSum_ += entry.value; } } else if (entry.entryType === 'largest-contentful-paint') { this.tickLargestContentfulPaint_(entry); } else if (entry.entryType == 'navigation' && !recordedNavigation) { [ 'domComplete', 'domContentLoadedEventEnd', 'domContentLoadedEventStart', 'domInteractive', 'loadEventEnd', 'loadEventStart', 'requestStart', 'responseStart', ].forEach((label) => this.tick(label, entry[label])); recordedNavigation = true; } else if (entry.entryType == 'event' && entry.interactionId) { this.tickInteractionToNextPaint_(entry.duration); } }; const entryTypesToObserve = []; if (this.win.PerformancePaintTiming) { // Programmatically read once as currently PerformanceObserver does not // report past entries as of Chromium 61. // https://bugs.chromium.org/p/chromium/issues/detail?id=725567 this.win.performance.getEntriesByType('paint').forEach(processEntry); entryTypesToObserve.push('paint'); } if (this.supportsEventTiming_) { this.createPerformanceObserver_(processEntry, { type: 'first-input', buffered: true, }); } if (this.supportsLayoutShift_) { this.createPerformanceObserver_(processEntry, { type: 'layout-shift', buffered: true, }); } if (this.supportsLargestContentfulPaint_) { // lcpObserver this.createPerformanceObserver_(processEntry, { type: 'largest-contentful-paint', buffered: true, }); } if (this.supportsNavigation_) { // Wrap in a try statement as there are some browsers (ex. chrome 73) // that will say it supports navigation but throws. this.createPerformanceObserver_(processEntry, { type: 'navigation', buffered: true, }); } if (this.supportsEvents_) { this.createPerformanceObserver_(processEntry, { type: 'event', durationThreshold: INP_REPORTING_THRESHOLD, // Minimim duration of 40ms, as implemented in Chrome web-vitals buffered: true, }); } if (entryTypesToObserve.length > 0) { this.createPerformanceObserver_(processEntry, { entryTypes: entryTypesToObserve, }); } } /** * @param {function(!PerformanceEntry)} processEntry * @param {!PerformanceObserverInit} init * @return {!PerformanceObserver} * @private */ createPerformanceObserver_(processEntry, init) { try { const obs = new this.win.PerformanceObserver((list) => { list.getEntries().forEach(processEntry); this.flush(); }); obs.observe(init); } catch (err) { dev().warn(TAG, err); } } /** * Whether the AMP doc is hidden. * @return {boolean} */ isVisibilityHidden_() { const state = this.ampdoc_.getVisibilityState(); return ( state === VisibilityState_Enum.INACTIVE || state === VisibilityState_Enum.HIDDEN ); } /** * When the viewer visibility state of the document changes to inactive or hidden, * send the layout score. * @private */ onAmpDocVisibilityChange_() { if (this.isVisibilityHidden_()) { this.tickCumulativeMetrics_(); this.flushLayoutShiftScore_(); } } /** @private */ recordGoogleFontExp_() { if (!this.googleFontExpRecorded_) { this.googleFontExpRecorded_ = true; const {win} = this; const googleFontExp = parseInt( computedStyle(win, win.document.body).getPropertyValue( '--google-font-exp' ), 10 ); if (googleFontExp >= 0) { this.addEnabledExperiment(`google-font-exp=${googleFontExp}`); } } } /** * Tick the metrics whose values change over time. * @private */ tickCumulativeMetrics_() { if (this.supportsLayoutShift_) { this.recordGoogleFontExp_(); this.tickCumulativeLayoutShiftScore_(); } } /** * Tick the layout shift metric, following the latest CWV standard. This uses * a 5s maximum window with a 1s gap between events to define a "session". * We report the maximum session value over the lifetime of the page as the CLS. * * @param {!LayoutShift} entry */ tickLayoutShiftScore_(entry) { if (!this.ampdoc_) { return; } if (this.isVisibilityHidden_()) { return; } const entries = this.layoutShiftEntries_; if (entries.length > 0) { const first = entries[0]; const last = entries[entries.length - 1]; if ( entry.startTime - last.startTime < CLS_SESSION_GAP && entry.startTime - first.startTime < CLS_SESSION_MAX ) { // This entry continues the current CLS window. entries.push(entry); return; } // This entry is the start of a new CLS window, but we haven't flushed the old value yet. this.flushLayoutShiftScore_(); } entries.push(entry); // Ensure we report the CLS when the session closes. We're not guaranteed // to get more LayoutShift entries, so we need some setTimeout magic to // ensure it happens. this.debouncedFlushLayoutShiftScore_(); } /** * Records the normalized CLS score, following the latest CWV standard. * See https://web.dev/evolving-cls/ */ flushLayoutShiftScore_() { const entries = this.layoutShiftEntries_; const old = this.metrics_.get(TickLabel_Enum.CUMULATIVE_LAYOUT_SHIFT); let union = 0; let sum = 0; for (const entry of entries) { if (entry.sources) { for (const source of entry.sources) { union |= getElementType(source.node); } } sum += entry.value; } entries.length = 0; this.recordGoogleFontExp_(); if (old == null || sum > old) { // We'll record the largest windowed CLS. this.metrics_.reset(TickLabel_Enum.CUMULATIVE_LAYOUT_SHIFT); this.metrics_.reset(TickLabel_Enum.CUMULATIVE_LAYOUT_SHIFT_TYPE_UNION); this.tickDelta(TickLabel_Enum.CUMULATIVE_LAYOUT_SHIFT, sum); this.tickDelta(TickLabel_Enum.CUMULATIVE_LAYOUT_SHIFT_TYPE_UNION, union); this.flush(); } } /** * Record the interaction to next paint score. * @param {number=} duration */ tickInteractionToNextPaint_(duration) { if (!this.ampdoc_) { return; } const old = this.metrics_.get(TickLabel_Enum.INTERACTION_TO_NEXT_PAINT); if (old == null || duration > old) { this.tickDelta( TickLabel_Enum.INTERACTION_TO_NEXT_PAINT, duration - (old ?? 0) ); this.flush(); } } /** * Tick the layout shift score metric. * * A value of the metric is recorded in under two names, `cls-1` and `cls-2`, * for the first two times the page transitions into a hidden lifecycle state * (when the page is navigated a way from, the tab is backgrounded for * another tab, or the user backgrounds the browser application). * * Since we can't reliably detect when a page session finally ends, * recording the value for these first two events should provide a fair * amount of visibility into this metric. */ tickCumulativeLayoutShiftScore_() { if (this.shiftScoresTicked_ === 0) { this.tickDelta( TickLabel_Enum.CUMULATIVE_LAYOUT_SHIFT_1, this.layoutShiftSum_ ); this.flush(); this.shiftScoresTicked_ = 1; } else if (this.shiftScoresTicked_ === 1) { this.tickDelta( TickLabel_Enum.CUMULATIVE_LAYOUT_SHIFT_2, this.layoutShiftSum_ ); this.flush(); this.shiftScoresTicked_ = 2; } } /** * Tick the largest contentful paint metrics. * Uses entry.startTime, which equates to: `renderTime ?? loadTime`. We can't * always use one or the other because: * - loadTime is 0 for non-remote resources (text) * - renderTime is undefined for crossorigin resources * * @param {!LargestContentfulPaint} entry */ tickLargestContentfulPaint_(entry) { const {element, startTime} = entry; const type = getElementType(element); this.tickDelta(TickLabel_Enum.LARGEST_CONTENTFUL_PAINT_TYPE, type); this.tickDelta(TickLabel_Enum.LARGEST_CONTENTFUL_PAINT, startTime); this.tickSinceVisible( TickLabel_Enum.LARGEST_CONTENTFUL_PAINT_VISIBLE, startTime ); this.flush(); } /** * Measure the delay the user perceives of how long it takes * to load the initial viewport. * @private */ measureUserPerceivedVisualCompletenessTime_() { const didStartInPrerender = !this.ampdoc_.hasBeenVisible(); let docVisibleTime = -1; this.ampdoc_.whenFirstVisible().then(() => { docVisibleTime = this.win.performance.now(); // Mark this first visible instance in the browser timeline. this.mark('visible'); }); this.whenViewportLayoutComplete_().then(() => { if (didStartInPrerender) { const userPerceivedVisualCompletenesssTime = docVisibleTime > -1 ? this.win.performance.now() - docVisibleTime : // Prerender was complete before visibility. 0; this.ampdoc_.whenFirstVisible().then(() => { // We only tick this if the page eventually becomes visible, // since otherwise we heavily skew the metric towards the // 0 case, since pre-renders that are never used are highly // likely to fully load before they are never used :) this.tickDelta( TickLabel_Enum.FIRST_VIEWPORT_READY, userPerceivedVisualCompletenesssTime ); }); this.prerenderComplete_(userPerceivedVisualCompletenesssTime); // Mark this instance in the browser timeline. this.mark(TickLabel_Enum.FIRST_VIEWPORT_READY); } else { // If it didnt start in prerender, no need to calculate anything // and we just need to tick `pc`. (it will give us the relative // time since the viewer initialized the timer) this.tick(TickLabel_Enum.FIRST_VIEWPORT_READY); this.prerenderComplete_(this.win.performance.now() - docVisibleTime); } this.flush(); }); } /** * Returns a promise that is resolved when resources in viewport * have been finished being laid out. * @return {!Promise} * @private */ whenViewportLayoutComplete_() { return this.resources_.whenFirstPass().then(() => { const {documentElement} = this.win.document; const size = Services.viewportForDoc(documentElement).getSize(); const rect = layoutRectLtwh(0, 0, size.width, size.height); return whenContentIniLoad( documentElement, this.win, rect, /* isInPrerender */ true ); }); } /** * Ticks a timing event. * * @param {TickLabel_Enum} label The variable name as it will be reported. * See TICKEVENTS.md for available metrics, and edit this file * when adding a new metric. * @param {number=} opt_delta The delta. Call tickDelta instead of setting * this directly. * @param {number=} opt_value The value to use. Overrides default calculation. */ tick(label, opt_delta, opt_value) { devAssert( opt_delta == undefined || opt_value == undefined, 'You may not set both opt_delta and opt_value.' ); const data = {'label': label}; let delta; if (opt_delta != undefined) { data['delta'] = delta = Math.max(opt_delta, 0); } else if (opt_value != undefined) { data['value'] = opt_value; } else { // Marking only makes sense for non-overridden values (and no deltas). this.mark(label); delta = this.win.performance.now(); data['value'] = this.timeOrigin_ + delta; } // Emit events. Used by `amp performance`. this.win.dispatchEvent( createCustomEvent( this.win, 'perf', /** @type {JsonObject} */ ({label, delta}) ) ); if (this.isMessagingReady_ && this.isPerformanceTrackingOn_) { this.viewer_.sendMessage('tick', data); } else { this.queueTick_(data); } this.metrics_.signal(label, delta); } /** * Add browser performance timeline entries for simple ticks. * These are for example exposed in WPT. * See https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark * @param {string} label */ mark(label) { this.win.performance.mark?.(label); } /** * Tick a very specific value for the label. Use this method if you * measure the time it took to do something yourself. * @param {TickLabel_Enum} label The variable name as it will be reported. * @param {number} value The value in milliseconds that should be ticked. */ tickDelta(label, value) { this.tick(label, value); } /** * Tick time delta since the document has become visible. * @param {TickLabel_Enum} label The variable name as it will be reported. * @param {number=} opt_delta The optional delta value in milliseconds. */ tickSinceVisible(label, opt_delta) { const delta = opt_delta == undefined ? this.win.performance.now() : opt_delta; const end = this.timeOrigin_ + delta; // If on Origin, use timeOrigin // If in a viewer, use firstVisibleTime const visibleTime = this.viewer_?.isEmbedded() ? this.ampdoc_?.getFirstVisibleTime() : this.timeOrigin_; const v = visibleTime ? Math.max(end - visibleTime, 0) : 0; this.tickDelta(label, v); } /** * Ask the viewer to flush the ticks */ flush() { if (this.isMessagingReady_ && this.isPerformanceTrackingOn_) { if (this.ampexp_ == null) { this.ampexp_ = Object.keys(this.enabledExperiments_).join(','); } this.viewer_.sendMessage( 'sendCsi', { 'ampexp': this.ampexp_, 'canonicalUrl': this.documentInfo_.canonicalUrl, 'eventid': this.eventid_, }, /* cancelUnsent */ true ); } } /** * @param {string} experimentId */ addEnabledExperiment(experimentId) { this.enabledExperiments_[experimentId] = true; this.ampexp_ = undefined; } /** * Queues the events to be flushed when tick function is set. * * @param {TickEventDef} data Tick data to be queued. * @private */ queueTick_(data) { // Start dropping the head of the queue if we've reached the limit // so that we don't take up too much memory in the runtime. if (this.events_.length >= QUEUE_LIMIT) { this.events_.shift(); } this.events_.push(data); } /** * Forwards all queued ticks to the viewer tick method. * @private */ flushQueuedTicks_() { if (!this.viewer_) { return; } if (!this.isPerformanceTrackingOn_) { // drop all queued ticks to not leak this.events_.length = 0; return; } this.events_.forEach((tickEvent) => { this.viewer_.sendMessage('tick', tickEvent); }); this.events_.length = 0; } /** * @private * @param {number} value */ prerenderComplete_(value) { if (this.viewer_) { this.viewer_.sendMessage( 'prerenderComplete', {'value': value}, /* cancelUnsent */ true ); } } /** * Identifies if the viewer is able to track performance. If the document is * not embedded, there is no messaging channel, so no performance tracking is * needed since there is nobody to forward the events. * @return {boolean} */ isPerformanceTrackingOn() { return this.isPerformanceTrackingOn_; } /** * Retrieve a promise for tick label, resolved with metric. Used by amp-analytics * * @param {TickLabel_Enum} label * @return {!Promise<time>} */ getMetric(label) { return this.metrics_.whenSignal(label); } } /** * Traverse node ancestors and return the highest level amp element. * Returns the given node if none are found. * * @param {!Node} node * @return {!Node} */ function getOutermostAmpElement(node) { let max = node; while ((node = node.parentNode) != null) { if (node.nodeName.startsWith('AMP-')) { max = node; } } return max; } /** * @param {!Window} window */ export function installPerformanceService(window) { registerServiceBuilder(window, 'performance', Performance); } /** * @param {!Window} window * @return {!Performance} */ export function performanceFor(window) { return getService(window, 'performance'); }