mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-01 03:16:56 -05:00
241 lines
9.9 KiB
CoffeeScript
241 lines
9.9 KiB
CoffeeScript
#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)
|
|
@latestGoalStates = goalStates
|
|
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 @
|
|
@shared.firstWorld = false
|
|
@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
|
|
console.log @id + " ready and looking for work. WorkQueue length is " + @shared.workQueue.length
|
|
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..."
|
|
#console.error "worker.postMessage: " + @worker.postMessage + ", work: " + work
|
|
@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
|