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
- trunk/Tools/ChangeLog
- trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/Ref.js
- trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/Utils.js
- trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/BaseComponents.js
- trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js
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