Changeset 248305 in webkit


Ignore:
Timestamp:
Aug 6, 2019 10:56:57 AM (5 years ago)
Author:
commit-queue@webkit.org
Message:

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

Patch by Zhifei Fang <zhifei_fang@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):

Location:
trunk/Tools
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/Tools/ChangeLog

    r248302 r248305  
     12019-08-06  Zhifei Fang  <zhifei_fang@apple.com>
     2
     3        [results.webkit.org Timeline] Performance improvements
     4        https://bugs.webkit.org/show_bug.cgi?id=200406
     5
     6        Reviewed by Jonathan Bedard.
     7
     8        1. Unhook the scroll event when a series/axis have been removed from the container
     9        2. Fix the axis's cache data structure out of sync.
     10        3. Use position:sticky to reduce the scrolling blink when update the presenter's transform
     11        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.
     12
     13
     14        * resultsdbpy/resultsdbpy/view/static/library/js/Ref.js:
     15        (Signal.prototype.removeListener):
     16        (prototype.stopAction): Unregsiter an action handler
     17        (Ref):
     18        (Ref.prototype.apply):
     19        (Ref.prototype.destory):
     20        * resultsdbpy/resultsdbpy/view/static/library/js/components/BaseComponents.js:
     21        (ApplyNewChildren):
     22        * resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js:
     23        (Timeline.CanvasSeriesComponent):
     24
    1252019-08-06  Jer Noble  <jer.noble@apple.com>
    226
  • trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/Ref.js

    r247664 r248305  
    4040    }
    4141    removeListener(fn) {
    42         const removeIndex = 0;
     42        let removeIndex = 0;
    4343        this.handlers.forEach((handler, index) => {
    4444            if (handler === fn)
    4545                removeIndex = index;
    4646        });
    47         this.handler.splice(removeIndex, 1);
     47        this.handlers.splice(removeIndex, 1);
    4848        return this;
    4949    }
     
    9191        return this;
    9292    }
     93    stopAction(fn) {
     94        this.signal.removeListener(fn);
     95        return this;
     96    }
    9397    error(fn) {
    9498        this.errorSignal.addListener(fn);
     
    244248        this.onStateUpdate = new Signal().addListener(config.onStateUpdate);
    245249        this.onElementMount = new Signal().addListener(config.onElementMount);
    246         this.onElementUnmout = new Signal().addListener(config.onElementUnmout);
     250        this.onElementUnmount = new Signal().addListener(config.onElementUnmount);
    247251    }
    248252    bind(element) {
     
    256260            return;
    257261        if (this.element && this.element !== element)
    258             this.onElementUnmout.emit(this.element);
     262            this.onElementUnmount.emit(this.element);
    259263        this.element = element;
    260264        //Initial state
     
    266270        if (!element || element !== this.element)
    267271            return;
    268         this.onElementUnmout.emit(this.element);
     272        this.onElementUnmount.emit(this.element);
    269273    }
    270274    toString() {
  • trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/Utils.js

    r247664 r248305  
    6666    return match.matches;
    6767}
    68 export {timeDifference, isDarkMode, Cookie};
     68
     69// Uses intersection observer: <https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API>
     70function createInsertionObservers(element, callback=()=>{}, startThreshold=0.0, endTreshold=1.0, step=0.1, option={}) {
     71    const useOption = {};
     72    useOption.root = option.root instanceof HTMLElement ? option.root : null;
     73    useOption.rootMargin = option.rootMargin ? option.rootMargin : "0";
     74    const thresholdArray = [];
     75    for (let i = startThreshold; i <= endTreshold; i+= step) {
     76        thresholdArray.push(i);
     77    }
     78    thresholdArray.forEach(threshold => {
     79        useOption.threshold = threshold;
     80        const observer = new IntersectionObserver(callback, useOption);
     81        observer.observe(element);
     82    });
     83}
     84
     85export {timeDifference, isDarkMode, Cookie, createInsertionObservers};
  • trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/BaseComponents.js

    r247904 r248305  
    4343        if (index < element.children.length) {
    4444            if (child !== element.children[index]) {
    45                 if (child instanceof HTMLElement)
    46                     element.replaceChild(child, element.children[index]);
    47                 else
    48                     DOM.replace(element.children[index], typeof itemProcessor === "function" ? itemProcessor(child) : child);
     45                if (child instanceof HTMLElement) {
     46                    element.children[index].before(child);
     47                } else
     48                    DOM.before(element.children[index], typeof itemProcessor === "function" ? itemProcessor(child) : child);
    4949            }
    5050        } else {
  • trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js

    r248168 r248305  
    2626from '../Ref.js';
    2727
    28 import {isDarkMode} from '../Utils.js';
     28import {isDarkMode, createInsertionObservers} from '../Utils.js';
    2929import {ListComponent, ListProvider, ListProviderReceiver} from './BaseComponents.js'
    3030
     
    9393        },
    9494        onStateUpdate: (element, stateDiff, state) => {
    95             if (typeof stateDiff.scrollLeft === 'number')
    96                 element.style.transform = `translate(${stateDiff.scrollLeft}px, 0)`;
    9795            if (stateDiff.resize) {
    9896                element.style.width = `${element.parentElement.parentElement.offsetWidth}px`;
     
    10199        }
    102100    });
    103     scrollEventStream.action((e) => {
    104         // Provide the logic scrollLeft
    105         presenterRef.setState({scrollLeft: e.target.scrollLeft});
    106     });
    107101    // Provide parent functions/event to children to use
    108102
    109103    return `<div class="content" ref="${scrollRef}">
    110         <div ref="${containerRef}">
    111             <div ref="${presenterRef}">${
     104        <div ref="${containerRef}" style="position: relative">
     105            <div ref="${presenterRef}" style="position: -webkit-sticky; position:sticky; top:0; left: 0">${
    112106                ListProvider((updateChildrenFunctions) => {
    113107                    if (exporter) {
     
    128122    let cachedScrollLeft = 0;
    129123    let offscreenCanvas = document.createElement('canvas');
     124    // Double buffering
     125    const offscreenCanvasBuffer = document.createElement('canvas');
    130126
    131127    // This function will call redrawCache to render a offscreen cache
     
    133129    // It will trigger redrawCache when cache don't have enough space
    134130    return (redrawCache, element, stateDiff, state, forceRedrawCache = false) => {
     131        // Check if the canvas display on the screen or not,
     132        // This will save render time
    135133        const width = typeof stateDiff.width === 'number' ? stateDiff.width : state.width;
    136134        if (width <= 0)
     
    156154        if (needToRedrawCache) {
    157155            // We draw everything on cache
    158             redrawCache(offscreenCanvas, element, stateDiff, state, () => {
     156             redrawCache(offscreenCanvas, element, stateDiff, state, () => {
    159157                cachedScrollLeft = scrollLeft < padding ? scrollLeft : scrollLeft - padding;
    160158                cachePosLeft = scrollLeft - cachedScrollLeft;
     
    332330    };
    333331
    334     const canvasRef = REF.createRef({
    335         state: {
    336             dots: initDots,
    337             scales: initScales,
    338             scrollLeft: 0,
    339             width: 0,
    340         },
    341         onElementMount: (element) => {
    342             setupCanvasHeightWithDpr(element, height);
    343             setupCanvasContextScale(element);
    344             if (onDotClick) {
    345                 element.addEventListener('click', (e) => {
    346                     let dots = getMouseEventTirggerDots(e, canvasRef.state.scrollLeft, element);
    347                     if (dots.length)
    348                         onDotClick(dots[0], e);
    349                 });
     332    return ListProviderReceiver((updateContainerWidth, onContainerScroll, onResize) => {
     333        const onScrollAction = (e) => {
     334            canvasRef.setState({scrollLeft: e.target.scrollLeft / getDevicePixelRatio()});
     335        };
     336        const onResizeAction = (width) => {
     337            canvasRef.setState({width: width});
     338        };
     339
     340        const canvasRef = REF.createRef({
     341            state: {
     342                dots: initDots,
     343                scales: initScales,
     344                scrollLeft: 0,
     345                width: 0,
     346                onScreen: true,
     347            },
     348            onElementMount: (element) => {
     349                setupCanvasHeightWithDpr(element, height);
     350                setupCanvasContextScale(element);
     351                if (onDotClick) {
     352                    element.addEventListener('click', (e) => {
     353                        let dots = getMouseEventTirggerDots(e, canvasRef.state.scrollLeft, element);
     354                        if (dots.length)
     355                            onDotClick(dots[0], e);
     356                    });
     357                }
     358
     359                if (onDotClick || onDotHover) {
     360                    element.addEventListener('mousemove', (e) => {
     361                        let dots = getMouseEventTirggerDots(e, canvasRef.state.scrollLeft, element);
     362                        if (dots.length) {
     363                            if (onDotHover)
     364                                onDotHover(dots[0], e);
     365                            element.style.cursor = "pointer";
     366                        } else
     367                            element.style.cursor = "default";
     368                    });
     369                }
     370
     371                createInsertionObservers(element, (entries) => {
     372                    canvasRef.setState({onScreen: entries[0].isIntersecting});
     373                }, 0, 0.01, 0.01);
     374            },
     375            onElementUnmount: (element) => {
     376                onContainerScroll.stopAction(onScrollAction);
     377                onResize.stopAction(onResizeAction);
     378            },
     379            onStateUpdate: (element, stateDiff, state) => {
     380                const context = element.getContext("2d");
     381                let forceRedrawCache = false;
     382                if (!state.onScreen && !stateDiff.onScreen)
     383                    return;
     384
     385                if (stateDiff.scales || stateDiff.dots || typeof stateDiff.scrollLeft === 'number' || typeof stateDiff.width === 'number' || stateDiff.onScreen) {
     386
     387                    if (stateDiff.scales) {
     388                        stateDiff.scales = stateDiff.scales.map(x => x);
     389                        forceRedrawCache = true;
     390                    }
     391                    if (stateDiff.dots) {
     392                        stateDiff.dots = stateDiff.dots.map(x => x);
     393                        forceRedrawCache = true;
     394                    }
     395                    requestAnimationFrame(() => offscreenCachedRender(redrawCache, element, stateDiff, state, forceRedrawCache));
     396                }
    350397            }
    351 
    352             if (onDotClick || onDotHover) {
    353                 element.addEventListener('mousemove', (e) => {
    354                     let dots = getMouseEventTirggerDots(e, canvasRef.state.scrollLeft, element);
    355                     if (dots.length) {
    356                         if (onDotHover)
    357                             onDotHover(dots[0], e);
    358                         element.style.cursor = "pointer";
    359                     } else
    360                         element.style.cursor = "default";
    361                 });
    362             }
    363         },
    364         onStateUpdate: (element, stateDiff, state) => {
    365             const context = element.getContext("2d");
    366             let forceRedrawCache = false;
    367             if (stateDiff.scales || stateDiff.dots || typeof stateDiff.scrollLeft === 'number' || typeof stateDiff.width === 'number') {
    368                 console.assert(dots.length <= scales.length);
    369                 if (stateDiff.scales || stateDiff.dots) {
    370                     forceRedrawCache = true;
    371                 }
    372                 requestAnimationFrame(() => offscreenCachedRender(redrawCache, element, stateDiff, state, forceRedrawCache));
    373             }
    374         }
    375     });
    376 
    377     return ListProviderReceiver((updateContainerWidth, onContainerScroll, onResize) => {
     398        });
     399
    378400        updateContainerWidth(scales.length * dotWidth * getDevicePixelRatio());
    379401        const updateData = (dots, scales) => {
     
    386408        if (typeof option.exporter === "function")
    387409            option.exporter(updateData);
    388         onContainerScroll.action((e) => {
    389             canvasRef.setState({scrollLeft: e.target.scrollLeft / getDevicePixelRatio()});
    390         });
    391         onResize.action((width) => {
    392             canvasRef.setState({width: width});
    393         });
     410        onContainerScroll.action(onScrollAction);
     411        onResize.action(onResizeAction);
    394412        return `<div class="series">
    395413            <canvas ref="${canvasRef}">
     
    684702    const initScaleGroupMapLinkList = getScalesMapLinkList(initScales);
    685703
    686     const canvasRef = REF.createRef({
    687         state: {
    688             scrollLeft: 0,
    689             width: 0,
    690             scales: initScales,
    691             scalesMapLinkList: initScaleGroupMapLinkList
    692         },
    693         onElementMount: (element) => {
    694             setupCanvasHeightWithDpr(element, canvasHeight);
    695             setupCanvasContextScale(element);
    696             if (onScaleClick) {
    697                 element.addEventListener('click', (e) => {
    698                     let scales = getMouseEventTirggerScales(e, canvasRef.state.scrollLeft, element);
    699                     if (scales.length)
    700                         onScaleClick(scales[0], e);
    701                 });
    702             }
    703 
    704             if (onScaleClick || onScaleHover) {
    705                 element.addEventListener('mousemove', (e) => {
    706                     let scales = getMouseEventTirggerScales(e, canvasRef.state.scrollLeft, element);
    707                     if (scales.length) {
    708                         if (onScaleHover)
    709                             onScaleHover(scales[0], e);
    710                         element.style.cursor = "pointer";
    711                     } else {
    712                         element.style.cursor = "default";
    713                     }
    714                 });
    715             }
    716         },
    717         onStateUpdate: (element, stateDiff, state) => {
    718             let forceRedrawCache = false;
    719             if (stateDiff.scales || typeof stateDiff.scrollLeft === 'number' || typeof stateDiff.width === 'number') {
    720                 if (stateDiff.scales) {
    721                     state.scalesMapLinkList = getScalesMapLinkList(stateDiff.scales);
    722                     forceRedrawCache = true;
    723                 }
    724                 requestAnimationFrame(() => offscreenCachedRender(redrawCache, element, stateDiff, state, forceRedrawCache));
    725             }
    726         }
    727     });
    728704
    729705    return {
    730706        series: ListProviderReceiver((updateContainerWidth, onContainerScroll, onResize) => {
     707            const onScrollAction = (e) => {
     708                canvasRef.setState({scrollLeft: e.target.scrollLeft / getDevicePixelRatio()});
     709            };
     710            const onResizeAction = (width) => {
     711                canvasRef.setState({width: width});
     712            };
     713
     714            const canvasRef = REF.createRef({
     715                state: {
     716                    scrollLeft: 0,
     717                    width: 0,
     718                    scales: initScales,
     719                    scalesMapLinkList: initScaleGroupMapLinkList
     720                },
     721                onElementMount: (element) => {
     722                    setupCanvasHeightWithDpr(element, canvasHeight);
     723                    setupCanvasContextScale(element);
     724                    if (onScaleClick) {
     725                        element.addEventListener('click', (e) => {
     726                            let scales = getMouseEventTirggerScales(e, canvasRef.state.scrollLeft, element);
     727                            if (scales.length)
     728                                onScaleClick(scales[0], e);
     729                        });
     730                    }
     731
     732                    if (onScaleClick || onScaleHover) {
     733                        element.addEventListener('mousemove', (e) => {
     734                            let scales = getMouseEventTirggerScales(e, canvasRef.state.scrollLeft, element);
     735                            if (scales.length) {
     736                                if (onScaleHover)
     737                                    onScaleHover(scales[0], e);
     738                                element.style.cursor = "pointer";
     739                            } else {
     740                                element.style.cursor = "default";
     741                            }
     742                        });
     743                    }
     744                },
     745                onElementUnmount: (element) => {
     746                    onContainerScroll.stopAction(onScrollAction);
     747                    onResize.stopAction(onResizeAction);
     748                },
     749                onStateUpdate: (element, stateDiff, state) => {
     750                    let forceRedrawCache = false;
     751                    if (stateDiff.scales || typeof stateDiff.scrollLeft === 'number' || typeof stateDiff.width === 'number') {
     752                        if (stateDiff.scales)
     753                            forceRedrawCache = true;
     754                        requestAnimationFrame(() => {
     755                            offscreenCachedRender(redrawCache, element, stateDiff, state, forceRedrawCache)
     756                        });
     757                    }
     758                }
     759            });
     760
     761
    731762            updateContainerWidth(scales.length * scaleWidth * getDevicePixelRatio());
    732763            const updateData = (scales) => {
    733                 updateContainerWidth(scales.length * scaleWidth * getDevicePixelRatio());
     764                // In case of modification while rendering
     765                const scalesCopy = scales.map(x => x);
     766                updateContainerWidth(scalesCopy.length * scaleWidth * getDevicePixelRatio());
    734767                canvasRef.setState({
    735                     scales: scales
     768                    scales: scalesCopy,
     769                    scalesMapLinkList: getScalesMapLinkList(scalesCopy)
    736770                });
    737771            }
    738772            if (typeof option.exporter === "function")
    739773                option.exporter(updateData);
    740             onContainerScroll.action((e) => {
    741                 canvasRef.setState({scrollLeft: e.target.scrollLeft / getDevicePixelRatio()});
    742             });
    743             onResize.action((width) => {
    744                 canvasRef.setState({width: width});
    745             });
     774            onContainerScroll.action(onScrollAction);
     775            onResize.action(onResizeAction);
    746776            return `<div class="x-axis">
    747777                <canvas ref="${canvasRef}">
Note: See TracChangeset for help on using the changeset viewer.