diff --git a/app/assets/javascripts/workers/worker_debug.js b/app/assets/javascripts/workers/worker_debug.js new file mode 100644 index 000000000..8b6c6cf58 --- /dev/null +++ b/app/assets/javascripts/workers/worker_debug.js @@ -0,0 +1,210 @@ +// There's no reason that this file is in JavaScript instead of CoffeeScript. +// We should convert it and update the brunch config. + +// If we wanted to be more robust, we could use this: https://github.com/padolsey/operative/blob/master/src/operative.js +if(typeof window !== 'undefined' || !self.importScripts) + throw "Attempt to load worker_world into main window instead of web worker."; + +// Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind +// This is here for running simuations in enviroments lacking function.bind (PhantomJS mostly) +if (!Function.prototype.bind) { + Function.prototype.bind = function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 internal IsCallable function + throw new TypeError("Function.prototype.bind (Shim) - target is not callable"); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; + }; +} + +// assign global window so that Brunch's require (in world.js) can go into it +self.window = self; +self.workerID = "Worker"; + +self.logLimit = 200; +self.logsLogged = 0; +var console = { + log: function() { + if(self.logsLogged++ == self.logLimit) + self.postMessage({type: 'console-log', args: ["Log limit " + self.logLimit + " reached; shutting up."], id: self.workerID}); + else if(self.logsLogged < self.logLimit) { + args = [].slice.call(arguments); + for(var i = 0; i < args.length; ++i) { + if(args[i] && args[i].constructor) { + if(args[i].constructor.className === "Thang" || args[i].isComponent) + args[i] = args[i].toString(); + } + } + try { + self.postMessage({type: 'console-log', args: args, id: self.workerID}); + } + catch(error) { + self.postMessage({type: 'console-log', args: ["Could not post log: " + args, error.toString(), error.stack, error.stackTrace], id: self.workerID}); + } + } + }}; // so that we don't crash when debugging statements happen +console.error = console.info = console.log; +self.console = console; + +importScripts('/javascripts/world.js'); + +// We could do way more from this: http://stackoverflow.com/questions/10653809/making-webworkers-a-safe-environment +Object.defineProperty(self, "XMLHttpRequest", { + get: function() { throw new Error("Access to XMLHttpRequest is forbidden."); }, + configurable: false +}); + +self.transferableSupported = function transferableSupported() { + // Not in IE, even in IE 11 + try { + var ab = new ArrayBuffer(1); + worker.postMessage(ab, [ab]); + return ab.byteLength == 0; + } catch(error) { + return false; + } + return false; +} + +var World = self.require('lib/world/world'); +var GoalManager = self.require('lib/world/GoalManager'); + +self.getCurrentFrame = function getCurrentFrame(args) { return self.world.frames.length; }; + +self.runWorld = function runWorld(args) { + self.postedErrors = {}; + self.t0 = new Date(); + self.firstWorld = args.firstWorld; + self.postedErrors = false; + self.logsLogged = 0; + + try { + self.world = new World(args.worldName, args.userCodeMap); + if(args.level) + self.world.loadFromLevel(args.level, true); + self.goalManager = new GoalManager(self.world); + self.goalManager.setGoals(args.goals); + self.goalManager.setCode(args.userCodeMap); + self.goalManager.worldGenerationWillBegin(); + self.world.setGoalManager(self.goalManager); + } + catch (error) { + self.onWorldError(error); + return; + } + Math.random = self.world.rand.randf; // so user code is predictable + self.world.loadFrames(self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress); +}; + +self.runWorldUntilFrame = function runWorldUntilFrame(args) { + self.postedErrors = {}; + self.t0 = new Date(); + self.firstWorld = args.firstWorld; + self.postedErrors = false; + self.logsLogged = 0; + if (!self.world) + { + try { + self.world = new World(args.worldName, args.userCodeMap); + if(args.level) + self.world.loadFromLevel(args.level, true); + self.goalManager = new GoalManager(self.world); + self.goalManager.setGoals(args.goals); + self.goalManager.setCode(args.userCodeMap); + self.goalManager.worldGenerationWillBegin(); + self.world.setGoalManager(self.goalManager); + } + catch (error) { + self.onWorldError(error); + return; + } + Math.random = self.world.rand.randf; // so user code is predictable + } + + self.world.totalFrames = args.frame; //hack to work around error checking + + self.world.loadFramesUntilFrame(args.frame, self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress); +}; + +self.onWorldLoaded = function onWorldLoaded() { + var t1 = new Date(); + var diff = t1 - self.t0; + var transferableSupported = self.transferableSupported(); + try { + var serialized = self.world.serialize(); + } + catch(error) { + console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace); + } + var t2 = new Date(); + //console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects); + try { + if(transferableSupported) + self.postMessage({type: 'new-debug-world', serialized: serialized.serializedWorld, goalStates: self.goalManager.getGoalStates()}, serialized.transferableObjects); + else + self.postMessage({type: 'new-debug-world', serialized: serialized.serializedWorld, goalStates: self.goalManager.getGoalStates()}); + } + catch(error) { + console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace); + } + var t3 = new Date(); + console.log("And it was so: (" + (diff / self.world.totalFrames).toFixed(3) + "ms per frame,", self.world.totalFrames, "frames)\nSimulation :", diff + "ms \nSerialization:", (t2 - t1) + "ms\nDelivery :", (t3 - t2) + "ms"); +}; + +self.onWorldError = function onWorldError(error) { + if(error instanceof Aether.problems.UserCodeProblem) { + if(!self.postedErrors[error.key]) { + var problem = error.serialize(); + self.postMessage({type: 'user-code-problem', problem: problem}); + self.postedErrors[error.key] = problem; + } + } + else { + console.log("Non-UserCodeError:", error.toString() + "\n" + error.stack || error.stackTrace); + } + /* We don't actually have the recoverable property any more; hmm + if(!self.firstWorld && !error.recoverable) { + self.abort(); + return false; + } + */ + return true; +}; + +self.onWorldLoadProgress = function onWorldLoadProgress(progress) { + self.postMessage({type: 'world-load-progress-changed', progress: progress}); +}; + +self.abort = function abort() { + if(self.world && self.world.name) { + console.log("About to abort:", self.world.name, typeof self.world.abort); + if(typeof self.world !== "undefined") + self.world.abort(); + self.world = null; + } + self.postMessage({type: 'abort'}); +}; + +self.reportIn = function reportIn() { + self.postMessage({type: 'reportIn'}); +} + +self.addEventListener('message', function(event) { + self[event.data.func](event.data.args); +}); + +self.postMessage({type: 'worker-initialized'}); diff --git a/app/lib/God.coffee b/app/lib/God.coffee index 6fe758391..983b264c6 100644 --- a/app/lib/God.coffee +++ b/app/lib/God.coffee @@ -23,6 +23,8 @@ module.exports = class God Backbone.Mediator.subscribe 'tome:cast-spells', @onTomeCast, @ @fillWorkerPool = _.throttle @fillWorkerPool, 3000, leading: false @fillWorkerPool() + #TODO: have this as a constructor option + @debugWorker = @createDebugWorker() onTomeCast: (e) -> return if @dead @@ -48,14 +50,31 @@ module.exports = class God worker.creationTime = new Date() worker.addEventListener 'message', @onWorkerMessage worker - + + createDebugWorker: -> + worker = new Worker '/javascripts/workers/worker_debug.js' + worker.creationTime = new Date() + worker.addEventListener 'message', @onDebugWorkerMessage + console.log "GOD: Created debug worker" + worker + onWorkerMessage: (event) => worker = event.target if event.data.type is 'worker-initialized' #console.log @id, "worker initialized after", ((new Date()) - worker.creationTime), "ms (before it was needed)" worker.initialized = true worker.removeEventListener 'message', @onWorkerMessage - + + onDebugWorkerMessage: (event) => + worker = event.target + switch event.data.type + when "worker-initialized" + worker.initialized = true + when 'new-debug-world' + console.log "Created new debug world!" + when 'console-log' + console.log "|" + @id + "'s " + @id + "|", event.data.args... + getAngel: -> freeAngel = null for angel in @angels @@ -106,7 +125,22 @@ module.exports = class God firstWorld: @firstWorld goals: @goalManager?.getGoals() }} - + + createDebugWorldUntilFrame: (frame) -> + @debugWorker.postMessage + func : 'runWorldUntilFrame' + args: + worldName: @level.name + userCodeMap: @getUserCodeMap() + level: @level + firstWorld: @firstWorld + goals: @goalManager?.getGoals() + frame: frame + + getDebugWorldCurrentFrame: -> + @debugWorker.postMessage + func: 'getCurrentFrame' + beholdWorld: (angel, serialized, goalStates) -> worldCreation = angel.started angel.free() diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee index f209f82d3..ed694cd9a 100644 --- a/app/lib/world/world.coffee +++ b/app/lib/world/world.coffee @@ -103,6 +103,36 @@ module.exports = class World loadProgressCallback? 1 loadedCallback() + loadFramesUntilFrame: (frameToLoadUntil, loadedCallback, errorCallback, loadProgressCallback) -> + return if @aborted + unless @thangs.length + console.log "Warning: loadFrames called on empty World" + t1 = now() + @t0 ?= t1 + i = @frames.length + while i < frameToLoadUntil + try + @getFrame(i) + ++i # increment this after we have succeeded in getting the frame, otherwise we'll have to do that frame again + catch error + # Not an Aether.errors.UserCodeError; maybe we can't recover + @addError error + for error in (@unhandledRuntimeErrors ? []) + return unless errorCallback error # errorCallback tells us whether the error is recoverable + @unhandledRuntimeErrors = [] + t2 = now() + if t2 - t1 > PROGRESS_UPDATE_INTERVAL + loadProgressCallback? i / @totalFrames + t1 = t2 + if t2 - @t0 > 1000 + console.log(' Loaded', i, 'of', frameToLoadUntil, "(+" + (t2 - @t0).toFixed(0) + "ms)") + @t0 = t2 + setTimeout((=> @loadFrames(loadedCallback, errorCallback, loadProgressCallback)), 0) + return + @ended = true + loadProgressCallback? 1 + loadedCallback() + abort: -> @aborted = true @@ -221,7 +251,7 @@ module.exports = class World @scriptNotes.push scriptNote return unless @goalManager @goalManager.submitWorldGenerationEvent(channel, event, @frames.length) - + setGoalState: (goalID, status) -> @goalManager.setGoalState(goalID, status)