Changeset 248305 in webkit
- Timestamp:
- Aug 6, 2019 10:56:57 AM (5 years ago)
- Location:
- trunk/Tools
- Files:
-
- 5 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/Tools/ChangeLog
r248302 r248305 1 2019-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 1 25 2019-08-06 Jer Noble <jer.noble@apple.com> 2 26 -
trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/Ref.js
r247664 r248305 40 40 } 41 41 removeListener(fn) { 42 const removeIndex = 0;42 let removeIndex = 0; 43 43 this.handlers.forEach((handler, index) => { 44 44 if (handler === fn) 45 45 removeIndex = index; 46 46 }); 47 this.handler .splice(removeIndex, 1);47 this.handlers.splice(removeIndex, 1); 48 48 return this; 49 49 } … … 91 91 return this; 92 92 } 93 stopAction(fn) { 94 this.signal.removeListener(fn); 95 return this; 96 } 93 97 error(fn) { 94 98 this.errorSignal.addListener(fn); … … 244 248 this.onStateUpdate = new Signal().addListener(config.onStateUpdate); 245 249 this.onElementMount = new Signal().addListener(config.onElementMount); 246 this.onElementUnmou t = new Signal().addListener(config.onElementUnmout);250 this.onElementUnmount = new Signal().addListener(config.onElementUnmount); 247 251 } 248 252 bind(element) { … … 256 260 return; 257 261 if (this.element && this.element !== element) 258 this.onElementUnmou t.emit(this.element);262 this.onElementUnmount.emit(this.element); 259 263 this.element = element; 260 264 //Initial state … … 266 270 if (!element || element !== this.element) 267 271 return; 268 this.onElementUnmou t.emit(this.element);272 this.onElementUnmount.emit(this.element); 269 273 } 270 274 toString() { -
trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/Utils.js
r247664 r248305 66 66 return match.matches; 67 67 } 68 export {timeDifference, isDarkMode, Cookie}; 68 69 // Uses intersection observer: <https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API> 70 function 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 85 export {timeDifference, isDarkMode, Cookie, createInsertionObservers}; -
trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/BaseComponents.js
r247904 r248305 43 43 if (index < element.children.length) { 44 44 if (child !== element.children[index]) { 45 if (child instanceof HTMLElement) 46 element. replaceChild(child, element.children[index]);47 else48 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); 49 49 } 50 50 } else { -
trunk/Tools/resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js
r248168 r248305 26 26 from '../Ref.js'; 27 27 28 import {isDarkMode } from '../Utils.js';28 import {isDarkMode, createInsertionObservers} from '../Utils.js'; 29 29 import {ListComponent, ListProvider, ListProviderReceiver} from './BaseComponents.js' 30 30 … … 93 93 }, 94 94 onStateUpdate: (element, stateDiff, state) => { 95 if (typeof stateDiff.scrollLeft === 'number')96 element.style.transform = `translate(${stateDiff.scrollLeft}px, 0)`;97 95 if (stateDiff.resize) { 98 96 element.style.width = `${element.parentElement.parentElement.offsetWidth}px`; … … 101 99 } 102 100 }); 103 scrollEventStream.action((e) => {104 // Provide the logic scrollLeft105 presenterRef.setState({scrollLeft: e.target.scrollLeft});106 });107 101 // Provide parent functions/event to children to use 108 102 109 103 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">${ 112 106 ListProvider((updateChildrenFunctions) => { 113 107 if (exporter) { … … 128 122 let cachedScrollLeft = 0; 129 123 let offscreenCanvas = document.createElement('canvas'); 124 // Double buffering 125 const offscreenCanvasBuffer = document.createElement('canvas'); 130 126 131 127 // This function will call redrawCache to render a offscreen cache … … 133 129 // It will trigger redrawCache when cache don't have enough space 134 130 return (redrawCache, element, stateDiff, state, forceRedrawCache = false) => { 131 // Check if the canvas display on the screen or not, 132 // This will save render time 135 133 const width = typeof stateDiff.width === 'number' ? stateDiff.width : state.width; 136 134 if (width <= 0) … … 156 154 if (needToRedrawCache) { 157 155 // We draw everything on cache 158 redrawCache(offscreenCanvas, element, stateDiff, state, () => {156 redrawCache(offscreenCanvas, element, stateDiff, state, () => { 159 157 cachedScrollLeft = scrollLeft < padding ? scrollLeft : scrollLeft - padding; 160 158 cachePosLeft = scrollLeft - cachedScrollLeft; … … 332 330 }; 333 331 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 } 350 397 } 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 378 400 updateContainerWidth(scales.length * dotWidth * getDevicePixelRatio()); 379 401 const updateData = (dots, scales) => { … … 386 408 if (typeof option.exporter === "function") 387 409 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); 394 412 return `<div class="series"> 395 413 <canvas ref="${canvasRef}"> … … 684 702 const initScaleGroupMapLinkList = getScalesMapLinkList(initScales); 685 703 686 const canvasRef = REF.createRef({687 state: {688 scrollLeft: 0,689 width: 0,690 scales: initScales,691 scalesMapLinkList: initScaleGroupMapLinkList692 },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 });728 704 729 705 return { 730 706 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 731 762 updateContainerWidth(scales.length * scaleWidth * getDevicePixelRatio()); 732 763 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()); 734 767 canvasRef.setState({ 735 scales: scales 768 scales: scalesCopy, 769 scalesMapLinkList: getScalesMapLinkList(scalesCopy) 736 770 }); 737 771 } 738 772 if (typeof option.exporter === "function") 739 773 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); 746 776 return `<div class="x-axis"> 747 777 <canvas ref="${canvasRef}">
Note: See TracChangeset
for help on using the changeset viewer.