2014-05-10 21:24:50 -04:00
# Every Angel has one web worker attached to it. It will call methods inside the worker and kill it if it times out.
# God is the public API; Angels are an implementation detail. Each God can have one or more Angels.
{ now } = require ' lib/world/world_utils '
World = require ' lib/world/world '
2014-11-28 20:49:41 -05:00
CocoClass = require ' core/CocoClass '
2014-09-29 20:31:58 -04:00
GoalManager = require ' lib/world/GoalManager '
2015-04-25 20:29:02 -04:00
{ sendContactMessage } = require ' core/contact '
2014-05-10 21:24:50 -04:00
2015-05-11 21:53:42 -04:00
reportedLoadErrorAlready = false
2014-05-10 21:24:50 -04:00
module.exports = class Angel extends CocoClass
@nicks: [ ' Archer ' , ' Lana ' , ' Cyril ' , ' Pam ' , ' Cheryl ' , ' Woodhouse ' , ' Ray ' , ' Krieger ' ]
2014-06-21 13:56:55 -04:00
infiniteLoopIntervalDuration: 10000 # check this often; must be longer than other two combined
infiniteLoopTimeoutDuration: 7500 # wait this long for a response when checking
2014-05-10 21:24:50 -04:00
abortTimeoutDuration: 500 # give in-process or dying workers this long to give up
2014-08-23 00:35:08 -04:00
subscriptions:
2014-08-23 20:26:56 -04:00
' level:flag-updated ' : ' onFlagEvent '
2014-08-26 01:05:24 -04:00
' playback:stop-real-time-playback ' : ' onStopRealTimePlayback '
2015-04-18 22:54:34 -04:00
' level:escape-pressed ' : ' onEscapePressed '
2014-08-23 00:35:08 -04:00
2014-05-10 21:24:50 -04:00
constructor: (@shared) ->
super ( )
@ say ' Got my wings. '
2015-04-10 11:24:19 -04:00
isIE = window . navigator and ( window . navigator . userAgent . search ( ' MSIE ' ) isnt - 1 or window . navigator . appName is ' Microsoft Internet Explorer ' )
2015-11-29 15:30:19 -05:00
slowerSimulations = isIE #or @shared.headless
# Since IE is so slow to serialize without transferable objects, we can't trust it.
# We also noticed the headless_client simulator needing more time. (This does both Simulators, though.) If we need to use lots of headless clients, enable this.
if slowerSimulations
2015-04-10 11:24:19 -04:00
@ infiniteLoopIntervalDuration *= 10
2014-05-10 21:24:50 -04:00
@ infiniteLoopTimeoutDuration *= 10
@ abortTimeoutDuration *= 10
@initialized = false
@running = false
2015-04-25 20:29:02 -04:00
@allLogs = [ ]
2014-05-10 21:24:50 -04:00
@ hireWorker ( )
@ shared . angels . push @
destroy: ->
@ fireWorker false
_ . remove @ shared . angels , @
super ( )
workIfIdle: ->
@ doWork ( ) unless @ running
# say: debugging stuff, usually off; log: important performance indicators, keep on
say: (args...) -> #@log args...
2015-01-05 13:44:17 -05:00
log: ->
2015-04-25 20:29:02 -04:00
# console.info.apply is undefined in IE9, CoffeeScript splats invocation won't work.
2014-11-26 15:40:16 -05:00
# http://stackoverflow.com/questions/5472938/does-ie9-support-console-log-and-is-it-a-real-function
message = " | #{ @ shared . godNick } ' s #{ @ nick } | "
message += " #{ arg } " for arg in arguments
console . info message
2015-04-25 20:29:02 -04:00
@ allLogs . push message
2014-05-10 21:24:50 -04:00
testWorker: =>
2014-05-24 00:24:50 -04:00
return if @ destroyed
clearTimeout @ condemnTimeout
@condemnTimeout = _ . delay @ infinitelyLooped , @ infiniteLoopTimeoutDuration
2014-06-30 22:16:26 -04:00
@ say ' Let \' s give it ' , @ infiniteLoopTimeoutDuration , ' to not loop. '
2014-05-24 00:24:50 -04:00
@ worker . postMessage func: ' reportIn '
2014-05-10 21:24:50 -04:00
onWorkerMessage: (event) =>
return @ say ' Currently aborting old work. ' if @ aborting and event . data . type isnt ' abort '
switch event . data . type
# First step: worker has to load the scripts.
when ' worker-initialized '
unless @ initialized
@ log " Worker initialized after #{ ( new Date ( ) ) - @ worker . creationTime } ms "
@initialized = true
@ doWork ( )
# We watch over the worker as it loads the world frames to make sure it doesn't infinitely loop.
when ' start-load-frames '
clearTimeout @ condemnTimeout
when ' report-in '
2014-06-30 22:16:26 -04:00
@ say ' Worker reported in. '
2014-05-10 21:24:50 -04:00
clearTimeout @ condemnTimeout
when ' end-load-frames '
clearTimeout @ condemnTimeout
2014-09-23 21:21:27 -04:00
@ beholdGoalStates event . data . goalStates , event . data . overallStatus # Work ends here if we're headless.
when ' end-preload-frames '
clearTimeout @ condemnTimeout
@ beholdGoalStates event . data . goalStates , event . data . overallStatus , true
2014-05-10 21:24:50 -04:00
2014-05-26 21:45:00 -04:00
# We have to abort like an infinite loop if we see one of these; they're not really recoverable
when ' non-user-code-problem '
2015-11-29 15:30:19 -05:00
Backbone . Mediator . publish ' god:non-user-code-problem ' , problem: event . data . problem , god: @ shared . god
2014-05-26 21:45:00 -04:00
if @ shared . firstWorld
2015-04-25 20:29:02 -04:00
@ infinitelyLooped ( false , true ) # For now, this should do roughly the right thing if it happens during load.
2014-05-26 21:45:00 -04:00
else
@ fireWorker ( )
2014-08-22 15:39:29 -04:00
# If it didn't finish simulating successfully, or we abort the worker.
2014-05-10 21:24:50 -04:00
when ' abort '
2014-06-30 22:16:26 -04:00
@ say ' Aborted. ' , event . data
2014-05-10 21:24:50 -04:00
clearTimeout @ abortTimeout
@aborting = false
@running = false
_ . remove @ shared . busyAngels , @
@ doWork ( )
2014-08-22 15:39:29 -04:00
# We pay attention to certain progress indicators as the world loads.
when ' console-log '
@ log event . data . args . . .
when ' user-code-problem '
2015-11-29 15:30:19 -05:00
Backbone . Mediator . publish ' god:user-code-problem ' , problem: event . data . problem , god: @ shared . god
2014-08-22 15:39:29 -04:00
when ' world-load-progress-changed '
2015-11-29 15:30:19 -05:00
Backbone . Mediator . publish ' god:world-load-progress-changed ' , progress: event . data . progress , god: @ shared . god
2014-09-21 17:35:59 -04:00
unless event . data . progress is 1 or @ work . preload or @ work . headless or @ work . synchronous or @ deserializationQueue . length or ( @ shared . firstWorld and not @ shared . spectate )
2014-08-22 15:39:29 -04:00
@ worker . postMessage func: ' serializeFramesSoFar ' # Stream it!
# We have some or all of the frames serialized, so let's send the (partially?) simulated world to the Surface.
when ' some-frames-serialized ' , ' new-world '
2014-08-22 17:59:32 -04:00
deserializationArgs = [ event . data . serialized , event . data . goalStates , event . data . startFrame , event . data . endFrame , @ streamingWorld ]
2014-08-22 15:39:29 -04:00
@ deserializationQueue . push deserializationArgs
if @ deserializationQueue . length is 1
@ beholdWorld deserializationArgs . . .
2014-05-10 21:24:50 -04:00
else
2014-06-30 22:16:26 -04:00
@ log ' Received unsupported message: ' , event . data
2014-05-10 21:24:50 -04:00
2014-09-23 21:21:27 -04:00
beholdGoalStates: (goalStates, overallStatus, preload=false) ->
2014-05-10 21:24:50 -04:00
return if @ aborting
2015-11-29 15:30:19 -05:00
Backbone . Mediator . publish ' god:goals-calculated ' , goalStates: goalStates , preload: preload , overallStatus: overallStatus , god: @ shared . god
@ shared . god . trigger ' goals-calculated ' , goalStates: goalStates , preload: preload , overallStatus: overallStatus
2014-05-10 21:24:50 -04:00
@ finishWork ( ) if @ shared . headless
2014-08-21 19:27:52 -04:00
beholdWorld: (serialized, goalStates, startFrame, endFrame, streamingWorld) ->
2014-05-10 21:24:50 -04:00
return if @ aborting
# Toggle BOX2D_ENABLED during deserialization so that if we have box2d in the namespace, the Collides Components still don't try to create bodies for deserialized Thangs upon attachment.
window . BOX2D_ENABLED = false
2014-10-23 21:11:12 -04:00
@streamingWorld = World . deserialize serialized , @ shared . worldClassMap , @ shared . lastSerializedWorldFrames , @ finishBeholdingWorld ( goalStates ) , startFrame , endFrame , @ work . level , streamingWorld
2014-05-10 21:24:50 -04:00
window . BOX2D_ENABLED = true
@shared.lastSerializedWorldFrames = serialized . frames
finishBeholdingWorld: (goalStates) -> (world) =>
2014-10-14 14:53:32 -04:00
return if @ aborting or @ destroyed
2014-08-21 19:27:52 -04:00
finished = world . frames . length is world . totalFrames
2014-08-22 00:23:45 -04:00
firstChangedFrame = world . findFirstChangedFrame @ shared . world
eventType = if finished then ' god:new-world-created ' else ' god:streaming-world-updated '
2014-08-21 19:27:52 -04:00
if finished
@shared.world = world
2014-11-18 15:40:28 -05:00
Backbone . Mediator . publish eventType , world: world , firstWorld: @ shared . firstWorld , goalStates: goalStates , team: me . team , firstChangedFrame: firstChangedFrame , finished: finished
2014-08-22 00:23:45 -04:00
if finished
2014-08-21 19:27:52 -04:00
for scriptNote in @ shared . world . scriptNotes
Backbone . Mediator . publish scriptNote . channel , scriptNote . event
@ shared . goalManager ? . world = world
@ finishWork ( )
else
2014-08-22 15:39:29 -04:00
@ deserializationQueue . shift ( ) # Finished with this deserialization.
if deserializationArgs = @ deserializationQueue [ 0 ] # Start another?
@ beholdWorld deserializationArgs . . .
2014-05-10 21:24:50 -04:00
finishWork: ->
2014-08-22 17:59:32 -04:00
@streamingWorld = null
2014-05-10 21:24:50 -04:00
@shared.firstWorld = false
2014-08-22 15:39:29 -04:00
@deserializationQueue = [ ]
2014-05-10 21:24:50 -04:00
@running = false
_ . remove @ shared . busyAngels , @
2014-09-19 11:11:08 -04:00
clearTimeout @ condemnTimeout
clearInterval @ purgatoryTimer
@condemnTimeout = @purgatoryTimer = null
2014-05-10 21:24:50 -04:00
@ doWork ( )
2014-05-11 20:42:32 -04:00
finalizePreload: ->
2014-06-30 22:16:26 -04:00
@ say ' Finalize preload. '
2014-05-11 20:42:32 -04:00
@ worker . postMessage func: ' finalizePreload '
2014-08-21 19:27:52 -04:00
@work.preload = false
2014-05-11 20:42:32 -04:00
2015-04-25 20:29:02 -04:00
infinitelyLooped: (escaped=false, nonUserCodeProblem=false) =>
2014-06-30 22:16:26 -04:00
@ say ' On infinitely looped! Aborting? ' , @ aborting
2014-05-10 21:24:50 -04:00
return if @ aborting
2014-06-30 22:16:26 -04:00
problem = type: ' runtime ' , level: ' error ' , id: ' runtime_InfiniteLoop ' , message: ' Code never finished. It \' s either really slow or has an infinite loop. '
2015-04-18 22:54:34 -04:00
problem.message = ' Escape pressed; code aborted. ' if escaped
2015-11-29 15:30:19 -05:00
Backbone . Mediator . publish ' god:user-code-problem ' , problem: problem , god: @ shared . god
Backbone . Mediator . publish ' god:infinite-loop ' , firstWorld: @ shared . firstWorld , nonUserCodeProblem: nonUserCodeProblem , god: @ shared . god
@ shared . god . trigger ' infinite-loop ' , firstWorld: @ shared . firstWorld , nonUserCodeProblem: nonUserCodeProblem # For Simulator. TODO: refactor all the god:* Mediator events to be local events.
2015-04-25 20:29:02 -04:00
@ reportLoadError ( ) if nonUserCodeProblem
2014-05-10 21:24:50 -04:00
@ fireWorker ( )
2015-04-25 20:29:02 -04:00
reportLoadError: ->
2015-05-11 21:53:42 -04:00
return if me . isAdmin ( ) or /dev=true/ . test ( window . location ? . href ? ' ' ) or reportedLoadErrorAlready
reportedLoadErrorAlready = true
2015-04-25 20:29:02 -04:00
context = email: me . get ( ' email ' )
context.message = " Automatic Report - Unable to Load Level \n Logs: \n " + @ allLogs . join ( ' \n ' )
if $ . browser
context.browser = " #{ $ . browser . platform } #{ $ . browser . name } #{ $ . browser . versionNumber } "
context.screenSize = " #{ screen ? . width ? $ ( window ) . width ( ) } x #{ screen ? . height ? $ ( window ) . height ( ) } "
context.subject = " Level Load Error: #{ @ work ? . level ? . name or ' Unknown Level ' } "
context.levelSlug = @ work ? . level ? . slug
2015-05-11 21:53:42 -04:00
sendContactMessage context
2015-04-25 20:29:02 -04:00
2014-05-10 21:24:50 -04:00
doWork: ->
return if @ aborting
2014-06-30 22:16:26 -04:00
return @ say ' Not initialized for work yet. ' unless @ initialized
2014-05-10 21:24:50 -04:00
if @ shared . workQueue . length
2014-05-11 20:42:32 -04:00
@work = @ shared . workQueue . shift ( )
return _ . defer @ simulateSync , @ work if @ work . synchronous
2014-06-30 22:16:26 -04:00
@ say ' Running world... '
2014-05-10 21:24:50 -04:00
@running = true
@ shared . busyAngels . push @
2014-08-22 15:39:29 -04:00
@deserializationQueue = [ ]
2014-05-11 20:42:32 -04:00
@ worker . postMessage func: ' runWorld ' , args: @ work
2014-05-10 21:24:50 -04:00
clearTimeout @ purgatoryTimer
2014-06-30 22:16:26 -04:00
@ say ' Infinite loop timer started at interval of ' , @ infiniteLoopIntervalDuration
2014-05-10 21:24:50 -04:00
@purgatoryTimer = setInterval @ testWorker , @ infiniteLoopIntervalDuration
else
2014-06-30 22:16:26 -04:00
@ say ' No work to do. '
2014-05-10 21:24:50 -04:00
@ hireWorker ( )
abort: ->
return unless @ worker and @ running
2014-06-30 22:16:26 -04:00
@ say ' Aborting... '
2014-05-10 21:24:50 -04:00
@running = false
2014-05-11 20:42:32 -04:00
@work = null
2014-08-22 17:59:32 -04:00
@streamingWorld = null
@deserializationQueue = null
2014-05-10 21:24:50 -04:00
_ . remove @ shared . busyAngels , @
@abortTimeout = _ . delay @ fireWorker , @ abortTimeoutDuration
@aborting = true
@ worker . postMessage func: ' abort '
fireWorker: (rehire=true) =>
2014-10-25 14:47:04 -04:00
return if @ destroyed
2014-05-10 21:24:50 -04:00
@aborting = false
@running = false
_ . remove @ shared . busyAngels , @
@ worker ? . removeEventListener ' message ' , @ onWorkerMessage
@ worker ? . terminate ( )
@worker = null
clearTimeout @ condemnTimeout
clearInterval @ purgatoryTimer
2014-06-30 22:16:26 -04:00
@ say ' Fired worker. '
2014-05-10 21:24:50 -04:00
@initialized = false
@work = null
2014-10-02 18:33:23 -04:00
@streamingWorld = null
@deserializationQueue = null
2014-05-10 21:24:50 -04:00
@ hireWorker ( ) if rehire
hireWorker: ->
2014-09-29 02:24:18 -04:00
unless Worker ?
unless @ initialized
@initialized = true
@ doWork ( )
return null
2014-05-10 21:24:50 -04:00
return if @ worker
2014-06-30 22:16:26 -04:00
@ say ' Hiring worker. '
2014-05-10 21:24:50 -04:00
@worker = new Worker @ shared . workerCode
@ worker . addEventListener ' message ' , @ onWorkerMessage
@worker.creationTime = new Date ( )
2014-08-23 20:26:56 -04:00
onFlagEvent: (e) ->
2014-08-23 00:35:08 -04:00
return unless @ running and @ work . realTime
2014-08-23 20:26:56 -04:00
@ worker . postMessage func: ' addFlagEvent ' , args: e
2014-08-23 00:35:08 -04:00
2014-08-26 01:05:24 -04:00
onStopRealTimePlayback: (e) ->
return unless @ running and @ work . realTime
@work.realTime = false
2015-04-18 22:54:34 -04:00
@lastRealTimeWork = new Date ( )
2014-08-26 01:05:24 -04:00
@ worker . postMessage func: ' stopRealTimePlayback '
2014-05-10 21:24:50 -04:00
2015-04-18 22:54:34 -04:00
onEscapePressed: (e) ->
return unless @ running and not @ work . realTime
return if ( new Date ( ) - @ lastRealTimeWork ) < 1000 # Fires right after onStopRealTimePlayback
@ infinitelyLooped true
2014-05-10 21:24:50 -04:00
#### Synchronous code for running worlds on main thread (profiling / IE9) ####
simulateSync: (work) =>
console ? . profile ? " World Generation #{ ( Math . random ( ) * 1000 ) . toFixed ( 0 ) } " if imitateIE9 ?
work.t0 = now ( )
work.testWorld = testWorld = new World work . userCodeMap
2014-11-26 15:40:16 -05:00
work.testWorld.levelSessionIDs = work . levelSessionIDs
work.testWorld.submissionCount = work . submissionCount
work.testWorld.flagHistory = work . flagHistory ? [ ]
2015-01-05 13:44:17 -05:00
work.testWorld.difficulty = work . difficulty
2014-05-10 21:24:50 -04:00
testWorld . loadFromLevel work . level
2014-11-26 15:40:16 -05:00
work.testWorld.preloading = work . preload
work.testWorld.headless = work . headless
work.testWorld.realTime = work . realTime
2014-05-10 21:24:50 -04:00
if @ shared . goalManager
2014-09-30 13:15:33 -04:00
testGM = new GoalManager ( testWorld )
2014-05-10 21:24:50 -04:00
testGM . setGoals work . goals
testGM . setCode work . userCodeMap
testGM . worldGenerationWillBegin ( )
testWorld . setGoalManager testGM
@ doSimulateWorld work
console ? . profileEnd ? ( ) if imitateIE9 ?
2014-06-30 22:16:26 -04:00
console . log ' Construction: ' , ( work . t1 - work . t0 ) . toFixed ( 0 ) , ' ms. Simulation: ' , ( work . t2 - work . t1 ) . toFixed ( 0 ) , ' ms -- ' , ( ( work . t2 - work . t1 ) / testWorld . frames . length ) . toFixed ( 3 ) , ' ms per frame, profiled. '
2014-05-10 21:24:50 -04:00
# If performance was really a priority in IE9, we would rework things to be able to skip this step.
goalStates = testGM ? . getGoalStates ( )
2014-09-29 02:24:18 -04:00
work . testWorld . goalManager . worldGenerationEnded ( ) if work . testWorld . ended
serialized = testWorld . serialize ( )
2014-05-10 21:24:50 -04:00
window . BOX2D_ENABLED = false
2014-11-26 15:40:16 -05:00
World . deserialize serialized . serializedWorld , @ shared . worldClassMap , @ shared . lastSerializedWorldFrames , @ finishBeholdingWorld ( goalStates ) , serialized . startFrame , serialized . endFrame , work . level
2014-05-10 21:24:50 -04:00
window . BOX2D_ENABLED = true
2014-09-29 02:24:18 -04:00
@shared.lastSerializedWorldFrames = serialized . serializedWorld . frames
2014-05-10 21:24:50 -04:00
doSimulateWorld: (work) ->
work.t1 = now ( )
Math . random = work . testWorld . rand . randf # so user code is predictable
2014-06-30 22:16:26 -04:00
Aether . replaceBuiltin ( ' Math ' , Math )
2014-07-19 23:26:13 -04:00
replacedLoDash = _ . runInContext ( window )
_ [ key ] = replacedLoDash [ key ] for key , val of replacedLoDash
2014-05-10 21:24:50 -04:00
i = 0
while i < work . testWorld . totalFrames
frame = work . testWorld . getFrame i ++
2015-11-29 15:30:19 -05:00
Backbone . Mediator . publish ' god:world-load-progress-changed ' , progress: 1 , god: @ shared . god
2014-05-10 21:24:50 -04:00
work.testWorld.ended = true
system . finish work . testWorld . thangs for system in work . testWorld . systems
work.t2 = now ( )