This is an automated email from the ASF dual-hosted git repository. wusheng pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/skywalking-client-js.git
commit 6c3818ab309c8ea7e4d037fedc7bf21241850b45 Author: Qiuxia Fan <[email protected]> AuthorDate: Wed Aug 5 18:52:59 2020 +0800 feat: add fmp metric --- src/performance/fmp.ts | 321 ++++++++++++++++++++++++++++++++++++++++++++++++ src/performance/perf.ts | 17 ++- 2 files changed, 334 insertions(+), 4 deletions(-) diff --git a/src/performance/fmp.ts b/src/performance/fmp.ts new file mode 100644 index 0000000..08e1bd1 --- /dev/null +++ b/src/performance/fmp.ts @@ -0,0 +1,321 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +function getStyle(element: Element | any, attr: any) { + if (window.getComputedStyle) { + return window.getComputedStyle(element, null)[attr]; + } else { + return element.currentStyle[attr]; + } +} + +const START_TIME: number = performance.now(); +const IGNORE_TAG_SET: string[] = ['SCRIPT', 'STYLE', 'META', 'HEAD', 'LINK']; + +enum ELE_WEIGHT { + SVG = 2, + IMG = 2, + CANVAS = 4, + OBJECT = 4, + EMBED = 4, + VIDEO = 4, +} + +const LIMIT: number = 3000; +const WW: number = window.innerWidth; +const WH: number = window.innerHeight; +const DELAY: number = 500; +interface ICalScore { + dpss: ICalScore[]; + st: number; + els: Els; + root?: Element; +} +type Els = Array<{ + $node: Element; + st: number; + weight: number; +}>; + +class FMPTiming { + private statusCollector: Array<{time: number}> = []; + private flag: boolean = true; + private observer: MutationObserver = null; + private callbackCount: number = 0; + private entries: any = {}; + private fmpCallback: any = null; + constructor(fmpCallback?: (res: any) => void) { + if (fmpCallback) { + this.fmpCallback = fmpCallback; + } + this.initObserver(); + } + private getFirstSnapShot(): void { + const time: number = performance.now(); + const $body: HTMLElement = document.body; + if ($body) { + this.setTag($body, this.callbackCount); + } + this.statusCollector.push({ + time, + }); + } + private initObserver() { + this.getFirstSnapShot(); + this.observer = new MutationObserver(() => { + this.callbackCount += 1; + const time = performance.now(); + const $body: HTMLElement = document.body; + if ($body) { + this.setTag($body, this.callbackCount); + } + this.statusCollector.push({ + time, + }); + }); + this.observer.observe(document, { + childList: true, + subtree: true, + }); + if (document.readyState === 'complete') { + this.calculateFinalScore(); + } else { + window.addEventListener('load', () => { + this.calculateFinalScore(); + }, false); + } + } + private calculateFinalScore() { + if (MutationEvent && this.flag) { + if (this.checkNeedCancel(START_TIME)) { + this.observer.disconnect(); + this.flag = false; + const res = this.getTreeScore(document.body); + let tp: ICalScore = null; + res.dpss.forEach((item: any) => { + if (tp && tp.st) { + if (tp.st < item.st) { + tp = item; + } + } else { + tp = item; + } + }); + performance.getEntries().forEach((item: PerformanceResourceTiming) => { + this.entries[item.name] = item.responseEnd; + }); + if (!tp) { + if (this.fmpCallback) { + this.fmpCallback({ + tp: null, + resultEls: [], + fmpTiming: 0, + }); + } + return false; + } + const resultEls: Els = this.filterResult(tp.els); + const fmpTiming: number = this.getFmpTime(resultEls); + if (this.fmpCallback) { + this.fmpCallback({ + tp, + resultEls, + fmpTiming, + }); + } + } else { + setTimeout(() => { + this.calculateFinalScore(); + }, DELAY); + } + } + } + private getFmpTime(resultEls: Els): number { + let rt = 0; + resultEls.forEach((item: any) => { + let time: number = 0; + if (item.weight === 1) { + const index: number = parseInt(item.$node.getAttribute('fmp_c'), 10); + time = this.statusCollector[index].time; + } else if (item.weight === 2) { + if (item.$node.tagName === 'IMG') { + time = this.entries[(item.$node as HTMLImageElement).src]; + } else if (item.$node.tagName === 'SVG') { + const index: number = parseInt(item.$node.getAttribute('fmp_c'), 10); + time = this.statusCollector[index].time; + } else { + const match = getStyle(item.$node, 'background-image').match(/url\(\"(.*?)\"\)/); + let url: string; + if (match && match[1]) { + url = match[1]; + } + if (url.indexOf('http') === -1) { + url = location.protocol + match[1]; + } + time = this.entries[url]; + } + } else if (item.weight === 4) { + if (item.$node.tagName === 'CANVAS') { + const index: number = parseInt(item.$node.getAttribute('fmp_c'), 10); + time = this.statusCollector[index].time; + } else if (item.$node.tagName === 'VIDEO') { + time = this.entries[(item.$node as HTMLVideoElement).src]; + if (!time) { + time = this.entries[(item.$node as HTMLVideoElement).poster]; + } + } + } + if (typeof time !== 'number') { + time = 0; + } + if (rt < time) { + rt = time; + } + }); + return rt; + } + private filterResult(els: Els): Els { + if (els.length === 1) { + return els; + } + let sum: number = 0; + els.forEach((item: any) => { + sum += item.st; + }); + const avg: number = sum / els.length; + return els.filter((item: any) => { + return item.st > avg; + }); + } + private checkNeedCancel(start: number): boolean { + const time: number = performance.now() - start; + const lastCalTime: number = this.statusCollector.length > 0 + ? this.statusCollector[this.statusCollector.length - 1].time + : 0; + return time > LIMIT || (time - lastCalTime > 1000); + } + private getTreeScore(node: Element): ICalScore | any { + if (!node) { + return {}; + } + const dpss = []; + const children: any = node.children; + for (const child of children) { + if (!child.getAttribute('fmp_c')) { + continue; + } + const s = this.getTreeScore(child); + if (s.st) { + dpss.push(s); + } + } + + return this.calcaulteScore(node, dpss); + } + private calcaulteScore($node: Element, dpss: ICalScore[]): ICalScore { + const { + width, + height, + left, + top, + } = $node.getBoundingClientRect(); + let isInViewPort: boolean = true; + if (WH < top || WW < left) { + isInViewPort = false; + } + + let sdp: number = 0; + dpss.forEach((item: any) => { + sdp += item.st; + }); + + let weight: number = Number(ELE_WEIGHT[$node.tagName as any]) || 1; + if (weight === 1 + && getStyle($node, 'background-image') + && getStyle($node, 'background-image') !== 'initial' + && getStyle($node, 'background-image') !== 'none') { + weight = ELE_WEIGHT.IMG; + } + let st: number = isInViewPort ? width * height * weight : 0; + let els = [{ $node, st, weight }]; + const root = $node; + const areaPercent = this.calculateAreaParent($node); + if (sdp > st * areaPercent || areaPercent === 0) { + st = sdp; + els = []; + dpss.forEach((item: any) => { + els = els.concat(item.els); + }); + } + return { + dpss, + st, + els, + root, + }; + } + private calculateAreaParent($node: Element): number { + const { + left, + right, + top, + bottom, + width, + height, + } = $node.getBoundingClientRect(); + + const winLeft: number = 0; + const winTop: number = 0; + const winRight: number = WW; + const winBottom: number = WH; + + const overlapX = (right - left) + (winRight - winLeft) - (Math.max(right, winRight) - Math.min(left, winLeft)); + const overlapY = (bottom - top) + (winBottom - winTop) - (Math.max(bottom, winBottom) - Math.min(top, winTop)); + if (overlapX <= 0 || overlapY <= 0) { + return 0; + } + return (overlapX * overlapY) / (width * height); + } + private setTag(target: Element, callbackCount: number): void { + const tagName: string = target.tagName; + if (IGNORE_TAG_SET.indexOf(tagName) === -1) { + const $children: HTMLCollection = target.children; + if ($children && $children.length > 0) { + for (let i = $children.length - 1; i >= 0; i--) { + const $child: Element = $children[i]; + const hasSetTag = $child.getAttribute('fmp_c') !== null; + if (!hasSetTag) { + const { + left, + top, + width, + height, + } = $child.getBoundingClientRect(); + if ( + WH < top || WW < left || width === 0 || height === 0 + ) { + continue; + } + $child.setAttribute('fmp_c', `${callbackCount}`); + } + this.setTag($child, callbackCount); + } + } + } + } +} + +export default FMPTiming; diff --git a/src/performance/perf.ts b/src/performance/perf.ts index 0ea7c84..2280447 100644 --- a/src/performance/perf.ts +++ b/src/performance/perf.ts @@ -16,7 +16,13 @@ * limitations under the License. */ +import FMP from './fmp'; class PagePerf { + private fmpTime: number = 0; + + constructor() { + new FMP(this.getFmpTiming); + } public getPerfTiming() { try { @@ -31,7 +37,7 @@ class PagePerf { if (loadTime < 0) { setTimeout(() => { this.getPerfTiming(); - }, 300); + }, 3000); return; } @@ -42,13 +48,10 @@ class PagePerf { } else { redirectTime = 0; } - return { redirectTime, dnsTime: timing.domainLookupEnd - timing.domainLookupStart, ttfbTime: timing.responseStart - timing.requestStart, // Time to First Byte - // appcacheTime: timing.domainLookupStart - timing.fetchStart, - // unloadTime: timing.unloadEventEnd - timing.unloadEventStart, tcpTime: timing.connectEnd - timing.connectStart, transTime: timing.responseEnd - timing.responseStart, domAnalysisTime: timing.domInteractive - timing.responseEnd, @@ -59,11 +62,17 @@ class PagePerf { sslTime: timing.connectEnd - timing.secureConnectionStart, // Only valid for HTTPS ttlTime: timing.domInteractive - timing.fetchStart, // time to interact firstPackTime: timing.responseStart - timing.domainLookupStart, // first pack time + fmpTime: this.fmpTime, // First Meaningful Paint }; } catch (e) { throw e; } } + + private getFmpTiming(data: any) { + console.log(data); + this.fmpTime = data.fmpTiming; + } } export default new PagePerf();
