You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2020/08/26 02:53:59 UTC

[skywalking-client-js] 39/48: feat: add fmp metric

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 <fi...@outlook.com>
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();