feat: remove the worker of mobile layout

This commit is contained in:
毅羽 2021-01-11 15:39:52 +08:00 committed by Yanyan Wang
parent 1f3a221638
commit d3cda8e1c9
3 changed files with 16 additions and 444 deletions

View File

@ -1,108 +1,42 @@
import { AbstractLayout, GraphData } from '@antv/g6-core';
import { Layout } from '../../layout';
import LayoutWorker from '../../layout/worker/layout.worker';
import { LAYOUT_MESSAGE } from '../../layout/worker/layoutConst';
import { gpuDetector } from '../../util/gpu';
import { mix } from '@antv/util';
import { IGraph } from '../../interface/graph';
// eslint-disable-next-line @typescript-eslint/no-implied-eval
const mockRaf = (cb: TimerHandler) => setTimeout(cb, 16);
const mockCaf = (reqId: number) => clearTimeout(reqId);
const helper = {
// pollyfill
requestAnimationFrame(callback: FrameRequestCallback) {
const fn =
typeof window !== 'undefined'
? window.requestAnimationFrame || window.webkitRequestAnimationFrame || mockRaf
: mockRaf;
return fn(callback);
},
cancelAnimationFrame(requestId: number) {
const fn =
typeof window !== 'undefined'
? window.cancelAnimationFrame || window.webkitCancelAnimationFrame || mockCaf
: mockCaf;
return fn(requestId);
},
};
const GPULayoutNames = ['fruchterman', 'gForce'];
export default class LayoutController extends AbstractLayout {
public graph: IGraph;
public destroyed: boolean;
private worker;
private workerData;
// private data;
private isGPU: boolean;
// the configurations of the layout
// private layoutCfg: any; // LayoutOptions
// the type name of the layout
// private layoutType: string;
// private data: GraphData;
// private layoutMethod: typeof Layout;
constructor(graph: IGraph) {
super(graph);
this.graph = graph;
this.layoutCfg = graph.get('layout') || {};
this.layoutType = this.layoutCfg.type;
this.worker = null;
this.workerData = {};
this.initLayout();
}
// eslint-disable-next-line class-methods-use-this
protected initLayout() {
// no data before rendering
}
// 更新布局参数
public updateLayoutCfg(cfg) {
const { graph, layoutMethod, layoutType, layoutCfg } = this;
// get layout worker and create one if not exists
private getWorker() {
if (this.worker) {
return this.worker;
}
if (typeof Worker === 'undefined') {
// 如果当前浏览器不支持 web worker则不使用 web worker
console.warn('Web worker is not supported in current browser.');
this.worker = null;
} else {
this.worker = new LayoutWorker();
}
return this.worker;
}
// stop layout worker
private stopWorker() {
const { workerData } = this;
if (!this.worker) {
this.layoutType = cfg.type;
if (!layoutMethod || layoutMethod.destroyed) {
this.layoutCfg = mix({}, layoutCfg, cfg);
this.layout();
return;
}
this.data = this.setDataFromGraph();
this.worker.terminate();
this.worker = null;
// 重新开始新的布局之前先取消之前布局的requestAnimationFrame。
if (workerData.requestId) {
helper.cancelAnimationFrame(workerData.requestId);
workerData.requestId = null;
}
if (workerData.requestId2) {
helper.cancelAnimationFrame(workerData.requestId2);
workerData.requestId2 = null;
layoutMethod.init(this.data);
layoutMethod.updateCfg(cfg);
graph.emit('beforelayout');
layoutMethod.execute();
if (this.layoutType !== 'force' && !layoutMethod.enableTick) {
graph.emit('afterlayout');
}
this.refreshLayout();
}
/**
@ -143,41 +77,6 @@ export default class LayoutController extends AbstractLayout {
const allHavePos = this.initPositions(layoutCfg.center, nodes);
let layoutType = this.layoutType;
let isGPU = false;
// 防止用户直接用 -gpu 结尾指定布局
if (layoutType && layoutType.split('-')[1] === 'gpu') {
layoutType = layoutType.split('-')[0];
layoutCfg.gpuEnabled = true;
}
// 若用户指定开启 gpu且当前浏览器支持 webgl且该算法存在 GPU 版本(目前仅支持 fruchterman 和 gForce使用 gpu 版本的布局
if (layoutType && layoutCfg.gpuEnabled) {
let enableGPU = true;
// 打开下面语句将会导致 webworker 报找不到 window
if (!gpuDetector().webgl) {
console.warn(`Your browser does not support webGL or GPGPU. The layout will run in CPU.`);
enableGPU = false;
}
if (!this.hasGPUVersion(layoutType)) {
console.warn(
`The '${layoutType}' layout does not support GPU calculation for now, it will run in CPU.`,
);
enableGPU = false;
}
if (enableGPU) {
layoutType = `${layoutType}-gpu`;
// layoutCfg.canvasEl = this.graph.get('canvas').get('el');
isGPU = true;
}
}
this.isGPU = isGPU;
this.stopWorker();
if (layoutCfg.workerEnabled && this.layoutWithWorker(this.data, success)) {
// 如果启用布局web worker并且浏览器支持web worker用web worker布局。否则回退到不用web worker布局。
return true;
}
if (layoutType === 'force' || layoutType === 'g6force' || layoutType === 'gForce') {
const { onTick } = layoutCfg;
@ -197,15 +96,6 @@ export default class LayoutController extends AbstractLayout {
};
} else if (this.layoutType === 'comboForce') {
layoutCfg.comboTrees = graph.get('comboTrees');
} else if (isGPU) {
const { onLayoutEnd } = layoutCfg;
layoutCfg.onLayoutEnd = () => {
this.refreshLayout();
if (onLayoutEnd) {
onLayoutEnd();
}
graph.emit('afterlayout');
};
}
let enableTick = false;
@ -249,8 +139,7 @@ export default class LayoutController extends AbstractLayout {
(hasLayoutType || !allHavePos) &&
this.layoutType !== 'force' &&
this.layoutType !== 'gForce' &&
!enableTick &&
!isGPU
!enableTick
) {
graph.emit('afterlayout');
this.refreshLayout();
@ -258,196 +147,12 @@ export default class LayoutController extends AbstractLayout {
return false;
}
/**
* layout with web worker
* @param {object} data graph data
* @param {function} success callback function
* @return {boolean} web worker
*/
private layoutWithWorker(data, success?: () => void): boolean {
const { nodes, edges } = data;
const { layoutCfg, graph, isGPU } = this;
const worker = this.getWorker();
// 每次worker message event handler调用之间的共享数据会被修改。
const { workerData } = this;
if (!worker) {
return false;
}
workerData.requestId = null;
workerData.requestId2 = null;
workerData.currentTick = null;
workerData.currentTickData = null;
graph.emit('beforelayout');
const offScreenCanvas = document.createElement('canvas');
const gpuWorkerAbility =
isGPU &&
typeof window !== 'undefined' &&
// eslint-disable-next-line @typescript-eslint/dot-notation
window.navigator &&
!navigator[`gpu`] && // WebGPU 还不支持 OffscreenCanvas
'OffscreenCanvas' in window &&
'transferControlToOffscreen' in offScreenCanvas;
// NOTE: postMessage的message参数里面不能包含函数否则postMessage会报错
// 例如:'function could not be cloned'。
// 详情参考https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
// 所以这里需要把过滤layoutCfg里的函数字段过滤掉。
const filteredLayoutCfg = filterObject(layoutCfg, (value) => typeof value !== 'function');
if (!gpuWorkerAbility) {
worker.postMessage({ type: LAYOUT_MESSAGE.RUN, nodes, edges, layoutCfg: filteredLayoutCfg });
} else {
const offscreen: any = (offScreenCanvas as any).transferControlToOffscreen();
// filteredLayoutCfg.canvas = offscreen;
filteredLayoutCfg.type = `${filteredLayoutCfg.type}-gpu`;
worker.postMessage(
{
type: LAYOUT_MESSAGE.GPURUN,
nodes,
edges,
layoutCfg: filteredLayoutCfg,
canvas: offscreen,
},
[offscreen],
);
}
worker.onmessage = (event) => {
this.handleWorkerMessage(event, data, success);
};
return true;
}
// success callback will be called when updating graph positions for the first time.
private handleWorkerMessage(event, data, success?: () => void) {
const { graph, workerData, layoutCfg } = this;
const eventData = event.data;
const { type } = eventData;
const onTick = () => {
if (layoutCfg.onTick) {
layoutCfg.onTick();
}
};
const onLayoutEnd = () => {
if (layoutCfg.onLayoutEnd) {
layoutCfg.onLayoutEnd();
}
graph.emit('afterlayout');
};
switch (type) {
case LAYOUT_MESSAGE.TICK:
workerData.currentTick = eventData.currentTick;
workerData.currentTickData = eventData;
if (!workerData.requestId) {
workerData.requestId = helper.requestAnimationFrame(function requestId() {
updateLayoutPosition(data, eventData);
graph.refreshPositions();
onTick();
if (eventData.currentTick === 1 && success) {
success();
}
if (eventData.currentTick === eventData.totalTicks) {
// 如果是最后一次tick
onLayoutEnd();
} else if (workerData.currentTick === eventData.totalTicks) {
// 注意这里workerData.currentTick可能已经不再是前面赋值时候的值了
// 因为在requestAnimationFrame等待时间里可能产生新的tick。
// 如果当前tick不是最后一次tick并且所有的tick消息都已发出来了那么需要用最后一次tick的数据再刷新一次。
workerData.requestId2 = helper.requestAnimationFrame(function requestId2() {
updateLayoutPosition(data, workerData.currentTickData);
graph.refreshPositions();
workerData.requestId2 = null;
onTick();
onLayoutEnd();
});
}
workerData.requestId = null;
});
}
break;
case LAYOUT_MESSAGE.END:
// 如果没有tick消息非力导布局
if (workerData.currentTick == null) {
updateLayoutPosition(data, eventData);
this.refreshLayout();
// 非力导布局没有tick消息只有end消息所以需要执行一次回调。
if (success) {
success();
}
graph.emit('afterlayout');
}
break;
case LAYOUT_MESSAGE.GPUEND:
// 如果没有tick消息非力导布局
if (workerData.currentTick == null) {
updateGPUWorkerLayoutPosition(data, eventData);
this.refreshLayout();
// 非力导布局没有tick消息只有end消息所以需要执行一次回调。
if (success) {
success();
}
graph.emit('afterlayout');
}
break;
case LAYOUT_MESSAGE.ERROR:
console.warn('Web-Worker layout error!', eventData.message);
break;
default:
break;
}
}
// 更新布局参数
public updateLayoutCfg(cfg) {
const { graph, layoutMethod, layoutType, layoutCfg } = this;
this.layoutType = cfg.type;
if (!layoutMethod || layoutMethod.destroyed) {
this.layoutCfg = mix({}, layoutCfg, cfg);
this.layout();
return;
}
this.data = this.setDataFromGraph();
this.stopWorker();
if (cfg.workerEnabled && this.layoutWithWorker(this.data, null)) {
// 如果启用布局web worker并且浏览器支持web worker用web worker布局。否则回退到不用web worker布局。
return;
}
layoutMethod.init(this.data);
layoutMethod.updateCfg(cfg);
graph.emit('beforelayout');
layoutMethod.execute();
if (this.layoutType !== 'force' && !layoutMethod.enableTick) {
graph.emit('afterlayout');
}
this.refreshLayout();
}
public hasGPUVersion(layoutName: string): boolean {
const length = GPULayoutNames.length;
for (let i = 0; i < length; i++) {
if (GPULayoutNames[i] === layoutName) return true;
}
return false;
}
public destroy() {
const { layoutMethod } = this;
if (layoutMethod) {
layoutMethod.destroy();
layoutMethod.destroyed = true;
}
const { worker } = this;
if (worker) {
worker.terminate();
this.worker = null;
}
this.destroyed = true;
this.graph.set('layout', undefined);
@ -457,41 +162,3 @@ export default class LayoutController extends AbstractLayout {
this.graph = null;
}
}
function updateLayoutPosition(data, layoutData) {
const { nodes } = data;
const { nodes: layoutNodes } = layoutData;
const nodeLength = nodes.length;
for (let i = 0; i < nodeLength; i++) {
const node = nodes[i];
node.x = layoutNodes[i].x;
node.y = layoutNodes[i].y;
}
}
function filterObject(collection, callback) {
const result = {};
if (collection && typeof collection === 'object') {
Object.keys(collection).forEach((key) => {
if (collection.hasOwnProperty(key) && callback(collection[key])) {
result[key] = collection[key];
}
});
return result;
}
return collection;
}
function updateGPUWorkerLayoutPosition(data, layoutData) {
const { nodes } = data;
const { vertexEdgeData } = layoutData;
const nodeLength = nodes.length;
for (let i = 0; i < nodeLength; i++) {
const node = nodes[i];
const x = vertexEdgeData[4 * i];
const y = vertexEdgeData[4 * i + 1];
node.x = x;
node.y = y;
}
}

View File

@ -1,77 +0,0 @@
/**
* @fileoverview web worker for layout
* @author changzhe.zb@antfin.com
*/
import { LAYOUT_MESSAGE } from './layoutConst';
import { getLayoutByName } from '@antv/layout';
interface Event {
type: string;
data: any;
}
const ctx: Worker = self as any;
function isLayoutMessage(event: Event) {
const { type } = event.data;
return type === LAYOUT_MESSAGE.RUN || type === LAYOUT_MESSAGE.GPURUN;
}
function handleLayoutMessage(event: Event) {
const { type } = event.data;
switch (type) {
case LAYOUT_MESSAGE.RUN: {
const { nodes, edges, layoutCfg = {} } = event.data;
const { type: layoutType } = layoutCfg;
const LayoutClass = getLayoutByName(layoutType);
if (!LayoutClass) {
ctx.postMessage({ type: LAYOUT_MESSAGE.ERROR, message: `layout ${layoutType} not found` });
break;
}
const layoutMethod = new LayoutClass(layoutCfg);
layoutMethod.init({ nodes, edges });
layoutMethod.execute();
ctx.postMessage({ type: LAYOUT_MESSAGE.END, nodes });
layoutMethod.destroy();
break;
}
case LAYOUT_MESSAGE.GPURUN: {
const { nodes, edges, layoutCfg = {}, canvas } = event.data;
const { type: layoutType } = layoutCfg;
const LayoutClass = getLayoutByName(layoutType);
if (!LayoutClass) {
ctx.postMessage({ type: LAYOUT_MESSAGE.ERROR, message: `layout ${layoutType} not found` });
break;
}
if (layoutType.split('-')[1] !== 'gpu') {
ctx.postMessage({
type: LAYOUT_MESSAGE.ERROR,
message: `layout ${layoutType} does not support GPU`,
});
break;
}
const layoutMethod = new LayoutClass(layoutCfg);
layoutMethod.init({ nodes, edges });
layoutMethod.executeWithWorker(canvas, ctx);
break;
}
default:
break;
}
}
// listen to message posted to web worker
ctx.onmessage = (event: Event) => {
if (isLayoutMessage(event)) {
handleLayoutMessage(event);
}
};
// https://stackoverflow.com/questions/50210416/webpack-worker-loader-fails-to-compile-typescript-worker
export default null as any;

View File

@ -1,18 +0,0 @@
/**
* @fileoverview constants for layout
* @author changzhe.zb@antfin.com
*/
/** layout message type */
export const LAYOUT_MESSAGE = {
// run layout
RUN: 'LAYOUT_RUN',
// layout ended with success
END: 'LAYOUT_END',
// layout error
ERROR: 'LAYOUT_ERROR',
// layout tick, used in force directed layout
TICK: 'LAYOUT_TICK',
GPURUN: 'GPU_LAYOUT_RUN',
GPUEND: 'GPU_LAYOUT_END',
};