diff --git a/src/web/App.mjs b/src/web/App.mjs index 9f1640e7..5735dca7 100755 --- a/src/web/App.mjs +++ b/src/web/App.mjs @@ -157,8 +157,6 @@ class App { action: "autobake", data: this.manager.input.getActiveTab() }); - - this.manager.controls.toggleBakeButtonFunction(false, true); } else { this.manager.controls.showStaleIndicator(); } diff --git a/src/web/ControlsWaiter.mjs b/src/web/ControlsWaiter.mjs index e7143875..e34cd911 100755 --- a/src/web/ControlsWaiter.mjs +++ b/src/web/ControlsWaiter.mjs @@ -60,7 +60,7 @@ class ControlsWaiter { if (btnBake.textContent.indexOf("Bake") > 0) { this.app.manager.input.bakeAll(); } else if (btnBake.textContent.indexOf("Cancel") > 0) { - this.manager.worker.cancelBake(); + this.manager.worker.cancelBake(false, true); } } @@ -72,12 +72,14 @@ class ControlsWaiter { if (this.manager.worker.step) { // Step has already been clicked so get the data from the output const activeTab = this.manager.input.getActiveTab(); - this.manager.worker.queueInput({ - input: this.manager.output.getOutput(activeTab, true), - inputNum: activeTab - }); + this.manager.worker.loadingOutputs++; this.app.progress = this.manager.output.outputs[activeTab].progress; this.app.bake(true); + this.manager.worker.queueInput({ + input: this.manager.output.getOutput(activeTab, true), + inputNum: activeTab, + bakeId: this.manager.worker.bakeId + }); } else { // First click of step, so get the output from the inputWorker this.manager.input.inputWorker.postMessage({ diff --git a/src/web/InputWaiter.mjs b/src/web/InputWaiter.mjs index 3d0fd5b1..3dc26bed 100644 --- a/src/web/InputWaiter.mjs +++ b/src/web/InputWaiter.mjs @@ -50,6 +50,7 @@ class InputWaiter { this.workerId = 0; this.maxWorkers = navigator.hardwareConcurrency || 4; this.maxTabs = 4; + this.inputTimeout = null; } /** @@ -266,8 +267,8 @@ class InputWaiter { case "queueInput": this.manager.worker.queueInput(r.data); break; - case "bake": - this.app.bake(r.data); + case "bakeAllInputs": + this.manager.worker.bakeAllInputs(r.data); break; case "displayTabSearchResults": this.displayTabSearchResults(r.data); @@ -332,11 +333,11 @@ class InputWaiter { const lines = inputData.input.length < (this.app.options.ioDisplayThreshold * 1024) ? inputData.input.count("\n") + 1 : null; this.setInputInfo(inputData.input.length, lines); + if (!silent) window.dispatchEvent(this.manager.statechange); } else { this.setFile(inputData); } - if (!silent) window.dispatchEvent(this.manager.statechange); }.bind(this)); } @@ -452,6 +453,8 @@ class InputWaiter { silent: false } }); + window.dispatchEvent(this.manager.statechange); + } } @@ -541,6 +544,38 @@ class InputWaiter { } + /** + * Debouncer to stop functions from being executed multiple times in a + * short space of time + * https://davidwalsh.name/javascript-debounce-function + * + * @param {function} func - The function to be executed after the debounce time + * @param {number} wait - The time (ms) to wait before executing the function + * @param {array} args - Array of arguments to be passed to func + * @returns {function} + */ + debounce(func, wait, args) { + return function() { + const context = this, + later = function() { + this.inputTimeout = null; + func.apply(context, args); + }; + clearTimeout(this.inputTimeout); + this.inputTimeout = setTimeout(later, wait); + }.bind(this); + } + + /** + * Handler for input change events. + * Debounces the input so we don't call autobake too often. + * + * @param {event} e + */ + debounceInputChange(e) { + this.debounce(this.inputChange.bind(this), 100, [e])(); + } + /** * Handler for input change events. * Updates the value stored in the inputWorker @@ -589,7 +624,7 @@ class InputWaiter { // and manually fire inputChange() e.preventDefault(); document.getElementById("input-text").value += pastedData; - this.inputChange(e); + this.debounceInputChange(e); } else { e.preventDefault(); e.stopPropagation(); @@ -1027,7 +1062,8 @@ class InputWaiter { * Resets the input, output and info areas, and creates a new inputWorker */ clearAllIoClick() { - this.manager.worker.cancelBake(); + this.manager.worker.cancelBake(true, true); + this.manager.worker.loaded = false; this.manager.output.removeAllOutputs(); this.manager.output.terminateZipWorker(); diff --git a/src/web/InputWorker.mjs b/src/web/InputWorker.mjs index ab88f705..67eef210 100644 --- a/src/web/InputWorker.mjs +++ b/src/web/InputWorker.mjs @@ -57,6 +57,9 @@ self.addEventListener("message", function(e) { case "bakeAll": self.bakeAllInputs(); break; + case "bakeNext": + self.bakeInput(r.data.inputNum, r.data.bakeId); + break; case "getLoadProgress": self.getLoadProgress(r.data); break; @@ -142,53 +145,60 @@ self.getLoadProgress = function(inputNum) { self.autoBake = function(inputNum, step=false) { const input = self.getInputObj(inputNum); if (input) { - let inputData = input.data; - if (typeof inputData !== "string") { - inputData = inputData.fileBuffer; - } self.postMessage({ - action: "queueInput", + action: "bakeAllInputs", data: { - input: inputData, - inputNum: parseInt(inputNum, 10), - override: false + nums: [parseInt(inputNum, 10)], + step: step } }); - self.postMessage({ - action: "bake", - data: step - }); } }; /** * Fired when we want to bake all inputs (bake button clicked) - * Queues all of the loaded inputs and sends a bake command + * Sends a list of inputNums to the workerwaiter */ self.bakeAllInputs = function() { - const inputNums = Object.keys(self.inputs); + const inputNums = Object.keys(self.inputs), + nums = []; for (let i = 0; i < inputNums.length; i++) { if (self.inputs[inputNums[i]].status === "loaded") { - let inputData = self.inputs[inputNums[i]].data; - if (typeof inputData !== "string") { - inputData = inputData.fileBuffer; - } - self.postMessage({ - action: "queueInput", - data: { - input: inputData, - inputNum: inputNums[i], - override: false - } - }); + nums.push(parseInt(inputNums[i], 10)); } } self.postMessage({ - action: "bake", - data: false + action: "bakeAllInputs", + data: { + nums: nums, + step: false + } }); +}; +/** + * Gets the data for the provided inputNum and sends it to the WorkerWaiter + * + * @param {number} inputNum + * @param {number} bakeId + */ +self.bakeInput = function(inputNum, bakeId) { + const inputObj = self.getInputObj(inputNum); + if (inputObj === null || inputObj === undefined) return; + if (inputObj.status !== "loaded") return; + + let inputData = inputObj.data; + if (typeof inputData !== "string") inputData = inputData.fileBuffer; + + self.postMessage({ + action: "queueInput", + data: { + input: inputData, + inputNum: inputNum, + bakeId: bakeId + } + }); }; /** @@ -789,6 +799,10 @@ self.removeInput = function(removeInputData) { delete self.inputs[inputNum]; + if (Object.keys(self.inputs).length === 0) { + self.addInput(true, "string"); + } + if (refreshTabs) { self.refreshTabs(inputNum, "left"); } diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index dd7f1a2a..66ac6f2d 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -144,7 +144,7 @@ class Manager { this.addDynamicListener("textarea.arg", "drop", this.recipe.textArgDrop, this.recipe); // Input - this.addMultiEventListener("#input-text", "keyup", this.input.inputChange, this.input); + this.addMultiEventListener("#input-text", "keyup", this.input.debounceInputChange, this.input); this.addMultiEventListener("#input-text", "paste", this.input.inputPaste, this.input); document.getElementById("reset-layout").addEventListener("click", this.app.resetLayout.bind(this.app)); document.getElementById("clr-io").addEventListener("click", this.input.clearAllIoClick.bind(this.input)); diff --git a/src/web/WorkerWaiter.mjs b/src/web/WorkerWaiter.mjs index b7f852e7..21ccae87 100644 --- a/src/web/WorkerWaiter.mjs +++ b/src/web/WorkerWaiter.mjs @@ -22,10 +22,13 @@ class WorkerWaiter { this.app = app; this.manager = manager; + this.loaded = false; this.chefWorkers = []; this.maxWorkers = navigator.hardwareConcurrency || 4; this.inputs = []; + this.inputNums = []; this.totalOutputs = 0; + this.loadingOutputs = 0; this.bakeId = 0; } @@ -47,14 +50,6 @@ class WorkerWaiter { * @returns {number} The index of the created worker */ addChefWorker() { - // First find if there are any inactive workers, as this will be - // more efficient than creating a new one - for (let i = 0; i < this.chefWorkers.length; i++) { - if (!this.chefWorkers[i].active) { - return i; - } - } - if (this.chefWorkers.length === this.maxWorkers) { // Can't create any more workers return -1; @@ -88,6 +83,22 @@ class WorkerWaiter { return this.chefWorkers.indexOf(newWorkerObj); } + /** + * Gets an inactive ChefWorker to be used for baking + * + * @param {boolean} [setActive=true] - If true, set the worker status to active + * @returns {number} - The index of the ChefWorker + */ + getInactiveChefWorker(setActive=true) { + for (let i = 0; i < this.chefWorkers.length; i++) { + if (!this.chefWorkers[i].active) { + this.chefWorkers[i].active = setActive; + return i; + } + } + return -1; + } + /** * Removes a ChefWorker * @@ -172,7 +183,12 @@ class WorkerWaiter { case "workerLoaded": this.app.workerLoaded = true; log.debug("ChefWorker loaded."); - this.app.loaded(); + if (!this.loaded) { + this.app.loaded(); + this.loaded = true; + } else { + this.bakeNextInput(this.getInactiveChefWorker(false)); + } break; case "statusMessage": // Status message should be done per output @@ -227,7 +243,7 @@ class WorkerWaiter { * Get the progress of the ChefWorkers */ getBakeProgress() { - const pendingInputs = this.inputs.length; + const pendingInputs = this.inputNums.length + this.loadingOutputs + this.inputs.length; let bakingInputs = 0; for (let i = 0; i < this.chefWorkers.length; i++) { @@ -249,12 +265,17 @@ class WorkerWaiter { /** * Cancels the current bake by terminating and removing all ChefWorkers + * + * @param {boolean} [silent=false] - If true, don't set the output + * @param {boolean} killAll - If true, kills all chefWorkers regardless of status */ - cancelBake() { + cancelBake(silent, killAll) { for (let i = this.chefWorkers.length - 1; i >= 0; i--) { - const inputNum = this.chefWorkers[i].inputNum; - this.removeChefWorker(this.chefWorkers[i]); - this.manager.output.updateOutputStatus("inactive", inputNum); + if (this.chefWorkers[i].active || killAll) { + const inputNum = this.chefWorkers[i].inputNum; + this.removeChefWorker(this.chefWorkers[i]); + this.manager.output.updateOutputStatus("inactive", inputNum); + } } this.setBakingStatus(false); @@ -262,23 +283,32 @@ class WorkerWaiter { this.manager.output.updateOutputStatus("inactive", this.inputs[i].inputNum); } + for (let i = 0; i < this.inputNums.length; i++) { + this.manager.output.updateOutputStatus("inactive", this.inputNums[i]); + } + this.inputs = []; + this.inputNums = []; this.totalOutputs = 0; - this.manager.output.set(this.manager.output.getActiveTab()); + if (!silent) this.manager.output.set(this.manager.output.getActiveTab()); } /** * Handle a worker completing baking + * + * @param {object} workerObj - Object containing the worker information + * @param {ChefWorker} workerObj.worker - The actual worker object + * @param {number} workerObj.inputNum - The inputNum of the input being baked by the worker + * @param {boolean} workerObj.active - If true, the worker is currrently baking an input */ workerFinished(workerObj) { + const workerIdx = this.chefWorkers.indexOf(workerObj); + this.chefWorkers[workerIdx].active = false; if (this.inputs.length > 0) { - this.bakeNextInput(this.chefWorkers.indexOf(workerObj)); - } else { + this.bakeNextInput(workerIdx); + } else if (this.inputNums.length === 0 && this.loadingOutputs === 0) { // The ChefWorker is no longer needed - log.debug("No more inputs to bake. Closing ChefWorker."); - workerObj.active = false; - this.removeChefWorker(workerObj); - + log.debug("No more inputs to bake."); const progress = this.getBakeProgress(); if (progress.total === progress.baked) { this.bakingComplete(); @@ -313,15 +343,15 @@ class WorkerWaiter { } /** - * Bakes the next input + * Bakes the next input and tells the inputWorker to load the next input * - * @param {number} workerIdx + * @param {number} workerIdx - The index of the worker to bake with */ bakeNextInput(workerIdx) { if (this.inputs.length === 0) return; if (workerIdx === -1) return; if (!this.chefWorkers[workerIdx]) return; - + this.chefWorkers[workerIdx].active = true; const nextInput = this.inputs.splice(0, 1)[0]; if (typeof nextInput.inputNum === "string") nextInput.inputNum = parseInt(nextInput.inputNum, 10); @@ -330,7 +360,6 @@ class WorkerWaiter { this.manager.output.updateOutputStatus("baking", nextInput.inputNum); this.chefWorkers[workerIdx].inputNum = nextInput.inputNum; - this.chefWorkers[workerIdx].active = true; const input = nextInput.input; if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) { this.chefWorkers[workerIdx].worker.postMessage({ @@ -359,6 +388,17 @@ class WorkerWaiter { } }); } + + if (this.inputNums.length > 0) { + this.manager.input.inputWorker.postMessage({ + action: "bakeNext", + data: { + inputNum: this.inputNums.splice(0, 1)[0], + bakeId: this.bakeId + } + }); + this.loadingOutputs++; + } } /** @@ -370,10 +410,6 @@ class WorkerWaiter { * @param {boolean} step */ bake(recipeConfig, options, progress, step) { - for (let i = this.chefWorkers.length - 1; i >= 0; i--) { - this.removeChefWorker(this.chefWorkers[i]); - } - this.setBakingStatus(true); this.manager.recipe.updateBreakpointIndicator(false); this.bakeStartTime = new Date().getTime(); @@ -383,15 +419,6 @@ class WorkerWaiter { this.progress = progress; this.step = step; - let numWorkers = this.maxWorkers; - if (this.inputs.length < numWorkers) { - numWorkers = this.inputs.length; - } - for (let i = 0; i < numWorkers; i++) { - const workerIdx = this.addChefWorker(); - if (workerIdx === -1) break; - this.bakeNextInput(workerIdx); - } this.displayProgress(); } @@ -401,28 +428,65 @@ class WorkerWaiter { * @param {object} inputData * @param {string | ArrayBuffer} inputData.input * @param {number} inputData.inputNum - * @param {boolean} inputData.override + * @param {number} inputData.bakeId */ queueInput(inputData) { - for (let i = 0; i < this.chefWorkers; i++) { - if (this.chefWorkers[i].inputNum === inputData.inputNum) { - this.chefWorkers[i].worker.terminate(); - this.chefWorkers.splice(i, 1); - this.bakeNextInput(this.addChefWorker()); - this.bakingInputs--; - break; + this.loadingOutputs--; + + if (this.app.baking && inputData.bakeId === this.bakeId) { + this.inputs.push(inputData); + this.bakeNextInput(this.getInactiveChefWorker(true)); + } + } + + /** + * Queues a list of inputNums to be baked by ChefWorkers, and begins baking + * + * @param {object} inputData + * @param {number[]} inputData.nums + * @param {boolean} inputData.step + */ + bakeAllInputs(inputData) { + if (this.app.baking) return; + const inputNums = inputData.nums; + const step = inputData.step; + + // Use cancelBake to clear out the inputs + this.cancelBake(true, false); + + this.inputNums = inputNums; + this.totalOutputs = inputNums.length; + + let inactiveWorkers = 0; + for (let i = 0; i < this.chefWorkers.length; i++) { + if (!this.chefWorkers[i].active) { + inactiveWorkers++; } } - this.manager.output.updateOutputMessage(`Input ${inputData.inputNum} has not been baked yet.`, inputData.inputNum, false); - this.manager.output.updateOutputStatus("pending", inputData.inputNum); + let numWorkers = (inputNums.length > this.maxWorkers) ? this.maxWorkers : inputNums.length; + numWorkers -= inactiveWorkers; - if (inputData.override) { - this.totalOutputs = 1; - this.inputs = [inputData]; - } else { - this.totalOutputs++; - this.inputs.push(inputData); + for (let i = 0; i < numWorkers; i++) { + this.addChefWorker(); + } + + this.app.bake(step); + + for (let i = 0; i < numWorkers + inactiveWorkers; i++) { + this.manager.input.inputWorker.postMessage({ + action: "bakeNext", + data: { + inputNum: this.inputNums.splice(0, 1)[0], + bakeId: this.bakeId + } + }); + this.loadingOutputs++; + } + + for (let i = 0; i < this.inputNums.length; i++) { + this.manager.output.updateOutputMessage(`Input ${inputNums[i]} has not been baked yet.`, inputNums[i], false); + this.manager.output.updateOutputStatus("pending", inputNums[i]); } } @@ -433,9 +497,11 @@ class WorkerWaiter { * @param {Object[]} [recipeConfig] */ silentBake(recipeConfig) { - // If there aren't any active ChefWorkers, addChefWorker will - // return an inactive worker instead of creating a new one - const workerId = this.addChefWorker(); + // If there aren't any active ChefWorkers, try to add one + let workerId = this.getInactiveChefWorker(); + if (workerId === -1) { + workerId = this.addChefWorker(); + } if (workerId === -1) return; this.chefWorkers[workerId].worker.postMessage({ action: "silentBake", @@ -487,7 +553,6 @@ class WorkerWaiter { */ displayProgress() { const progress = this.getBakeProgress(); - if (progress.total === progress.baked) return; const percentComplete = ((progress.pending + progress.baking) / progress.total) * 100; diff --git a/src/web/html/index.html b/src/web/html/index.html index 906822b8..052a282d 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -218,15 +218,6 @@
- diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css index e6381797..b01b8d7e 100755 --- a/src/web/stylesheets/layout/_io.css +++ b/src/web/stylesheets/layout/_io.css @@ -58,7 +58,6 @@ border-bottom: 1px solid var(--primary-border-colour); border-left: 1px solid var(--primary-border-colour); height: var(--tab-height); - width: calc(100% - 75px); clear: none; } diff --git a/tests/browser/nightwatch.js b/tests/browser/nightwatch.js index 52b4e7bb..23100d8d 100644 --- a/tests/browser/nightwatch.js +++ b/tests/browser/nightwatch.js @@ -82,7 +82,6 @@ module.exports = { browser .useCss() .setValue("#input-text", "Don't Panic.") - .waitForElementNotVisible("#stale-indicator", 1000) .click("#bake"); // Check output