Include pixelworks and terminate workers before creating new ones

This commit is contained in:
Tim Schaub
2020-05-16 21:33:39 -06:00
parent bfc035415e
commit b16c2e0062
6 changed files with 660 additions and 19 deletions

View File

@@ -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();
}
}
}
/**