Include pixelworks and terminate workers before creating new ones
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* @module ol/source/Raster
|
||||
*/
|
||||
import Disposable from '../Disposable.js';
|
||||
import Event from '../events/Event.js';
|
||||
import EventType from '../events/EventType.js';
|
||||
import ImageCanvas from '../ImageCanvas.js';
|
||||
@@ -11,12 +12,354 @@ import SourceState from './State.js';
|
||||
import TileLayer from '../layer/Tile.js';
|
||||
import TileQueue from '../TileQueue.js';
|
||||
import TileSource from './Tile.js';
|
||||
import {Processor} from 'pixelworks/lib/index.js';
|
||||
import {assign} from '../obj.js';
|
||||
import {createCanvasContext2D} from '../dom.js';
|
||||
import {create as createTransform} from '../transform.js';
|
||||
import {equals, getCenter, getHeight, getWidth} from '../extent.js';
|
||||
|
||||
let hasImageData = true;
|
||||
try {
|
||||
new ImageData(10, 10);
|
||||
} catch (_) {
|
||||
hasImageData = false;
|
||||
}
|
||||
|
||||
const context = document.createElement('canvas').getContext('2d');
|
||||
|
||||
/**
|
||||
* @param {Uint8ClampedArray} data Image data.
|
||||
* @param {number} width Number of columns.
|
||||
* @param {number} height Number of rows.
|
||||
* @return {ImageData} Image data.
|
||||
*/
|
||||
export function newImageData(data, width, height) {
|
||||
if (hasImageData) {
|
||||
return new ImageData(data, width, height);
|
||||
} else {
|
||||
const imageData = context.createImageData(width, height);
|
||||
imageData.data.set(data);
|
||||
return imageData;
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
/**
|
||||
* Create a function for running operations. This function is serialized for
|
||||
* use in a worker.
|
||||
* @param {function(Array, Object):*} operation The operation.
|
||||
* @return {function(Object):ArrayBuffer} A function that takes an object with
|
||||
* buffers, meta, imageOps, width, and height properties and returns an array
|
||||
* buffer.
|
||||
*/
|
||||
function createMinion(operation) {
|
||||
let workerHasImageData = true;
|
||||
try {
|
||||
new ImageData(10, 10);
|
||||
} catch (_) {
|
||||
workerHasImageData = false;
|
||||
}
|
||||
|
||||
function newWorkerImageData(data, width, height) {
|
||||
if (workerHasImageData) {
|
||||
return new ImageData(data, width, height);
|
||||
} else {
|
||||
return {data: data, width: width, height: height};
|
||||
}
|
||||
}
|
||||
|
||||
return function (data) {
|
||||
// bracket notation for minification support
|
||||
const buffers = data['buffers'];
|
||||
const meta = data['meta'];
|
||||
const imageOps = data['imageOps'];
|
||||
const width = data['width'];
|
||||
const height = data['height'];
|
||||
|
||||
const numBuffers = buffers.length;
|
||||
const numBytes = buffers[0].byteLength;
|
||||
let output, b;
|
||||
|
||||
if (imageOps) {
|
||||
const images = new Array(numBuffers);
|
||||
for (b = 0; b < numBuffers; ++b) {
|
||||
images[b] = newWorkerImageData(
|
||||
new Uint8ClampedArray(buffers[b]),
|
||||
width,
|
||||
height
|
||||
);
|
||||
}
|
||||
output = operation(images, meta).data;
|
||||
} else {
|
||||
output = new Uint8ClampedArray(numBytes);
|
||||
const arrays = new Array(numBuffers);
|
||||
const pixels = new Array(numBuffers);
|
||||
for (b = 0; b < numBuffers; ++b) {
|
||||
arrays[b] = new Uint8ClampedArray(buffers[b]);
|
||||
pixels[b] = [0, 0, 0, 0];
|
||||
}
|
||||
for (let i = 0; i < numBytes; i += 4) {
|
||||
for (let j = 0; j < numBuffers; ++j) {
|
||||
const array = arrays[j];
|
||||
pixels[j][0] = array[i];
|
||||
pixels[j][1] = array[i + 1];
|
||||
pixels[j][2] = array[i + 2];
|
||||
pixels[j][3] = array[i + 3];
|
||||
}
|
||||
const pixel = operation(pixels, meta);
|
||||
output[i] = pixel[0];
|
||||
output[i + 1] = pixel[1];
|
||||
output[i + 2] = pixel[2];
|
||||
output[i + 3] = pixel[3];
|
||||
}
|
||||
}
|
||||
return output.buffer;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a worker for running operations.
|
||||
* @param {Object} config Configuration.
|
||||
* @param {function(MessageEvent): void} onMessage Called with a message event.
|
||||
* @return {Worker} The worker.
|
||||
*/
|
||||
function createWorker(config, onMessage) {
|
||||
const lib = Object.keys(config.lib || {}).map(function (name) {
|
||||
return 'var ' + name + ' = ' + config.lib[name].toString() + ';';
|
||||
});
|
||||
|
||||
const lines = lib.concat([
|
||||
'var __minion__ = (' + createMinion.toString() + ')(',
|
||||
config.operation.toString(),
|
||||
');',
|
||||
'self.addEventListener("message", function(event) {',
|
||||
' var buffer = __minion__(event.data);',
|
||||
' self.postMessage({buffer: buffer, meta: event.data.meta}, [buffer]);',
|
||||
'});',
|
||||
]);
|
||||
|
||||
const blob = new Blob(lines, {type: 'text/javascript'});
|
||||
const source = URL.createObjectURL(blob);
|
||||
const worker = new Worker(source);
|
||||
worker.addEventListener('message', onMessage);
|
||||
return worker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} FauxMessageEvent
|
||||
* @property {Object} data Message data.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a faux worker for running operations.
|
||||
* @param {ProcessorOptions} config Configuration.
|
||||
* @param {function(FauxMessageEvent): void} onMessage Called with a message event.
|
||||
* @return {Object} The faux worker.
|
||||
*/
|
||||
function createFauxWorker(config, onMessage) {
|
||||
const minion = createMinion(config.operation);
|
||||
let terminated = false;
|
||||
return {
|
||||
postMessage: function (data) {
|
||||
setTimeout(function () {
|
||||
if (terminated) {
|
||||
return;
|
||||
}
|
||||
onMessage({data: {buffer: minion(data), meta: data['meta']}});
|
||||
}, 0);
|
||||
},
|
||||
terminate: function () {
|
||||
terminated = true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} ProcessorOptions
|
||||
* @property {number} threads Number of workers to spawn.
|
||||
* @property {function(Array, Object):*} operation The operation.
|
||||
* @property {Object} [lib] Functions that will be made available to operations run in a worker.
|
||||
* @property {number} queue The number of queued jobs to allow.
|
||||
* @property {boolean} [imageOps=false] Pass all the image data to the operation instead of a single pixel.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @classdesc
|
||||
* A processor runs pixel or image operations in workers.
|
||||
*/
|
||||
export class Processor extends Disposable {
|
||||
/**
|
||||
* @param {ProcessorOptions} config Configuration.
|
||||
*/
|
||||
constructor(config) {
|
||||
super();
|
||||
|
||||
this._imageOps = !!config.imageOps;
|
||||
let threads;
|
||||
if (config.threads === 0) {
|
||||
threads = 0;
|
||||
} else if (this._imageOps) {
|
||||
threads = 1;
|
||||
} else {
|
||||
threads = config.threads || 1;
|
||||
}
|
||||
const workers = [];
|
||||
if (threads) {
|
||||
for (let i = 0; i < threads; ++i) {
|
||||
workers[i] = createWorker(config, this._onWorkerMessage.bind(this, i));
|
||||
}
|
||||
} else {
|
||||
workers[0] = createFauxWorker(
|
||||
config,
|
||||
this._onWorkerMessage.bind(this, 0)
|
||||
);
|
||||
}
|
||||
this._workers = workers;
|
||||
this._queue = [];
|
||||
this._maxQueueLength = config.queue || Infinity;
|
||||
this._running = 0;
|
||||
this._dataLookup = {};
|
||||
this._job = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run operation on input data.
|
||||
* @param {Array.<Array|ImageData>} inputs Array of pixels or image data
|
||||
* (depending on the operation type).
|
||||
* @param {Object} meta A user data object. This is passed to all operations
|
||||
* and must be serializable.
|
||||
* @param {function(Error, ImageData, Object): void} callback Called when work
|
||||
* completes. The first argument is any error. The second is the ImageData
|
||||
* generated by operations. The third is the user data object.
|
||||
*/
|
||||
process(inputs, meta, callback) {
|
||||
this._enqueue({
|
||||
inputs: inputs,
|
||||
meta: meta,
|
||||
callback: callback,
|
||||
});
|
||||
this._dispatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a job to the queue.
|
||||
* @param {Object} job The job.
|
||||
*/
|
||||
_enqueue(job) {
|
||||
this._queue.push(job);
|
||||
while (this._queue.length > this._maxQueueLength) {
|
||||
this._queue.shift().callback(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a job.
|
||||
*/
|
||||
_dispatch() {
|
||||
if (this._running === 0 && this._queue.length > 0) {
|
||||
const job = this._queue.shift();
|
||||
this._job = job;
|
||||
const width = job.inputs[0].width;
|
||||
const height = job.inputs[0].height;
|
||||
const buffers = job.inputs.map(function (input) {
|
||||
return input.data.buffer;
|
||||
});
|
||||
const threads = this._workers.length;
|
||||
this._running = threads;
|
||||
if (threads === 1) {
|
||||
this._workers[0].postMessage(
|
||||
{
|
||||
buffers: buffers,
|
||||
meta: job.meta,
|
||||
imageOps: this._imageOps,
|
||||
width: width,
|
||||
height: height,
|
||||
},
|
||||
buffers
|
||||
);
|
||||
} else {
|
||||
const length = job.inputs[0].data.length;
|
||||
const segmentLength = 4 * Math.ceil(length / 4 / threads);
|
||||
for (let i = 0; i < threads; ++i) {
|
||||
const offset = i * segmentLength;
|
||||
const slices = [];
|
||||
for (let j = 0, jj = buffers.length; j < jj; ++j) {
|
||||
slices.push(buffers[i].slice(offset, offset + segmentLength));
|
||||
}
|
||||
this._workers[i].postMessage(
|
||||
{
|
||||
buffers: slices,
|
||||
meta: job.meta,
|
||||
imageOps: this._imageOps,
|
||||
width: width,
|
||||
height: height,
|
||||
},
|
||||
slices
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle messages from the worker.
|
||||
* @param {number} index The worker index.
|
||||
* @param {MessageEvent} event The message event.
|
||||
*/
|
||||
_onWorkerMessage(index, event) {
|
||||
if (this.disposed) {
|
||||
return;
|
||||
}
|
||||
this._dataLookup[index] = event.data;
|
||||
--this._running;
|
||||
if (this._running === 0) {
|
||||
this._resolveJob();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a job. If there are no more worker threads, the processor callback
|
||||
* will be called.
|
||||
*/
|
||||
_resolveJob() {
|
||||
const job = this._job;
|
||||
const threads = this._workers.length;
|
||||
let data, meta;
|
||||
if (threads === 1) {
|
||||
data = new Uint8ClampedArray(this._dataLookup[0]['buffer']);
|
||||
meta = this._dataLookup[0]['meta'];
|
||||
} else {
|
||||
const length = job.inputs[0].data.length;
|
||||
data = new Uint8ClampedArray(length);
|
||||
meta = new Array(length);
|
||||
const segmentLength = 4 * Math.ceil(length / 4 / threads);
|
||||
for (let i = 0; i < threads; ++i) {
|
||||
const buffer = this._dataLookup[i]['buffer'];
|
||||
const offset = i * segmentLength;
|
||||
data.set(new Uint8ClampedArray(buffer), offset);
|
||||
meta[i] = this._dataLookup[i]['meta'];
|
||||
}
|
||||
}
|
||||
this._job = null;
|
||||
this._dataLookup = {};
|
||||
job.callback(
|
||||
null,
|
||||
newImageData(data, job.inputs[0].width, job.inputs[0].height),
|
||||
meta
|
||||
);
|
||||
this._dispatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate all workers associated with the processor.
|
||||
*/
|
||||
disposeInternal() {
|
||||
for (let i = 0; i < this._workers.length; ++i) {
|
||||
this._workers[i].terminate();
|
||||
}
|
||||
this._workers.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that takes an array of input data, performs some operation, and
|
||||
* returns an array of output data.
|
||||
@@ -141,9 +484,9 @@ class RasterSource extends ImageSource {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {*}
|
||||
* @type {Processor}
|
||||
*/
|
||||
this.worker_ = null;
|
||||
this.processor_ = null;
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -259,7 +602,11 @@ class RasterSource extends ImageSource {
|
||||
* @api
|
||||
*/
|
||||
setOperation(operation, opt_lib) {
|
||||
this.worker_ = new Processor({
|
||||
if (this.processor_) {
|
||||
this.processor_.dispose();
|
||||
}
|
||||
|
||||
this.processor_ = new Processor({
|
||||
operation: operation,
|
||||
imageOps: this.operationType_ === RasterOperationType.IMAGE,
|
||||
queue: 1,
|
||||
@@ -385,7 +732,7 @@ class RasterSource extends ImageSource {
|
||||
this.dispatchEvent(
|
||||
new RasterSourceEvent(RasterEventType.BEFOREOPERATIONS, frameState, data)
|
||||
);
|
||||
this.worker_.process(
|
||||
this.processor_.process(
|
||||
imageDatas,
|
||||
data,
|
||||
this.onWorkerComplete_.bind(this, frameState)
|
||||
@@ -445,6 +792,12 @@ class RasterSource extends ImageSource {
|
||||
getImageInternal() {
|
||||
return null; // not implemented
|
||||
}
|
||||
|
||||
disposeInternal() {
|
||||
if (this.processor_) {
|
||||
this.processor_.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user