Merge pull request #1683 from mzgoddard/sequencer-thread-cleanup

Sequencer thread cleanup
This commit is contained in:
Karishma Chadha 2018-11-07 17:15:11 -05:00 committed by GitHub
commit 4bb2d19584
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 98 additions and 59 deletions

View file

@ -241,6 +241,13 @@ class Runtime extends EventEmitter {
*/
this._nonMonitorThreadCount = 0;
/**
* All threads that finished running and were removed from this.threads
* by behaviour in Sequencer.stepThreads.
* @type {Array<Thread>}
*/
this._lastStepDoneThreads = null;
/**
* Currently known number of clones, used to enforce clone limit.
* @type {number}
@ -1663,6 +1670,9 @@ class Runtime extends EventEmitter {
this._emitProjectRunStatus(
this.threads.length + doneThreads.length -
this._getMonitorThreadCount([...this.threads, ...doneThreads]));
// Store threads that completed this iteration for testing and other
// internal purposes.
this._lastStepDoneThreads = doneThreads;
if (this.renderer) {
// @todo: Only render when this.redrawRequested or clones rendered.
if (this.profiler !== null) {

View file

@ -74,7 +74,7 @@ class Sequencer {
let numActiveThreads = Infinity;
// Whether `stepThreads` has run through a full single tick.
let ranFirstTick = false;
const doneThreads = this.runtime.threads.map(() => null);
const doneThreads = [];
// Conditions for continuing to stepping threads:
// 1. We must have threads in the list, and some must be active.
// 2. Time elapsed must be less than WORK_TIME.
@ -91,19 +91,17 @@ class Sequencer {
}
numActiveThreads = 0;
let stoppedThread = false;
// Attempt to run each thread one time.
for (let i = 0; i < this.runtime.threads.length; i++) {
const activeThread = this.runtime.threads[i];
// Check if the thread is done so it is not executed.
if (activeThread.stack.length === 0 ||
activeThread.status === Thread.STATUS_DONE) {
// Finished with this thread.
doneThreads[i] = activeThread;
stoppedThread = true;
continue;
}
// A thread was removed, added or this thread was restarted.
if (doneThreads[i] !== null) {
doneThreads[i] = null;
}
if (activeThread.status === Thread.STATUS_YIELD_TICK &&
!ranFirstTick) {
// Clear single-tick yield from the last call of `stepThreads`.
@ -130,6 +128,13 @@ class Sequencer {
if (activeThread.status === Thread.STATUS_RUNNING) {
numActiveThreads++;
}
// Check if the thread completed while it just stepped to make
// sure we remove it before the next iteration of all threads.
if (activeThread.stack.length === 0 ||
activeThread.status === Thread.STATUS_DONE) {
// Finished with this thread.
stoppedThread = true;
}
}
// We successfully ticked once. Prevents running STATUS_YIELD_TICK
// threads on the next tick.
@ -138,28 +143,23 @@ class Sequencer {
if (this.runtime.profiler !== null) {
this.runtime.profiler.stop();
}
}
// Filter inactive threads from `this.runtime.threads`.
numActiveThreads = 0;
for (let i = 0; i < this.runtime.threads.length; i++) {
const thread = this.runtime.threads[i];
if (doneThreads[i] === null) {
this.runtime.threads[numActiveThreads] = thread;
numActiveThreads++;
}
}
this.runtime.threads.length = numActiveThreads;
// Filter undefined and null values from `doneThreads`.
let numDoneThreads = 0;
for (let i = 0; i < doneThreads.length; i++) {
const maybeThread = doneThreads[i];
if (maybeThread !== null) {
doneThreads[numDoneThreads] = maybeThread;
numDoneThreads++;
// Filter inactive threads from `this.runtime.threads`.
if (stoppedThread) {
let nextActiveThread = 0;
for (let i = 0; i < this.runtime.threads.length; i++) {
const thread = this.runtime.threads[i];
if (thread.stack.length !== 0 &&
thread.status !== Thread.STATUS_DONE) {
this.runtime.threads[nextActiveThread] = thread;
nextActiveThread++;
} else {
doneThreads.push(thread);
}
}
this.runtime.threads.length = nextActiveThread;
}
}
doneThreads.length = numDoneThreads;
return doneThreads;
}

View file

@ -45,15 +45,19 @@ test('edge activated hat thread runs once every frame', t => {
t.equal(vm.runtime.threads.length, 0);
vm.runtime._step();
t.equal(vm.runtime.threads.length, 1);
checkIsHatThread(t, vm, vm.runtime.threads[0]);
t.assert(vm.runtime.threads[0].status === Thread.STATUS_DONE);
let threads = vm.runtime._lastStepDoneThreads;
t.equal(vm.runtime.threads.length, 0);
t.equal(threads.length, 1);
checkIsHatThread(t, vm, threads[0]);
t.assert(threads[0].status === Thread.STATUS_DONE);
// Check that the hat thread is added again when another step is taken
vm.runtime._step();
t.equal(vm.runtime.threads.length, 1);
checkIsHatThread(t, vm, vm.runtime.threads[0]);
t.assert(vm.runtime.threads[0].status === Thread.STATUS_DONE);
threads = vm.runtime._lastStepDoneThreads;
t.equal(vm.runtime.threads.length, 0);
t.equal(threads.length, 1);
checkIsHatThread(t, vm, threads[0]);
t.assert(threads[0].status === Thread.STATUS_DONE);
t.end();
});
});
@ -79,15 +83,19 @@ test('edge activated hat thread not added twice', t => {
t.equal(vm.runtime.threads.length, 0);
vm.runtime._step();
let doneThreads = vm.runtime._lastStepDoneThreads;
t.equal(vm.runtime.threads.length, 1);
t.equal(doneThreads.length, 0);
const prevThread = vm.runtime.threads[0];
checkIsHatThread(t, vm, vm.runtime.threads[0]);
t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING);
// Check that no new threads are added when another step is taken
vm.runtime._step();
doneThreads = vm.runtime._lastStepDoneThreads;
// There should now be one done hat thread and one new hat thread to run
t.equal(vm.runtime.threads.length, 1);
t.equal(doneThreads.length, 0);
checkIsHatThread(t, vm, vm.runtime.threads[0]);
t.assert(vm.runtime.threads[0] === prevThread);
t.end();
@ -115,29 +123,33 @@ test('edge activated hat thread does not interrupt stack click thread', t => {
t.equal(vm.runtime.threads.length, 0);
vm.runtime._step();
t.equal(vm.runtime.threads.length, 1);
checkIsHatThread(t, vm, vm.runtime.threads[0]);
t.assert(vm.runtime.threads[0].status === Thread.STATUS_DONE);
let doneThreads = vm.runtime._lastStepDoneThreads;
t.equal(vm.runtime.threads.length, 0);
t.equal(doneThreads.length, 1);
checkIsHatThread(t, vm, doneThreads[0]);
t.assert(doneThreads[0].status === Thread.STATUS_DONE);
// Add stack click thread on this hat
vm.runtime.toggleScript(vm.runtime.threads[0].topBlock, {stackClick: true});
vm.runtime.toggleScript(doneThreads[0].topBlock, {stackClick: true});
// Check that the hat thread is added again when another step is taken
vm.runtime._step();
t.equal(vm.runtime.threads.length, 2);
doneThreads = vm.runtime._lastStepDoneThreads;
t.equal(vm.runtime.threads.length, 0);
t.equal(doneThreads.length, 2);
let hatThread;
let stackClickThread;
if (vm.runtime.threads[0].stackClick) {
stackClickThread = vm.runtime.threads[0];
hatThread = vm.runtime.threads[1];
if (doneThreads[0].stackClick) {
stackClickThread = doneThreads[0];
hatThread = doneThreads[1];
} else {
stackClickThread = vm.runtime.threads[1];
hatThread = vm.runtime.threads[0];
stackClickThread = doneThreads[1];
hatThread = doneThreads[0];
}
checkIsHatThread(t, vm, hatThread);
checkIsStackClickThread(t, vm, stackClickThread);
t.assert(vm.runtime.threads[0].status === Thread.STATUS_DONE);
t.assert(vm.runtime.threads[1].status === Thread.STATUS_DONE);
t.assert(doneThreads[0].status === Thread.STATUS_DONE);
t.assert(doneThreads[1].status === Thread.STATUS_DONE);
t.end();
});
});
@ -163,7 +175,9 @@ test('edge activated hat thread does not interrupt stack click thread', t => {
t.equal(vm.runtime.threads.length, 0);
vm.runtime._step();
let doneThreads = vm.runtime._lastStepDoneThreads;
t.equal(vm.runtime.threads.length, 1);
t.equal(doneThreads.length, 0);
checkIsHatThread(t, vm, vm.runtime.threads[0]);
t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING);
@ -174,20 +188,22 @@ test('edge activated hat thread does not interrupt stack click thread', t => {
// Check that the hat thread is added again when another step is taken
vm.runtime._step();
t.equal(vm.runtime.threads.length, 2);
doneThreads = vm.runtime._lastStepDoneThreads;
t.equal(vm.runtime.threads.length, 0);
t.equal(doneThreads.length, 2);
let hatThread;
let stackClickThread;
if (vm.runtime.threads[0].stackClick) {
stackClickThread = vm.runtime.threads[0];
hatThread = vm.runtime.threads[1];
if (doneThreads[0].stackClick) {
stackClickThread = doneThreads[0];
hatThread = doneThreads[1];
} else {
stackClickThread = vm.runtime.threads[1];
hatThread = vm.runtime.threads[0];
stackClickThread = doneThreads[1];
hatThread = doneThreads[0];
}
checkIsHatThread(t, vm, hatThread);
checkIsStackClickThread(t, vm, stackClickThread);
t.assert(vm.runtime.threads[0].status === Thread.STATUS_DONE);
t.assert(vm.runtime.threads[1].status === Thread.STATUS_DONE);
t.assert(doneThreads[0].status === Thread.STATUS_DONE);
t.assert(doneThreads[1].status === Thread.STATUS_DONE);
t.end();
});
});

View file

@ -55,13 +55,19 @@ test('monitor thread runs every frame', t => {
t.equal(vm.runtime.threads.length, 0);
vm.runtime._step();
checkMonitorThreadPresent(t, vm.runtime.threads);
t.assert(vm.runtime.threads[0].status === Thread.STATUS_DONE);
let doneThreads = vm.runtime._lastStepDoneThreads;
t.equal(vm.runtime.threads.length, 0);
t.equal(doneThreads.length, 1);
checkMonitorThreadPresent(t, doneThreads);
t.assert(doneThreads[0].status === Thread.STATUS_DONE);
// Check that both are added again when another step is taken
vm.runtime._step();
checkMonitorThreadPresent(t, vm.runtime.threads);
t.assert(vm.runtime.threads[0].status === Thread.STATUS_DONE);
doneThreads = vm.runtime._lastStepDoneThreads;
t.equal(vm.runtime.threads.length, 0);
t.equal(doneThreads.length, 1);
checkMonitorThreadPresent(t, doneThreads);
t.assert(doneThreads[0].status === Thread.STATUS_DONE);
t.end();
});
});
@ -103,12 +109,18 @@ test('monitor thread not added twice', t => {
t.equal(vm.runtime.threads.length, 0);
vm.runtime._step();
let doneThreads = vm.runtime._lastStepDoneThreads;
t.equal(vm.runtime.threads.length, 1);
t.equal(doneThreads.length, 0);
checkMonitorThreadPresent(t, vm.runtime.threads);
t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING);
const prevThread = vm.runtime.threads[0];
// Check that both are added again when another step is taken
vm.runtime._step();
doneThreads = vm.runtime._lastStepDoneThreads;
t.equal(vm.runtime.threads.length, 1);
t.equal(doneThreads.length, 0);
checkMonitorThreadPresent(t, vm.runtime.threads);
t.equal(vm.runtime.threads[0], prevThread);
t.end();

View file

@ -14,8 +14,10 @@ test('importing sb2 project with monitors', t => {
// Evaluate playground data and exit
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
// All monitors should leave threads running
t.equal(threads.length, 5);
// All monitors should create threads that finish during the step and
// are revoved from runtime.threads.
t.equal(threads.length, 0);
t.equal(vm.runtime._lastStepDoneThreads.length, 5);
// There should be one additional hidden monitor that is in the monitorState but
// does not start a thread.
t.equal(vm.runtime._monitorState.size, 6);

View file

@ -181,8 +181,7 @@ test('stepThreads', t => {
t.strictEquals(s.stepThreads().length, 0);
generateThread(r);
t.strictEquals(r.threads.length, 1);
t.strictEquals(s.stepThreads().length, 0);
r.threads[0].status = Thread.STATUS_RUNNING;
// Threads should be marked DONE and removed in the same step they finish.
t.strictEquals(s.stepThreads().length, 1);
t.end();