Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add requestIdleCallback support for performance #2181

Merged
merged 15 commits into from
Jan 10, 2024
4 changes: 3 additions & 1 deletion src/core/Browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ if (!IS_NODE) {
imageBitMap = typeof window !== 'undefined' && isFunction(window.createImageBitmap),
resizeObserver = typeof window !== 'undefined' && isFunction(window.ResizeObserver),
btoa = typeof window !== 'undefined' && isFunction(window.btoa),
proxy = typeof window !== 'undefined' && isFunction(window.Proxy);
proxy = typeof window !== 'undefined' && isFunction(window.Proxy),
requestIdleCallback = typeof window !== 'undefined' && isFunction(window.requestIdleCallback);


let chromeVersion = 0;
Expand Down Expand Up @@ -127,6 +128,7 @@ if (!IS_NODE) {
monitorDPRChange: true,
supportsPassive,
proxy,
requestIdleCallback,
// removeDPRListening: (map) => {
// // if (map) {
// // delete maps[map.id];
Expand Down
127 changes: 127 additions & 0 deletions src/core/MicroTask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import PromisePolyfill from './Promise';
import { requestAnimFrame } from './util';
import { isFunction, isNil, isNumber } from './util/common';
import { getGlobalWorkerPool } from './worker/WorkerPool';
import Browser from './Browser';
import globalConfig from '../globalConfig';

let tasks = [];

/**
*
* @param {Object|Function} task - a micro task(Promise)
* @param {Number} task.count - task run count
* @param {Function} task.run - task run function
* @return {Promise}
* @example
* const run =()=>{
* //do some things
* };
* runTaskAsync({count:4,run}).then(result=>{})
* runTaskAsync(run).then(result=>{})
*/
export function runTaskAsync(task) {
startTasks();
const promise = new PromisePolyfill((resolve, reject) => {
if (!task) {
reject(new Error('task is null'));
return;
}
if (isFunction(task)) {
task = { count: 1, run: task };
}
if (!task.run) {
reject(new Error('task.run is null'));
return;
}
if (isNil(task.count)) {
task.count = 1;
}
task.count = Math.ceil(task.count);
if (!isNumber(task.count)) {
reject(new Error('task.count is not number'));
return;
}
task.results = [];
tasks.push(task);
task.resolve = resolve;
});
return promise;
}

function executeMicroTasks() {
if (tasks.length === 0) {
return;
}
const runingTasks = [], endTasks = [];
let len = tasks.length;
for (let i = 0; i < len; i++) {
const task = tasks[i];
task.count--;
if (task.count === -1) {
endTasks.push(task);
} else {
runingTasks.push(task);
const result = task.run();
task.results.push(result);
}
}
tasks = runingTasks;
len = endTasks.length;
for (let i = 0; i < len; i++) {
const task = endTasks[i];
if (task.resolve) {
task.resolve(task.results);
}
}
}

let broadcastIdleMessage = true;
function loop() {
if (broadcastIdleMessage) {
getGlobalWorkerPool().broadcastIdleMessage();
} else {
getGlobalWorkerPool().commit();
}
executeMicroTasks();
broadcastIdleMessage = !broadcastIdleMessage;
}

function frameLoop(deadline) {
const { idleTimeRemaining, idleLog, idleTimeout } = globalConfig;
if (Browser.requestIdleCallback) {
if (deadline && deadline.timeRemaining) {
const t = deadline.timeRemaining();
if (t > idleTimeRemaining || deadline.didTimeout) {
if (deadline.didTimeout && idleLog) {
console.error('idle timeout in', idleTimeout);
}
loop();
} else if (t <= idleTimeRemaining && idleLog) {
console.warn('currrent page is busy,the timeRemaining is', t);
}
}
requestIdleCallback(frameLoop, { timeout: idleTimeout });
} else {
loop();
// Fallback to requestAnimFrame
requestAnimFrame(frameLoop);
if (idleLog) {
console.warn('current env not support requestIdleCallback. Fallback to requestAnimFrame');
}
}
}

let started = false;
export function startTasks() {
if (started) {
return;
}
started = true;
const { idleTimeout } = globalConfig;
if (Browser.requestIdleCallback) {
requestIdleCallback(frameLoop, { timeout: idleTimeout });
} else {
requestAnimFrame(frameLoop);
}
}
2 changes: 2 additions & 0 deletions src/core/worker/Actor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getGlobalWorkerPool } from './WorkerPool';
import { UID } from '../util';
import { createAdapter } from './Worker';
import { adapterHasCreated, pushAdapterCreated, workersHasCreated } from './CoreWorkers';
import { startTasks } from '../MicroTask';

let dedicatedWorker = 0;

Expand Down Expand Up @@ -47,6 +48,7 @@ const EMPTY_BUFFERS = [];
class Actor {

constructor(workerKey) {
startTasks();
this._delayMessages = [];
this.initializing = false;
const hasCreated = adapterHasCreated(workerKey);
Expand Down
31 changes: 30 additions & 1 deletion src/core/worker/Worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ const header = `
postMessage({adapterName:key});
return;
}
// postMessage when main thread idle
if(msg.messageType==='idle'){
var messageCount = msg.messageCount||5;
handleMessageQueue(messageCount);
return;
}
if (msg.messageType === 'batch') {
const messages = msg.messages;
if (messages) {
Expand Down Expand Up @@ -81,6 +87,19 @@ const header = `
}
}

var messageResultQueue = [];

function handleMessageQueue(messageCount){
if(messageResultQueue.length===0){
return;
}
var queues = messageResultQueue.slice(0,messageCount);
queues.forEach(function(queue){
post(queue.callback,queue.err,queue.data,queue.buffers);
});
messageResultQueue=messageResultQueue.slice(messageCount,Infinity);
}

function post(callback, err, data, buffers) {
var msg = {
callback : callback
Expand All @@ -96,9 +115,19 @@ const header = `
postMessage(msg);
}
}

function joinQueue(callback,err,data,buffers){
messageResultQueue.push({
callback:callback,
err:err,
data:data,
buffers:buffers
});
}

function wrap(callback) {
return function (err, data, buffers) {
post(callback, err, data, buffers);
joinQueue(callback, err, data, buffers);
};
}
var workerExports;
Expand Down
37 changes: 28 additions & 9 deletions src/core/worker/WorkerPool.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { requestAnimFrame } from '../util';
// import { requestAnimFrame } from '../util';
import { setWorkerPool, setWorkersCreated } from './CoreWorkers';
import { getWorkerSourcePath } from './Worker';
import globalConfig from '../../globalConfig';

const hardwareConcurrency = typeof window !== 'undefined' ? (window.navigator.hardwareConcurrency || 4) : 0;
const workerCount = Math.max(Math.floor(hardwareConcurrency / 2), 1);
Expand Down Expand Up @@ -42,7 +43,7 @@ class MessageBatch {
export default class WorkerPool {
constructor() {
this.active = {};
this.workerCount = typeof window !== 'undefined' ? (window.MAPTALKS_WORKER_COUNT || workerCount) : 0;
this.workerCount = typeof window !== 'undefined' ? (globalConfig.workerCount || window.MAPTALKS_WORKER_COUNT || workerCount) : 0;
this._messages = [];
this._messageBuffers = [];
}
Expand Down Expand Up @@ -101,6 +102,24 @@ export default class WorkerPool {
}
}
}

getWorkers() {
return this.workers || [];
}

broadcastMessage(message) {
const workers = this.getWorkers();
workers.forEach(worker => {
worker.postMessage({ messageType: message, messageCount: globalConfig.workerConcurrencyCount });
});
return this;
}

broadcastIdleMessage() {
this.broadcastMessage('idle');
return this;
}

}

let globalWorkerPool;
Expand All @@ -112,10 +131,10 @@ export function getGlobalWorkerPool() {
return globalWorkerPool;
}

function frameLoop() {
getGlobalWorkerPool().commit();
requestAnimFrame(frameLoop);
}
if (requestAnimFrame) {
requestAnimFrame(frameLoop);
}
// function frameLoop() {
// getGlobalWorkerPool().commit();
// requestAnimFrame(frameLoop);
// }
// if (requestAnimFrame) {
// requestAnimFrame(frameLoop);
// }
54 changes: 53 additions & 1 deletion src/geometry/GeoJSON.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
isString,
parseJSON,
isArrayHasData,
pushIn
pushIn,
isNumber
} from '../core/util';
import Marker from './Marker';
import LineString from './LineString';
Expand All @@ -14,6 +15,8 @@ import MultiPolygon from './MultiPolygon';
import GeometryCollection from './GeometryCollection';
import Geometry from './Geometry';
import { GEOJSON_TYPES } from '../core/Constants';
import PromisePolyfill from './../core/Promise';
import { runTaskAsync } from '../core/MicroTask';

const types = {
'Marker': Marker,
Expand Down Expand Up @@ -96,6 +99,55 @@ const GeoJSON = {
}

},
/**
* async Convert one or more GeoJSON objects to geometry
* @param {String|Object|Object[]} geoJSON - GeoJSON objects or GeoJSON string
* @param {Function} [foreachFn=undefined] - callback function for each geometry
* @param {Number} [countPerTime=2000] - Number of graphics converted per time
* @return {Promise}
* @example
* GeoJSON.toGeometryAsync(geoJSON).then(geos=>{
* console.log(geos);
* })
* */
toGeometryAsync(geoJSON, foreachFn, countPerTime = 2000) {
if (isString(geoJSON)) {
geoJSON = parseJSON(geoJSON);
}
return new PromisePolyfill((resolve) => {
const resultGeos = [];
if (geoJSON && (Array.isArray(geoJSON) || Array.isArray(geoJSON.features))) {
const pageSize = isNumber(countPerTime) ? Math.round(countPerTime) : 2000;
const features = geoJSON.features || geoJSON;
const count = Math.ceil(features.length / pageSize);
let page = 1;
const run = () => {
const startIndex = (page - 1) * pageSize, endIndex = (page) * pageSize;
const fs = features.slice(startIndex, endIndex);
const geos = GeoJSON.toGeometry(fs, foreachFn);
page++;
return geos;
};
runTaskAsync({ count, run }).then((geoList) => {
for (let i = 0, len = geoList.length; i < len; i++) {
const geo = geoList[i];
if (!geo) {
continue;
}
if (Array.isArray(geo)) {
pushIn(resultGeos, geo);
} else {
resultGeos.push(geo);
}
}
resolve(resultGeos);
});
} else {
const geo = GeoJSON.toGeometry(geoJSON, foreachFn);
resolve(geo);
}
});
},

/**
* Convert single GeoJSON object
Expand Down
21 changes: 21 additions & 0 deletions src/globalConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* global config
* idle/worker etc
*/
const globalConfig = {
//dev env
dev: false,
//test env
test: false,
//idle logging
idleLog: false,
//idle 时间阈值
idleTimeRemaining: 8,
//idle 超时阈值
idleTimeout: 1000,
//worker 数量
workerCount: 0,
//worker 通信并发数量
workerConcurrencyCount: 5
};
export default globalConfig;
4 changes: 3 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { version } from '../package.json';
export { default as globalconfig } from './globalConfig';
export * from './core/Constants';
export { default as Browser } from './core/Browser';
import * as Util from './core/util';
import * as DomUtil from './core/util/dom';
import * as StringUtil from './core/util/strings';
import * as MapboxUtil from './core/mapbox';
export { Util, DomUtil, StringUtil, MapboxUtil };
import * as MicroTask from './core/MicroTask';
export { Util, DomUtil, StringUtil, MapboxUtil, MicroTask };
export { default as LRUCache } from './core/util/LRUCache';
export { default as Ajax } from './core/Ajax';
export { default as Canvas } from './core/Canvas';
Expand Down