This commit is contained in:
Scott Erickson 2014-05-07 12:25:14 -07:00
commit ff51f18a90
5 changed files with 496 additions and 9 deletions

View file

@ -0,0 +1,378 @@
// 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 = "DebugWorker";
self.logLimit = 2000;
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;
// 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;
}
importScripts('/javascripts/world.js');
var World = self.require('lib/world/world');
var GoalManager = self.require('lib/world/GoalManager');
serializedClasses = {
"Thang": self.require('lib/world/thang'),
"Vector": self.require('lib/world/vector'),
"Rectangle": self.require('lib/world/rectangle')
}
self.getCurrentFrame = function getCurrentFrame(args) { return self.world.frames.length; };
//optimize this later
self.currentUserCodeMapCopy = {};
self.currentWorldFrame = 0;
self.maxSerializationDepth = 3;
self.stringifyValue = function(value, depth) {
var brackets, i, isArray, isObject, key, prefix, s, sep, size, v, values, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3;
if (!value || _.isString(value)) {
return value;
}
if (_.isFunction(value)) {
if (depth === 2) {
return void 0;
} else {
return "<Function>";
}
}
if (value === this.thang && depth) {
return "<this " + value.id + ">";
}
if (depth === 2) {
if (((_ref = value.constructor) != null ? _ref.className : void 0) === "Thang") {
value = "<" + (value.type || value.spriteName) + " - " + value.id + ", " + (value.pos ? value.pos.toString() : 'non-physical') + ">";
} else {
value = value.toString();
}
return value;
}
isArray = _.isArray(value);
isObject = _.isObject(value);
if (!(isArray || isObject)) {
return value.toString();
}
brackets = isArray ? ["[", "]"] : ["{", "}"];
size = _.size(value);
if (!size) {
return brackets.join("");
}
values = [];
if (isArray) {
for (_i = 0, _len = value.length; _i < _len; _i++) {
v = value[_i];
s = this.stringifyValue(v, depth + 1);
if (s !== void 0) {
values.push("" + s);
}
}
} else {
_ref2 = (_ref1 = value.apiProperties) != null ? _ref1 : _.keys(value);
for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
key = _ref2[_j];
if (key[0] === "_") continue;
s = this.stringifyValue(value[key], depth + 1);
if (s !== void 0) {
values.push(key + ": " + s);
}
}
}
sep = '\n' + ((function() {
var _k, _results;
_results = [];
for (i = _k = 0; 0 <= depth ? _k < depth : _k > depth; i = 0 <= depth ? ++_k : --_k) {
_results.push(" ");
}
return _results;
})()).join('');
prefix = (_ref3 = value.constructor) != null ? _ref3.className : void 0;
if (isArray) {
if (prefix == null) {
prefix = "Array";
}
}
if (isObject) {
if (prefix == null) {
prefix = "Object";
}
}
prefix = prefix ? prefix + " " : "";
return "" + prefix + brackets[0] + sep + " " + (values.join(sep + ' ')) + sep + brackets[1];
};
var cache = {};
self.invalidateCache = function () {
cache = {};
};
self.retrieveValueFromCache = function (thangID, spellID, variableChain, frame) {
var frameCache, thangCache, spellCache;
if ((frameCache = cache[frame]) && (thangCache = frameCache[thangID]) && (spellCache = thangCache[spellID]))
return spellCache[variableChain.join()];
return undefined;
};
self.updateCache = function (thangID, spellID, variableChain, frame, value) {
var key, keys, currentObject;
keys = [frame,thangID, spellID, variableChain.join()];
currentObject = cache;
for (var i = 0, len = keys.length - 1; i < len; i++)
{
key = keys[i];
if (!(key in currentObject))
currentObject[key] = {};
currentObject = currentObject[key];
}
currentObject[keys[keys.length - 1]] = value;
};
self.retrieveValueFromFrame = function retrieveValueFromFrame(args) {
var cacheValue;
if (args.frame === self.currentWorldFrame && (cacheValue = self.retrieveValueFromCache(args.currentThangID, args.currentSpellID, args.variableChain, args.frame)))
return self.postMessage({type: 'debug-value-return', serialized: {"key": args.variableChain.join("."), "value": cacheValue}});
var retrieveProperty = function retrieveProperty(currentThangID, currentSpellID, variableChain)
{
var prop;
var value;
var keys = [];
for (var i = 0, len = variableChain.length; i < len; i++) {
prop = variableChain[i];
if (prop === "this")
{
value = self.world.thangMap[currentThangID];
}
else if (i === 0)
{
try
{
var flowStates = self.world.userCodeMap[currentThangID][currentSpellID].flow.states;
//we have to go to the second last flowState as we run the world for one additional frame
//to collect the flow
value = _.last(flowStates[flowStates.length - 2].statements).variables[prop];
}
catch (e)
{
value = undefined;
}
}
else
{
value = value[prop];
}
keys.push(prop);
if (!value) break;
var classOfValue;
if (classOfValue = serializedClasses[value.CN])
{
if (value.CN === "Thang")
{
var thang = self.world.thangMap[value.id];
value = thang || "<Thang " + value.id + " (non-existent)>"
}
else
{
value = classOfValue.deserializeFromAether(value);
}
}
}
var serializedProperty = {
"key": keys.join("."),
"value": self.stringifyValue(value,0)
};
self.updateCache(currentThangID,currentSpellID,variableChain, args.frame, serializedProperty.value);
self.postMessage({type: 'debug-value-return', serialized: serializedProperty});
};
self.enableFlowOnThangSpell(args.currentThangID, args.currentSpellID, args.userCodeMap);
self.setupWorldToRunUntilFrame(args);
self.world.loadFramesUntilFrame(
args.frame,
retrieveProperty.bind({},args.currentThangID, args.currentSpellID, args.variableChain),
self.onWorldError,
self.onWorldLoadProgress
);
};
self.enableFlowOnThangSpell = function enableFlowOnThang(thangID, spellID, userCodeMap) {
try {
if (userCodeMap[thangID][spellID].originalOptions.includeFlow === true &&
userCodeMap[thangID][spellID].originalOptions.noSerializationInFlow === true)
return;
else
{
userCodeMap[thangID][spellID].originalOptions.includeFlow = true;
userCodeMap[thangID][spellID].originalOptions.noSerializationInFlow = true;
var temporaryAether = Aether.deserialize(userCodeMap[thangID][spellID]);
temporaryAether.transpile(temporaryAether.raw);
userCodeMap[thangID][spellID] = temporaryAether.serialize();
}
}
catch (e) {
console.log("there was an error enabling flow on thang spell:" + e)
}
};
self.setupWorldToRunUntilFrame = function setupWorldToRunUntilFrame(args) {
self.postedErrors = {};
self.t0 = new Date();
self.firstWorld = args.firstWorld;
self.postedErrors = false;
self.logsLogged = 0;
var stringifiedUserCodeMap = JSON.stringify(args.userCodeMap);
var userCodeMapHasChanged = ! _.isEqual(self.currentUserCodeMapCopy, stringifiedUserCodeMap);
self.currentUserCodeMapCopy = stringifiedUserCodeMap;
if (!self.world || userCodeMapHasChanged || args.frame != self.currentWorldFrame) {
self.invalidateCache();
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.currentWorldFrame = args.frame;
}
};
self.runWorldUntilFrame = function runWorldUntilFrame(args) {
self.setupWorldToRunUntilFrame(args);
self.world.loadFramesUntilFrame(args.frame, self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress);
};
self.onWorldLoaded = function onWorldLoaded() {
console.log("World loaded!");
};
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'});

View file

@ -22,8 +22,13 @@ module.exports = class God
@angels = []
@firstWorld = true
Backbone.Mediator.subscribe 'tome:cast-spells', @onTomeCast, @
@retriveValueFromFrame = _.throttle @retrieveValueFromFrame, 1000
Backbone.Mediator.subscribe 'tome:spell-debug-value-request', @retrieveValueFromFrame, @
@fillWorkerPool = _.throttle @fillWorkerPool, 3000, leading: false
@fillWorkerPool()
#TODO: have this as a constructor option
@debugWorker = @createDebugWorker()
@currentUserCodeMap = {}
workerCode: '/javascripts/workers/worker_world.js' #Can be a string or a function.
@ -51,6 +56,13 @@ module.exports = class God
worker.addEventListener 'message', @onWorkerMessage(worker)
worker
createDebugWorker: ->
worker = new Worker '/javascripts/workers/worker_debug.js'
worker.creationTime = new Date()
worker.addEventListener 'message', @onDebugWorkerMessage
worker
onWorkerMessage: (worker) =>
unless worker.onMessage?
worker.onMessage = (event) =>
@ -62,6 +74,18 @@ module.exports = class God
console.warn "Received strange word from God: #{event.data.type}"
worker.onMessage
onDebugWorkerMessage: (event) =>
worker = event.target
switch event.data.type
when "worker-initialized"
worker.initialized = true
when 'new-debug-world'
console.log "New Debug world!"
when 'console-log'
console.log "|" + @id + "'s " + @id + "|", event.data.args...
when 'debug-value-return'
Backbone.Mediator.publish 'god:debug-value-return', event.data.serialized
getAngel: ->
freeAngel = null
for angel in @angels
@ -113,6 +137,22 @@ module.exports = class God
goals: @goalManager?.getGoals()
}}
retrieveValueFromFrame: (args) ->
if not args.thangID or not args.spellID or not args.variableChain then return
args.frame ?= @world.age / @world.dt
@debugWorker.postMessage
func: 'retrieveValueFromFrame'
args:
worldName: @level.name
userCodeMap: @currentUserCodeMap
level: @level
firstWorld: @firstWorld
goals: @goalManager?.getGoals()
frame: args.frame
currentThangID: args.thangID
currentSpellID: args.spellID
variableChain: args.variableChain
#Coffeescript needs getters and setters.
setGoalManager: (@goalManager) =>
@ -144,6 +184,7 @@ module.exports = class God
finishBeholdingWorld: (newWorld) =>
newWorld.findFirstChangedFrame @world
@world = newWorld
@currentUserCodeMap = @filterUserCodeMapWhenFromWorld @world.userCodeMap
errorCount = (t for t in @world.thangs when t.errorsOut).length
Backbone.Mediator.publish('god:new-world-created', world: @world, firstWorld: @firstWorld, errorCount: errorCount, goalStates: @latestGoalStates, team: me.team)
for scriptNote in @world.scriptNotes
@ -154,6 +195,23 @@ module.exports = class God
unless _.find @angels, 'busy'
@spells = null # Don't hold onto old spells; memory leaks
filterUserCodeMapWhenFromWorld: (worldUserCodeMap) ->
newUserCodeMap = {}
for thangName, thang of worldUserCodeMap
newUserCodeMap[thangName] = {}
for spellName,aether of thang
shallowFilteredObject = _.pick aether, ['raw','pure','originalOptions']
newUserCodeMap[thangName][spellName] = _.cloneDeep shallowFilteredObject
newUserCodeMap[thangName][spellName] = _.defaults newUserCodeMap[thangName][spellName],
flow: {}
metrics: {}
problems:
errors: []
infos: []
warnings: []
style: {}
newUserCodeMap
getUserCodeMap: ->
userCodeMap = {}
for spellKey, spell of @spells
@ -167,6 +225,10 @@ module.exports = class God
@dead = true
Backbone.Mediator.unsubscribe('tome:cast-spells', @onTomeCast, @)
@goalManager?.destroy()
@debugWorker?.terminate()
@debugWorker?.removeEventListener 'message', @onDebugWorkerMessage
@debugWorker ?= null
@currentUserCodeMap = null
@goalManager = null
@fillWorkerPool = null
@simulateWorld = null

View file

@ -8,7 +8,6 @@ WorldScriptNote = require './world_script_note'
{now, consolidateThangs, typedArraySupport} = require './world_utils'
Component = require 'lib/world/component'
System = require 'lib/world/system'
PROGRESS_UPDATE_INTERVAL = 200
DESERIALIZATION_INTERVAL = 20
@ -107,6 +106,35 @@ 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 #state is gathered at next frame
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
loadProgressCallback? 1
loadedCallback()
abort: ->
@aborted = true
@ -225,7 +253,7 @@ module.exports = class World
@scriptNotes.push scriptNote
return unless @goalManager
@goalManager.submitWorldGenerationEvent(channel, event, @frames.length)
setGoalState: (goalID, status) ->
@goalManager.setGoalState(goalID, status)

View file

@ -13,6 +13,8 @@ module.exports = class DebugView extends View
subscriptions:
'god:new-world-created': 'onNewWorld'
'god:debug-value-return': 'handleDebugValue'
'tome:spell-shown': 'changeCurrentThangAndSpell'
events: {}
@ -20,12 +22,26 @@ module.exports = class DebugView extends View
super options
@ace = options.ace
@thang = options.thang
@spell = options.spell
@variableStates = {}
@globals = {Math: Math, _: _, String: String, Number: Number, Array: Array, Object: Object} # ... add more as documented
for className, klass of serializedClasses
@globals[className] = klass
for className, serializedClass of serializedClasses
@globals[className] = serializedClass
@onMouseMove = _.throttle @onMouseMove, 25
changeCurrentThangAndSpell: (thangAndSpellObject) ->
@thang = thangAndSpellObject.thang
@spell = thangAndSpellObject.spell
handleDebugValue: (returnObject) ->
{key, value} = returnObject
if @variableChain and not key is @variableChain.join(".") then return
@$el.find("code").text "#{key}: #{value}"
@$el.show().css(@pos)
afterRender: ->
super()
@ace.on "mousemove", @onMouseMove
@ -58,7 +74,8 @@ module.exports = class DebugView extends View
token = prev
start = it.getCurrentTokenColumn()
chain.unshift token.value
if token and (token.value of @variableStates or token.value is "this" or @globals[token.value])
#Highlight all tokens, so true overrides all other conditions TODO: Refactor this later
if token and (true or token.value of @variableStates or token.value is "this" or @globals[token.value])
@variableChain = chain
offsetX = e.domEvent.offsetX ? e.clientX - $(e.domEvent.target).offset().left
offsetY = e.domEvent.offsetY ? e.clientY - $(e.domEvent.target).offset().top
@ -79,8 +96,11 @@ module.exports = class DebugView extends View
update: ->
if @variableChain
{key, value} = @deserializeVariableChain @variableChain
@$el.find("code").text "#{key}: #{value}"
Backbone.Mediator.publish 'tome:spell-debug-value-request',
thangID: @thang.id
spellID: @spell.name
variableChain: @variableChain
@$el.find("code").text "Finding value..."
@$el.show().css(@pos)
else
@$el.hide()
@ -98,7 +118,6 @@ module.exports = class DebugView extends View
@hoveredProperty = if @variableChain?.length is 2 then owner: @variableChain[0], property: @variableChain[1] else {}
unless _.isEqual oldHoveredProperty, @hoveredProperty
Backbone.Mediator.publish 'tome:spell-debug-property-hovered', @hoveredProperty
updateMarker: ->
if @marker
@ace.getSession().removeMarker @marker

View file

@ -193,7 +193,7 @@ module.exports = class SpellView extends View
@createToolbarView()
createDebugView: ->
@debugView = new SpellDebugView ace: @ace, thang: @thang
@debugView = new SpellDebugView ace: @ace, thang: @thang, spell:@spell
@$el.append @debugView.render().$el.hide()
createToolbarView: ->