fix(client): execute web workers concurrently
This commit is contained in:
committed by
mrugesh mohapatra
parent
e8f5b54d63
commit
845b966bda
@ -95,7 +95,7 @@ async function transformSASS(element) {
|
||||
await Promise.all(
|
||||
[].map.call(styleTags, async style => {
|
||||
style.type = 'text/css';
|
||||
style.innerHTML = await sassWorker.execute(style.innerHTML, 5000);
|
||||
style.innerHTML = await sassWorker.execute(style.innerHTML, 5000).done;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -90,19 +90,12 @@ export function getTestRunner(buildData, proxyLogger, document) {
|
||||
function getJSTestRunner({ build, sources }, proxyLogger) {
|
||||
const code = sources && 'index' in sources ? sources['index'] : '';
|
||||
|
||||
const testWorker = createWorker('test-evaluator');
|
||||
const testWorker = createWorker('test-evaluator', { terminateWorker: true });
|
||||
|
||||
return async (testString, testTimeout) => {
|
||||
try {
|
||||
testWorker.on('LOG', proxyLogger);
|
||||
return await testWorker.execute(
|
||||
{ build, testString, code, sources },
|
||||
testTimeout
|
||||
);
|
||||
} finally {
|
||||
testWorker.killWorker();
|
||||
testWorker.remove('LOG', proxyLogger);
|
||||
}
|
||||
return (testString, testTimeout) => {
|
||||
return testWorker
|
||||
.execute({ build, testString, code, sources }, testTimeout)
|
||||
.on('LOG', proxyLogger).done;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,101 +1,153 @@
|
||||
class WorkerExecutor {
|
||||
constructor(workerName, location) {
|
||||
this.workerName = workerName;
|
||||
this.worker = null;
|
||||
this.observers = {};
|
||||
this.location = location;
|
||||
constructor(
|
||||
workerName,
|
||||
{ location = '/js/', concurrency = 2, terminateWorker = false } = {}
|
||||
) {
|
||||
this._workerName = workerName;
|
||||
this._workers = [];
|
||||
this._queue = [];
|
||||
this._running = 0;
|
||||
this._concurrency = concurrency;
|
||||
this._terminateWorker = terminateWorker;
|
||||
this._location = location;
|
||||
|
||||
this.execute = this.execute.bind(this);
|
||||
this.killWorker = this.killWorker.bind(this);
|
||||
this.getWorker = this.getWorker.bind(this);
|
||||
this._getWorker = this._getWorker.bind(this);
|
||||
}
|
||||
|
||||
async getWorker() {
|
||||
if (this.worker === null) {
|
||||
this.worker = await new Promise((resolve, reject) => {
|
||||
const worker = new Worker(`${this.location}${this.workerName}.js`);
|
||||
async _getWorker() {
|
||||
let worker;
|
||||
if (this._workers.length) {
|
||||
worker = this._workers.shift();
|
||||
} else {
|
||||
worker = await new Promise((resolve, reject) => {
|
||||
const worker = new Worker(`${this._location}${this._workerName}.js`);
|
||||
worker.onmessage = e => {
|
||||
if (e.data && e.data.type && e.data.type === 'contentLoaded') {
|
||||
resolve(worker);
|
||||
}
|
||||
};
|
||||
worker.onerror = e => reject(e.message);
|
||||
worker.onerror = err => reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
return this.worker;
|
||||
return worker;
|
||||
}
|
||||
|
||||
killWorker() {
|
||||
if (this.worker !== null) {
|
||||
this.worker.terminate();
|
||||
this.worker = null;
|
||||
_pushTask(task) {
|
||||
this._queue.push(task);
|
||||
this._next();
|
||||
}
|
||||
|
||||
_handleTaskEnd(task) {
|
||||
return () => {
|
||||
this._running--;
|
||||
if (task._worker) {
|
||||
const worker = task._worker;
|
||||
if (this._terminateWorker) {
|
||||
worker.terminate();
|
||||
} else {
|
||||
worker.onmessage = null;
|
||||
worker.onerror = null;
|
||||
this._workers.push(worker);
|
||||
}
|
||||
}
|
||||
this._next();
|
||||
};
|
||||
}
|
||||
|
||||
_next() {
|
||||
while (this._running < this._concurrency && this._queue.length) {
|
||||
const task = this._queue.shift();
|
||||
const handleTaskEnd = this._handleTaskEnd(task);
|
||||
task._execute(this._getWorker).done.then(handleTaskEnd, handleTaskEnd);
|
||||
this._running++;
|
||||
}
|
||||
}
|
||||
|
||||
async execute(data, timeout = 1000) {
|
||||
const worker = await this.getWorker();
|
||||
return new Promise((resolve, reject) => {
|
||||
// Handle timeout
|
||||
execute(data, timeout = 1000) {
|
||||
const task = eventify({});
|
||||
task._execute = function(getWorker) {
|
||||
getWorker().then(
|
||||
worker => {
|
||||
task._worker = worker;
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.killWorker();
|
||||
done('timeout');
|
||||
task._worker.terminate();
|
||||
task._worker = null;
|
||||
this.emit('error', { message: 'timeout' });
|
||||
}, timeout);
|
||||
|
||||
const done = (err, data) => {
|
||||
clearTimeout(timeoutId);
|
||||
this.remove('error', handleError);
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = e => {
|
||||
done(e.message);
|
||||
};
|
||||
this.on('error', handleError);
|
||||
|
||||
worker.postMessage(data);
|
||||
|
||||
// Handle result
|
||||
worker.onmessage = e => {
|
||||
if (e.data && e.data.type) {
|
||||
this.handleEvent(e.data.type, e.data.data);
|
||||
this.emit(e.data.type, e.data.data);
|
||||
return;
|
||||
}
|
||||
done(null, e.data);
|
||||
clearTimeout(timeoutId);
|
||||
this.emit('done', e.data);
|
||||
};
|
||||
|
||||
worker.onerror = e => {
|
||||
this.handleEvent('error', { message: e.message });
|
||||
clearTimeout(timeoutId);
|
||||
this.emit('error', { message: e.message });
|
||||
};
|
||||
|
||||
worker.postMessage(data);
|
||||
},
|
||||
err => this.emit('error', err)
|
||||
);
|
||||
return this;
|
||||
};
|
||||
|
||||
task.done = new Promise((resolve, reject) => {
|
||||
task
|
||||
.once('done', data => resolve(data))
|
||||
.once('error', err => reject(err.message));
|
||||
});
|
||||
|
||||
this._pushTask(task);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
const eventify = self => {
|
||||
self._events = {};
|
||||
|
||||
self.on = (event, listener) => {
|
||||
if (typeof self._events[event] === 'undefined') {
|
||||
self._events[event] = [];
|
||||
}
|
||||
self._events[event].push(listener);
|
||||
return self;
|
||||
};
|
||||
|
||||
self.removeListener = (event, listener) => {
|
||||
if (typeof self._events[event] !== 'undefined') {
|
||||
const index = self._events[event].indexOf(listener);
|
||||
if (index !== -1) {
|
||||
self._events[event].splice(index, 1);
|
||||
}
|
||||
}
|
||||
return self;
|
||||
};
|
||||
|
||||
self.emit = (event, ...args) => {
|
||||
if (typeof self._events[event] !== 'undefined') {
|
||||
self._events[event].forEach(listener => {
|
||||
listener.apply(self, args);
|
||||
});
|
||||
}
|
||||
return self;
|
||||
};
|
||||
|
||||
handleEvent(type, data) {
|
||||
const observers = this.observers[type] || [];
|
||||
for (const observer of observers) {
|
||||
observer(data);
|
||||
}
|
||||
}
|
||||
self.once = (event, listener) => {
|
||||
self.on(event, function handler(...args) {
|
||||
self.removeListener(handler);
|
||||
listener.apply(self, args);
|
||||
});
|
||||
return self;
|
||||
};
|
||||
|
||||
on(type, callback) {
|
||||
const observers = this.observers[type] || [];
|
||||
observers.push(callback);
|
||||
this.observers[type] = observers;
|
||||
}
|
||||
return self;
|
||||
};
|
||||
|
||||
remove(type, callback) {
|
||||
const observers = this.observers[type] || [];
|
||||
const index = observers.indexOf(callback);
|
||||
if (index !== -1) {
|
||||
observers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function createWorkerExecutor(workerName, location = '/js/') {
|
||||
return new WorkerExecutor(workerName, location);
|
||||
export default function createWorkerExecutor(workerName, options) {
|
||||
return new WorkerExecutor(workerName, options);
|
||||
}
|
||||
|
@ -333,13 +333,13 @@ async function createTestRunnerForJSChallenge({ files }, solution) {
|
||||
const { build, sources } = await buildJSChallenge({ files });
|
||||
const code = sources && 'index' in sources ? sources['index'] : '';
|
||||
|
||||
const testWorker = createWorker('test-evaluator');
|
||||
const testWorker = createWorker('test-evaluator', { terminateWorker: true });
|
||||
return async ({ text, testString }) => {
|
||||
try {
|
||||
const { pass, err } = await testWorker.execute(
|
||||
{ testString, build, code, sources },
|
||||
5000
|
||||
);
|
||||
).done;
|
||||
if (!pass) {
|
||||
throw new AssertionError(`${text}\n${err.message}`);
|
||||
}
|
||||
@ -348,8 +348,6 @@ async function createTestRunnerForJSChallenge({ files }, solution) {
|
||||
? `${text}\n${err}`
|
||||
: (err.message = `${text}
|
||||
${err.message}`);
|
||||
} finally {
|
||||
testWorker.killWorker();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user