Title: [248305] trunk/Tools
Revision
248305
Author
commit-qu...@webkit.org
Date
2019-08-06 10:56:57 -0700 (Tue, 06 Aug 2019)

Log Message

[results.webkit.org Timeline] Performance improvements
https://bugs.webkit.org/show_bug.cgi?id=200406

Patch by Zhifei Fang <zhifei_f...@apple.com> on 2019-08-06
Reviewed by Jonathan Bedard.

1. Unhook the scroll event when a series/axis have been removed from the container
2. Fix the axis's cache data structure out of sync.
3. Use position:sticky to reduce the scrolling blink when update the presenter's transform
4. Use intersection observer to detect if the canvas on screen or not, if a canvas is not on the screen, we do nothing, this will eliminate render requests we send out.

* resultsdbpy/resultsdbpy/view/static/library/js/Ref.js:
(Signal.prototype.removeListener):
(prototype.stopAction): Unregsiter an action handler
(Ref):
(Ref.prototype.apply):
(Ref.prototype.destory):
* resultsdbpy/resultsdbpy/view/static/library/js/components/BaseComponents.js:
(ApplyNewChildren):
* resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js:
(Timeline.CanvasSeriesComponent):

Modified Paths

Diff

Modified: trunk/Tools/ChangeLog (248304 => 248305)


--- trunk/Tools/ChangeLog	2019-08-06 17:37:39 UTC (rev 248304)
+++ trunk/Tools/ChangeLog	2019-08-06 17:56:57 UTC (rev 248305)
@@ -1,3 +1,27 @@
+2019-08-06  Zhifei Fang  <zhifei_f...@apple.com>
+
+        [results.webkit.org Timeline] Performance improvements
+        https://bugs.webkit.org/show_bug.cgi?id=200406
+
+        Reviewed by Jonathan Bedard.
+
+        1. Unhook the scroll event when a series/axis have been removed from the container 
+        2. Fix the axis's cache data structure out of sync.
+        3. Use position:sticky to reduce the scrolling blink when update the presenter's transform
+        4. Use intersection observer to detect if the canvas on screen or not, if a canvas is not on the screen, we do nothing, this will eliminate render requests we send out.
+
+
+        * resultsdbpy/resultsdbpy/view/static/library/js/Ref.js:
+        (Signal.prototype.removeListener):
+        (prototype.stopAction): Unregsiter an action handler
+        (Ref):
+        (Ref.prototype.apply):
+        (Ref.prototype.destory):
+        * resultsdbpy/resultsdbpy/view/static/library/js/components/BaseComponents.js:
+        (ApplyNewChildren):
+        * resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js:
+        (Timeline.CanvasSeriesComponent):
+
 2019-08-06  Jer Noble  <jer.no...@apple.com>
 
         Add test for behavior introduced in r248174

Modified: trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/Ref.js (248304 => 248305)


--- trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/Ref.js	2019-08-06 17:37:39 UTC (rev 248304)
+++ trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/Ref.js	2019-08-06 17:56:57 UTC (rev 248305)
@@ -39,12 +39,12 @@
         return this;
     }
     removeListener(fn) {
-        const removeIndex = 0;
+        let removeIndex = 0;
         this.handlers.forEach((handler, index) => {
             if (handler === fn)
                 removeIndex = index;
         });
-        this.handler.splice(removeIndex, 1);
+        this.handlers.splice(removeIndex, 1);
         return this;
     }
     removeAllListener() {
@@ -90,6 +90,10 @@
         this.signal.addListener(fn);
         return this;
     }
+    stopAction(fn) {
+        this.signal.removeListener(fn);
+        return this;
+    }
     error(fn) {
         this.errorSignal.addListener(fn);
         return this;
@@ -243,7 +247,7 @@
         this.state = config.state !== undefined ? config.state : {};
         this._onStateUpdate_ = new Signal().addListener(config.onStateUpdate);
         this._onElementMount_ = new Signal().addListener(config.onElementMount);
-        this._onElementUnmout_ = new Signal().addListener(config.onElementUnmout);
+        this._onElementUnmount_ = new Signal().addListener(config.onElementUnmount);
     }
     bind(element) {
         this.apply(element.querySelector(`[ref="${this.key}"]`));
@@ -255,7 +259,7 @@
         if (this.element === element)
             return;
         if (this.element && this.element !== element)
-            this.onElementUnmout.emit(this.element);
+            this.onElementUnmount.emit(this.element);
         this.element = element;
         //Initial state
         if (this.state !== undefined && this.state !== null)
@@ -265,7 +269,7 @@
     destory(element) {
         if (!element || element !== this.element)
             return;
-        this.onElementUnmout.emit(this.element);
+        this.onElementUnmount.emit(this.element);
     }
     toString() {
         return this.key;

Modified: trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/Utils.js (248304 => 248305)


--- trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/Utils.js	2019-08-06 17:37:39 UTC (rev 248304)
+++ trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/Utils.js	2019-08-06 17:56:57 UTC (rev 248305)
@@ -65,4 +65,21 @@
     const match = window.matchMedia('(prefers-color-scheme: dark)');
     return match.matches;
 }
-export {timeDifference, isDarkMode, Cookie};
+
+// Uses intersection observer: <https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API>
+function createInsertionObservers(element, callback=()=>{}, startThreshold=0.0, endTreshold=1.0, step=0.1, option={}) {
+    const useOption = {};
+    useOption.root = option.root instanceof HTMLElement ? option.root : null;
+    useOption.rootMargin = option.rootMargin ? option.rootMargin : "0";
+    const thresholdArray = [];
+    for (let i = startThreshold; i <= endTreshold; i+= step) {
+        thresholdArray.push(i);
+    }
+    thresholdArray.forEach(threshold => {
+        useOption.threshold = threshold;
+        const observer = new IntersectionObserver(callback, useOption);
+        observer.observe(element);
+    });
+}
+
+export {timeDifference, isDarkMode, Cookie, createInsertionObservers};

Modified: trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/BaseComponents.js (248304 => 248305)


--- trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/BaseComponents.js	2019-08-06 17:37:39 UTC (rev 248304)
+++ trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/BaseComponents.js	2019-08-06 17:56:57 UTC (rev 248305)
@@ -42,10 +42,10 @@
     newChildren.forEach((child, index) => {
         if (index < element.children.length) {
             if (child !== element.children[index]) {
-                if (child instanceof HTMLElement)
-                    element.replaceChild(child, element.children[index]);
-                else
-                    DOM.replace(element.children[index], typeof itemProcessor === "function" ? itemProcessor(child) : child);
+                if (child instanceof HTMLElement) {
+                    element.children[index].before(child);
+                } else
+                    DOM.before(element.children[index], typeof itemProcessor === "function" ? itemProcessor(child) : child);
             }
         } else {
             if (child instanceof HTMLElement)

Modified: trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js (248304 => 248305)


--- trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js	2019-08-06 17:37:39 UTC (rev 248304)
+++ trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js	2019-08-06 17:56:57 UTC (rev 248305)
@@ -25,7 +25,7 @@
 }
 from '../Ref.js';
 
-import {isDarkMode} from '../Utils.js';
+import {isDarkMode, createInsertionObservers} from '../Utils.js';
 import {ListComponent, ListProvider, ListProviderReceiver} from './BaseComponents.js'
 
 function pointCircleCollisionDetact(point, circle) {
@@ -92,8 +92,6 @@
             resizeEventStream.add(element.offsetWidth);
         },
         onStateUpdate: (element, stateDiff, state) => {
-            if (typeof stateDiff.scrollLeft === 'number')
-                element.style.transform = `translate(${stateDiff.scrollLeft}px, 0)`;
             if (stateDiff.resize) {
                 element.style.width = `${element.parentElement.parentElement.offsetWidth}px`;
                 resizeEventStream.add(element.offsetWidth);
@@ -100,15 +98,11 @@
             }
         }
     });
-    scrollEventStream.action((e) => {
-        // Provide the logic scrollLeft
-        presenterRef.setState({scrollLeft: e.target.scrollLeft});
-    });
     // Provide parent functions/event to children to use
 
     return `<div class="content" ref="${scrollRef}">
-        <div ref="${containerRef}">
-            <div ref="${presenterRef}">${
+        <div ref="${containerRef}" style="position: relative">
+            <div ref="${presenterRef}" style="position: -webkit-sticky; position:sticky; top:0; left: 0">${
                 ListProvider((updateChildrenFunctions) => {
                     if (exporter) {
                         exporter((children) => {
@@ -127,11 +121,15 @@
 function offscreenCachedRenderFactory(padding, height) {
     let cachedScrollLeft = 0;
     let offscreenCanvas = document.createElement('canvas');
+    // Double buffering
+    const offscreenCanvasBuffer = document.createElement('canvas');
 
     // This function will call redrawCache to render a offscreen cache
     // and copy the viewport area from of it
     // It will trigger redrawCache when cache don't have enough space
     return (redrawCache, element, stateDiff, state, forceRedrawCache = false) => {
+        // Check if the canvas display on the screen or not,
+        // This will save render time
         const width = typeof stateDiff.width === 'number' ? stateDiff.width : state.width;
         if (width <= 0)
             // Nothing to render
@@ -155,7 +153,7 @@
 
         if (needToRedrawCache) {
             // We draw everything on cache
-            redrawCache(offscreenCanvas, element, stateDiff, state, () => {
+             redrawCache(offscreenCanvas, element, stateDiff, state, () => {
                 cachedScrollLeft = scrollLeft < padding ? scrollLeft : scrollLeft - padding;
                 cachePosLeft = scrollLeft - cachedScrollLeft;
                 if (cachePosLeft < 0)
@@ -331,50 +329,74 @@
         }
     };
 
-    const canvasRef = REF.createRef({
-        state: {
-            dots: initDots,
-            scales: initScales,
-            scrollLeft: 0,
-            width: 0,
-        },
-        onElementMount: (element) => {
-            setupCanvasHeightWithDpr(element, height);
-            setupCanvasContextScale(element);
-            if (onDotClick) {
-                element.addEventListener('click', (e) => {
-                    let dots = getMouseEventTirggerDots(e, canvasRef.state.scrollLeft, element);
-                    if (dots.length)
-                        onDotClick(dots[0], e);
-                });
-            }
+    return ListProviderReceiver((updateContainerWidth, onContainerScroll, onResize) => {
+        const _onScrollAction_ = (e) => {
+            canvasRef.setState({scrollLeft: e.target.scrollLeft / getDevicePixelRatio()});
+        };
+        const _onResizeAction_ = (width) => {
+            canvasRef.setState({width: width});
+        };
 
-            if (onDotClick || onDotHover) {
-                element.addEventListener('mousemove', (e) => {
-                    let dots = getMouseEventTirggerDots(e, canvasRef.state.scrollLeft, element);
-                    if (dots.length) {
-                        if (onDotHover)
-                            onDotHover(dots[0], e);
-                        element.style.cursor = "pointer";
-                    } else
-                        element.style.cursor = "default";
-                });
-            }
-        },
-        onStateUpdate: (element, stateDiff, state) => {
-            const context = element.getContext("2d");
-            let forceRedrawCache = false;
-            if (stateDiff.scales || stateDiff.dots || typeof stateDiff.scrollLeft === 'number' || typeof stateDiff.width === 'number') {
-                console.assert(dots.length <= scales.length);
-                if (stateDiff.scales || stateDiff.dots) {
-                    forceRedrawCache = true;
+        const canvasRef = REF.createRef({
+            state: {
+                dots: initDots,
+                scales: initScales,
+                scrollLeft: 0,
+                width: 0,
+                onScreen: true,
+            },
+            onElementMount: (element) => {
+                setupCanvasHeightWithDpr(element, height);
+                setupCanvasContextScale(element);
+                if (onDotClick) {
+                    element.addEventListener('click', (e) => {
+                        let dots = getMouseEventTirggerDots(e, canvasRef.state.scrollLeft, element);
+                        if (dots.length)
+                            onDotClick(dots[0], e);
+                    });
                 }
-                requestAnimationFrame(() => offscreenCachedRender(redrawCache, element, stateDiff, state, forceRedrawCache));
+
+                if (onDotClick || onDotHover) {
+                    element.addEventListener('mousemove', (e) => {
+                        let dots = getMouseEventTirggerDots(e, canvasRef.state.scrollLeft, element);
+                        if (dots.length) {
+                            if (onDotHover)
+                                onDotHover(dots[0], e);
+                            element.style.cursor = "pointer";
+                        } else
+                            element.style.cursor = "default";
+                    });
+                }
+
+                createInsertionObservers(element, (entries) => {
+                    canvasRef.setState({onScreen: entries[0].isIntersecting});
+                }, 0, 0.01, 0.01);
+            },
+            onElementUnmount: (element) => {
+                onContainerScroll.stopAction(onScrollAction);
+                onResize.stopAction(onResizeAction);
+            },
+            onStateUpdate: (element, stateDiff, state) => {
+                const context = element.getContext("2d");
+                let forceRedrawCache = false;
+                if (!state.onScreen && !stateDiff.onScreen)
+                    return;
+
+                if (stateDiff.scales || stateDiff.dots || typeof stateDiff.scrollLeft === 'number' || typeof stateDiff.width === 'number' || stateDiff.onScreen) {
+
+                    if (stateDiff.scales) {
+                        stateDiff.scales = stateDiff.scales.map(x => x);
+                        forceRedrawCache = true;
+                    }
+                    if (stateDiff.dots) {
+                        stateDiff.dots = stateDiff.dots.map(x => x);
+                        forceRedrawCache = true;
+                    }
+                    requestAnimationFrame(() => offscreenCachedRender(redrawCache, element, stateDiff, state, forceRedrawCache));
+                }
             }
-        }
-    });
+        });
 
-    return ListProviderReceiver((updateContainerWidth, onContainerScroll, onResize) => {
         updateContainerWidth(scales.length * dotWidth * getDevicePixelRatio());
         const updateData = (dots, scales) => {
             updateContainerWidth(scales.length * dotWidth * getDevicePixelRatio());
@@ -385,12 +407,8 @@
         };
         if (typeof option.exporter === "function")
             option.exporter(updateData);
-        onContainerScroll.action((e) => {
-            canvasRef.setState({scrollLeft: e.target.scrollLeft / getDevicePixelRatio()});
-        });
-        onResize.action((width) => {
-            canvasRef.setState({width: width});
-        });
+        onContainerScroll.action(onScrollAction);
+        onResize.action(onResizeAction);
         return `<div class="series">
             <canvas ref="${canvasRef}">
         </div>`;
@@ -683,66 +701,78 @@
     };
     const initScaleGroupMapLinkList = getScalesMapLinkList(initScales);
 
-    const canvasRef = REF.createRef({
-        state: {
-            scrollLeft: 0,
-            width: 0,
-            scales: initScales,
-            scalesMapLinkList: initScaleGroupMapLinkList
-        },
-        onElementMount: (element) => {
-            setupCanvasHeightWithDpr(element, canvasHeight);
-            setupCanvasContextScale(element);
-            if (onScaleClick) {
-                element.addEventListener('click', (e) => {
-                    let scales = getMouseEventTirggerScales(e, canvasRef.state.scrollLeft, element);
-                    if (scales.length)
-                        onScaleClick(scales[0], e);
-                });
-            }
 
-            if (onScaleClick || onScaleHover) {
-                element.addEventListener('mousemove', (e) => {
-                    let scales = getMouseEventTirggerScales(e, canvasRef.state.scrollLeft, element);
-                    if (scales.length) {
-                        if (onScaleHover)
-                            onScaleHover(scales[0], e);
-                        element.style.cursor = "pointer";
-                    } else {
-                        element.style.cursor = "default";
+    return {
+        series: ListProviderReceiver((updateContainerWidth, onContainerScroll, onResize) => {
+            const _onScrollAction_ = (e) => {
+                canvasRef.setState({scrollLeft: e.target.scrollLeft / getDevicePixelRatio()});
+            };
+            const _onResizeAction_ = (width) => {
+                canvasRef.setState({width: width});
+            };
+
+            const canvasRef = REF.createRef({
+                state: {
+                    scrollLeft: 0,
+                    width: 0,
+                    scales: initScales,
+                    scalesMapLinkList: initScaleGroupMapLinkList
+                },
+                onElementMount: (element) => {
+                    setupCanvasHeightWithDpr(element, canvasHeight);
+                    setupCanvasContextScale(element);
+                    if (onScaleClick) {
+                        element.addEventListener('click', (e) => {
+                            let scales = getMouseEventTirggerScales(e, canvasRef.state.scrollLeft, element);
+                            if (scales.length)
+                                onScaleClick(scales[0], e);
+                        });
                     }
-                });
-            }
-        },
-        onStateUpdate: (element, stateDiff, state) => {
-            let forceRedrawCache = false;
-            if (stateDiff.scales || typeof stateDiff.scrollLeft === 'number' || typeof stateDiff.width === 'number') {
-                if (stateDiff.scales) {
-                    state.scalesMapLinkList = getScalesMapLinkList(stateDiff.scales);
-                    forceRedrawCache = true;
+
+                    if (onScaleClick || onScaleHover) {
+                        element.addEventListener('mousemove', (e) => {
+                            let scales = getMouseEventTirggerScales(e, canvasRef.state.scrollLeft, element);
+                            if (scales.length) {
+                                if (onScaleHover)
+                                    onScaleHover(scales[0], e);
+                                element.style.cursor = "pointer";
+                            } else {
+                                element.style.cursor = "default";
+                            }
+                        });
+                    }
+                },
+                onElementUnmount: (element) => {
+                    onContainerScroll.stopAction(onScrollAction);
+                    onResize.stopAction(onResizeAction);
+                },
+                onStateUpdate: (element, stateDiff, state) => {
+                    let forceRedrawCache = false;
+                    if (stateDiff.scales || typeof stateDiff.scrollLeft === 'number' || typeof stateDiff.width === 'number') {
+                        if (stateDiff.scales)
+                            forceRedrawCache = true;
+                        requestAnimationFrame(() => {
+                            offscreenCachedRender(redrawCache, element, stateDiff, state, forceRedrawCache)
+                        });
+                    }
                 }
-                requestAnimationFrame(() => offscreenCachedRender(redrawCache, element, stateDiff, state, forceRedrawCache));
-            }
-        }
-    });
+            });
 
-    return {
-        series: ListProviderReceiver((updateContainerWidth, onContainerScroll, onResize) => {
+
             updateContainerWidth(scales.length * scaleWidth * getDevicePixelRatio());
             const updateData = (scales) => {
-                updateContainerWidth(scales.length * scaleWidth * getDevicePixelRatio());
+                // In case of modification while rendering
+                const scalesCopy = scales.map(x => x);
+                updateContainerWidth(scalesCopy.length * scaleWidth * getDevicePixelRatio());
                 canvasRef.setState({
-                    scales: scales
+                    scales: scalesCopy,
+                    scalesMapLinkList: getScalesMapLinkList(scalesCopy)
                 });
             }
             if (typeof option.exporter === "function")
                 option.exporter(updateData);
-            onContainerScroll.action((e) => {
-                canvasRef.setState({scrollLeft: e.target.scrollLeft / getDevicePixelRatio()});
-            });
-            onResize.action((width) => {
-                canvasRef.setState({width: width});
-            });
+            onContainerScroll.action(onScrollAction);
+            onResize.action(onResizeAction);
             return `<div class="x-axis">
                 <canvas ref="${canvasRef}">
             </div>`;
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to