2014-05-05 20:37:14 -04:00
#Sane rewrite of God (a thread pool)
{ now } = require ' lib/world/world_utils '
World = require ' lib/world/world '
# ##
Every Angel has exactly one WebWorker attached to it .
It will call methods inside the webwrker and kill it if it times out .
# ##
class Angel
@cyanide: 0xDEADBEEF
infiniteLoopIntervalDuration: 7500 # check this often (must be more than the others added)
infiniteLoopTimeoutDuration: 10000 # wait this long when we check
abortTimeoutDuration: 500 # give in-process or dying workers this long to give up
constructor: (@id, @shared) ->
console . log @ id + " : Creating Angel "
if ( navigator . userAgent or navigator . vendor or window . opera ) . search ( " MSIE " ) isnt - 1
@ infiniteLoopIntervalDuration *= 20 # since it's so slow to serialize without transferable objects, we can't trust it
@ infiniteLoopTimeoutDuration *= 20
@ abortTimeoutDuration *= 10
@initialized = false
@running = false
@ hireWorker ( )
@ shared . angels . push @
testWorker: =>
if @ initialized
@ worker . postMessage { func: ' reportIn ' }
# Are there any errors when webworker isn't loaded properly?
onWorkerMessage: (event) =>
#console.log JSON.stringify event
if @ aborting and not
event . data . type is ' abort '
console . log id + " is currently aborting old work. "
return
switch event . data . type
when ' start-load-frames '
clearTimeout ( @ condemnTimeout )
@condemnTimeout = _ . delay @ infinitelyLooped , @ infiniteLoopTimeoutDuration
when ' end-load-frames '
console . log @ id + ' : No condemn this time. '
clearTimeout ( @ condemnTimeout )
when ' worker-initialized '
unless @ initialized
console . log @ id + " : Worker initialized after " , ( ( new Date ( ) ) - @ worker . creationTime ) , " ms "
@initialized = true
@ doWork ( )
when ' new-world '
@ beholdWorld event . data . serialized , event . data . goalStates
when ' world-load-progress-changed '
Backbone . Mediator . publish ' god:world-load-progress-changed ' , event . data
when ' console-log '
console . log " | " + @ id + " | " , event . data . args . . .
when ' user-code-problem '
Backbone . Mediator . publish ' god:user-code-problem ' , problem: event . data . problem
when ' abort '
console . log @ id , " aborted. "
clearTimeout @ abortTimeout
@aborting = false
@running = false
@ shared . busyAngels . pop @
@ doWork ( )
when ' reportIn '
clearTimeout @ condemnTimeout
else
console . log @ id + " received unsupported message: " , event . data
beholdWorld: (serialized, goalStates) ->
return if @ aborting
unless serialized
# We're only interested in goalStates. (Simulator)
2014-05-06 12:49:04 -04:00
@latestGoalStates = goalStates
2014-05-05 20:37:14 -04:00
Backbone . Mediator . publish ( ' god:goals-calculated ' , goalStates: goalStates )
@running = false
@ shared . busyAngels . pop @
# console.warn "Goal states: " + JSON.stringify(goalStates)
window . BOX2D_ENABLED = false # Flip this off so that if we have box2d in the namespace, the Collides Components still don't try to create bodies for deserialized Thangs upon attachment
World . deserialize serialized , @ shared . worldClassMap , @ lastSerializedWorldFrames , @ finishBeholdingWorld ( goalStates )
window . BOX2D_ENABLED = true
@lastSerializedWorldFrames = serialized . frames
finishBeholdingWorld: (goalStates) => (world) =>
return if @ aborting
world . findFirstChangedFrame @ shared . world
@shared.world = world
errorCount = ( t for t in @ shared . world . thangs when t . errorsOut ) . length
Backbone . Mediator . publish ( ' god:new-world-created ' , world: world , firstWorld: @ shared . firstWorld , errorCount: errorCount , goalStates: goalStates )
for scriptNote in @ shared . world . scriptNotes
Backbone . Mediator . publish scriptNote . channel , scriptNote . event
@ shared . goalManager ? . world = world
@running = false
@ shared . busyAngels . pop @
2014-05-06 12:49:04 -04:00
@shared.firstWorld = false
2014-05-05 20:37:14 -04:00
@ doWork ( )
infinitelyLooped: =>
unless @ aborting
problem = type: " runtime " , level: " error " , id: " runtime_InfiniteLoop " , message: " Code never finished. It ' s either really slow or has an infinite loop. "
Backbone . Mediator . publish ' god:user-code-problem ' , problem: problem
Backbone . Mediator . publish ' god:infinite-loop ' , firstWorld: @ shared . firstWorld
@ fireWorker ( )
workIfIdle: ->
@ doWork ( ) unless @ running
doWork: =>
#console.log "work."
return if @ aborted
2014-05-06 12:49:04 -04:00
console . log @ id + " ready and looking for work. WorkQueue length is " + @ shared . workQueue . length
2014-05-05 20:37:14 -04:00
if @ initialized and @ shared . workQueue . length
work = @ shared . workQueue . pop ( )
if work is Angel . cyanide # Kill all other Angels, too
console . log @ id + " : ' work is poison ' "
@ shared . workQueue . push Angel . cyanide
@ free ( )
else
console . log @ id + " : Sending the worker to work. "
@running = true
@ shared . busyAngels . push @
console . log " Running world... "
2014-05-05 23:07:34 -04:00
#console.error "worker.postMessage: " + @worker.postMessage + ", work: " + work
2014-05-05 20:37:14 -04:00
@ worker . postMessage func: ' runWorld ' , args: work
console . log @ id + " : Setting interval. "
clearTimeout @ purgatoryTimer
@purgatoryTimer = setInterval @ testWorker , @ infiniteLoopIntervalDuration
else
console . log " No work for " + @ id
@ hireWorker ( )
abort: =>
if @ worker and @ running
console . log " Aborting " + @ id
@running = false
@ shared . busyAngels . pop @
@abortTimeout = _ . delay @ terminate , @ fireWorker , @ abortTimeoutDuration
@ worker . postMessage func: ' abort '
@aborting = true
@work = null
fireWorker: (rehire=true) =>
@aborting = false
@running = false
@ shared . busyAngels . pop @
@ worker ? . removeEventListener ' message ' , @ onWorkerMessage
@ worker ? . terminate ( )
@worker = null
clearTimeout @ condemnTimeout
clearInterval @ purgatoryTimer
console . log " Fired worker. "
@initialized = false
@work = null
@ hireWorker ( ) if rehire
hireWorker: ->
unless @ worker
console . log @ id + " : Hiring worker. "
@worker = new Worker @ shared . workerCode
@ worker . addEventListener ' message ' , @ onWorkerMessage
@worker.creationTime = new Date ( )
#@worker.postMessage func: 'initialized' else
kill: ->
@ fireWorker false
@ shared . angels . pop @
clearTimeout @ condemnTimeout
clearTimeout @ purgatoryTimer
@purgatoryTimer = null
@condemnTimeout = null
module.exports = class God
ids: [ ' Athena ' , ' Baldr ' , ' Crom ' , ' Dagr ' , ' Eris ' , ' Freyja ' , ' Great Gish ' , ' Hades ' , ' Ishtar ' , ' Janus ' , ' Khronos ' , ' Loki ' , ' Marduk ' , ' Negafook ' , ' Odin ' , ' Poseidon ' , ' Quetzalcoatl ' , ' Ra ' , ' Shiva ' , ' Thor ' , ' Umvelinqangi ' , ' Týr ' , ' Vishnu ' , ' Wepwawet ' , ' Xipe Totec ' , ' Yahweh ' , ' Zeus ' , ' 上帝 ' , ' Tiamat ' , ' 盘古 ' , ' Phoebe ' , ' Artemis ' , ' Osiris ' , " 嫦娥 " , ' Anhur ' , ' Teshub ' , ' Enlil ' , ' Perkele ' , ' Chaos ' , ' Hera ' , ' Iris ' , ' Theia ' , ' Uranus ' , ' Stribog ' , ' Sabazios ' , ' Izanagi ' , ' Ao ' , ' Tāwhirimātea ' , ' Tengri ' , ' Inmar ' , ' Torngarsuk ' , ' Centzonhuitznahua ' , ' Hunab Ku ' , ' Apollo ' , ' Helios ' , ' Thoth ' , ' Hyperion ' , ' Alectrona ' , ' Eos ' , ' Mitra ' , ' Saranyu ' , ' Freyr ' , ' Koyash ' , ' Atropos ' , ' Clotho ' , ' Lachesis ' , ' Tyche ' , ' Skuld ' , ' Urðr ' , ' Verðandi ' , ' Camaxtli ' , ' Huhetotl ' , ' Set ' , ' Anu ' , ' Allah ' , ' Anshar ' , ' Hermes ' , ' Lugh ' , ' Brigit ' , ' Manannan Mac Lir ' , ' Persephone ' , ' Mercury ' , ' Venus ' , ' Mars ' , ' Azrael ' , ' He-Man ' , ' Anansi ' , ' Issek ' , ' Mog ' , ' Kos ' , ' Amaterasu Omikami ' , ' Raijin ' , ' Susanowo ' , ' Blind Io ' , ' The Lady ' , ' Offler ' , ' Ptah ' , ' Anubis ' , ' Ereshkigal ' , ' Nergal ' , ' Thanatos ' , ' Macaria ' , ' Angelos ' , ' Erebus ' , ' Hecate ' , ' Hel ' , ' Orcus ' , ' Ishtar-Deela Nakh ' , ' Prometheus ' , ' Hephaestos ' , ' Sekhmet ' , ' Ares ' , ' Enyo ' , ' Otrera ' , ' Pele ' , ' Hadúr ' , ' Hachiman ' , ' Dayisun Tngri ' , ' Ullr ' , ' Lua ' , ' Minerva ' ]
nextID: ->
@lastID = ( if @ lastID ? then @ lastID + 1 else Math . floor ( @ ids . length * Math . random ( ) ) ) % @ ids . length
@ ids [ @ lastID ]
# Charlie's Angels are all given access to this.
angelsShare: {
workerCode: ' /javascripts/workers/worker_world.js ' # Either path or function
workQueue: [ ]
firstWorld: true
world: undefined
goalManager: undefined
worldClassMap: undefined
angels: [ ]
busyAngels: [ ] # Busy angels will automatically register here.
}
constructor: (options) ->
options ? = { }
@angelsShare.workerCode = options . workerCode if options . workerCode
# ~20MB per idle worker + angel overhead - in this implementation, every Angel maps to 1 worker
angelCount = options . maxAngels ? options . maxWorkerPoolSize ? 2 # How many concurrent Angels/web workers to use at a time
_ . delay ( => new Angel @ nextID ( ) , @ angelsShare ) , 250 * i for i in [ 0 . . . angelCount ] # Don't generate all Angels at once.
Backbone . Mediator . subscribe ' tome:cast-spells ' , @ onTomeCast , @
onTomeCast: (e) ->
@ createWorld e . spells
setGoalManager: (goalManager) =>
@angelsShare.goalManager = goalManager
setWorldClassMap: (worldClassMap) =>
@angelsShare.worldClassMap = worldClassMap
getUserCodeMap: (spells) ->
userCodeMap = { }
for spellKey , spell of spells
for thangID , spellThang of spell . thangs
( userCodeMap [ thangID ] ? = { } ) [ spell . name ] = spellThang . aether . serialize ( )
#console.log userCodeMap
userCodeMap
createWorld: (spells) =>
angel . abort ( ) for angel in @ angelsShare . busyAngels # We really only ever want one world calculated per God
#console.log "Level: " + @level
@ angelsShare . workQueue . push
worldName: @ level . name
userCodeMap: @ getUserCodeMap ( spells )
level: @ level
goals: @ angelsShare . goalManager ? . getGoals ( )
angel . workIfIdle ( ) for angel in @ angelsShare . angels
destroy: =>
console . log " Destroying Buddha "
@createWorld = -> console . log " CreateWorld already gone. "
@ angelsShare . workQueue . push Angel . cyanide
angel . kill for angel in @ angelsShare . busyAngels
Backbone . Mediator . unsubscribe ( ' tome:cast-spells ' , @ onTomeCast , @ )
@ angelsShare . goalManager ? . destroy ( )
@angelsShare.goalManager = null
@angelsShare = null