Fixed many issues with world streaming.

This commit is contained in:
Nick Winter 2014-08-22 12:39:29 -07:00
parent 9b31e28536
commit 611ecbf470
5 changed files with 62 additions and 44 deletions

View file

@ -64,16 +64,6 @@ module.exports = class Angel extends CocoClass
clearTimeout @condemnTimeout
@beholdGoalStates event.data.goalStates # Work ends here if we're headless.
# We pay attention to certain progress indicators as the world loads.
when 'world-load-progress-changed'
Backbone.Mediator.publish 'god:world-load-progress-changed', event.data
unless event.data.progress is 1 or @work.preload or @work.headless or @work.synchronous or @deserializingStreamingFrames or @shared.firstWorld
@worker.postMessage func: 'serializeFramesSoFar' # Stream it!
when 'console-log'
@log event.data.args...
when 'user-code-problem'
Backbone.Mediator.publish 'god:user-code-problem', problem: event.data.problem
# We have to abort like an infinite loop if we see one of these; they're not really recoverable
when 'non-user-code-problem'
Backbone.Mediator.publish 'god:non-user-code-problem', problem: event.data.problem
@ -82,16 +72,7 @@ module.exports = class Angel extends CocoClass
else
@fireWorker()
# We have some of the frames serialized, so let's send the partially simulated world to the Surface.
when 'some-frames-serialized'
#console.log "angel received some frames", event.data.serialized, "with goals", event.data.goalStates, "and streaming into world", @shared.streamingWorld
@deserializingStreamingFrames = true
@beholdWorld event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @shared.streamingWorld
# Either the world finished simulating successfully, or we abort the worker.
when 'new-world'
#console.log "angel received alll frames", event.data.serialized, "and have streaming world", @shared.streamingWorld
@beholdWorld event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @shared.streamingWorld
# If it didn't finish simulating successfully, or we abort the worker.
when 'abort'
@say 'Aborted.', event.data
clearTimeout @abortTimeout
@ -100,6 +81,23 @@ module.exports = class Angel extends CocoClass
_.remove @shared.busyAngels, @
@doWork()
# We pay attention to certain progress indicators as the world loads.
when 'console-log'
@log event.data.args...
when 'user-code-problem'
Backbone.Mediator.publish 'god:user-code-problem', problem: event.data.problem
when 'world-load-progress-changed'
Backbone.Mediator.publish 'god:world-load-progress-changed', event.data
unless event.data.progress is 1 or @work.preload or @work.headless or @work.synchronous or @deserializationQueue.length or @shared.firstWorld
@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'
deserializationArgs = [event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @shared.streamingWorld]
@deserializationQueue.push deserializationArgs
if @deserializationQueue.length is 1
@beholdWorld deserializationArgs...
else
@log 'Received unsupported message:', event.data
@ -131,12 +129,14 @@ module.exports = class Angel extends CocoClass
@shared.goalManager?.world = world
@finishWork()
else
@deserializingStreamingFrames = false
@deserializationQueue.shift() # Finished with this deserialization.
if deserializationArgs = @deserializationQueue[0] # Start another?
@beholdWorld deserializationArgs...
finishWork: ->
@shared.streamingWorld = null
@shared.firstWorld = false
@deserializingStreamingFrames = false
@deserializationQueue = []
@running = false
_.remove @shared.busyAngels, @
@doWork()
@ -163,6 +163,7 @@ module.exports = class Angel extends CocoClass
@say 'Running world...'
@running = true
@shared.busyAngels.push @
@deserializationQueue = []
@worker.postMessage func: 'runWorld', args: @work
clearTimeout @purgatoryTimer
@say 'Infinite loop timer started at interval of', @infiniteLoopIntervalDuration

View file

@ -378,7 +378,7 @@ module.exports = Surface = class Surface extends CocoClass
ffScrubDuration = 1000 * ffToTime / ffSpeed
ffScrubDuration = Math.min(ffScrubDuration, ffInterval)
ffFactor = ffInterval / ffScrubDuration
if ffFactor > 2
if ffFactor > 1.5
createjs.Tween.removeTweens(@)
@scrubbingTo = null
@fastForwarding = true

View file

@ -85,7 +85,7 @@ module.exports = class Thang
throw new Error "Two types were specified for trackable property #{prop}: #{oldType} and #{type}."
keepTrackedProperty: (prop) ->
# Hmm; can we do this faster?
# Wish we could do this faster, but I can't think of how.
propIndex = @trackedPropertiesKeys.indexOf prop
if propIndex isnt -1
@trackedPropertiesUsed[propIndex] = true
@ -147,6 +147,8 @@ module.exports = class Thang
for trackedFinalProperty in @trackedFinalProperties ? []
# TODO: take some (but not all) of serialize logic from ThangState to handle other types
o.finalState[trackedFinalProperty] = @[trackedFinalProperty]
# Since we might keep tracked properties later during streaming, we need to know which we think are unused.
o.unusedTrackedPropertyKeys = (@trackedPropertiesKeys[propIndex] for used, propIndex in @trackedPropertiesUsed when not used)
o
@deserialize: (o, world, classMap) ->
@ -154,6 +156,8 @@ module.exports = class Thang
for [componentClassName, componentConfig] in o.components
componentClass = classMap[componentClassName]
t.addComponents [componentClass, componentConfig]
t.unusedTrackedPropertyKeys = o.unusedTrackedPropertyKeys
t.unusedTrackedPropertyValues = (t[prop] for prop in o.unusedTrackedPropertyKeys)
for prop, val of o.finalState
# TODO: take some (but not all) of deserialize logic from ThangState to handle other types
t[prop] = val

View file

@ -65,7 +65,10 @@ module.exports = class ThangState
# Get the property, whether we have it stored in @props or in @trackedPropertyValues. Optimize it.
# Figured based on http://jsperf.com/object-vs-array-vs-native-linked-list/13 that it should be faster with small arrays to do the indexOf reads (each up to 24x faster) than to do a single object read, and then we don't have to maintain an extra @props object; just keep array
propIndex = @trackedPropertyKeys.indexOf prop
return null if propIndex is -1
if propIndex is -1
initialPropIndex = @thang.unusedTrackedPropertyKeys.indexOf prop
return null if initialPropIndex is -1
return @thang.unusedTrackedPropertyValues[initialPropIndex]
value = @props[propIndex]
return value if value isnt undefined or @hasRestored
return @props[propIndex] = @getStoredProp propIndex
@ -74,22 +77,28 @@ module.exports = class ThangState
# Restore trackedProperties' values to @thang, retrieving them from @trackedPropertyValues if needed. Optimize it.
return @ if @thang._state is @ and not @thang.partialState
unless @hasRestored # Restoring in a deserialized World for first time
for prop, propIndex in @thang.unusedTrackedPropertyKeys when @trackedPropertyKeys.indexOf(prop) is -1
@thang[prop] = @thang.unusedTrackedPropertyValues[propIndex]
props = []
for prop, propIndex in @trackedPropertyKeys
type = @trackedPropertyTypes[propIndex]
storage = @trackedPropertyValues[propIndex]
props.push(@thang[prop] = @getStoredProp propIndex, type, storage)
props.push @thang[prop] = @getStoredProp propIndex, type, storage
#console.log @frameIndex, @thang.id, prop, propIndex, type, storage, 'got', @thang[prop]
@props = props
@trackedPropertyTypes = @trackedPropertyValues = @specialKeysToValues = null # leave @trackedPropertyKeys for indexing
@hasRestored = true
else # Restoring later times
for prop, propIndex in @thang.unusedTrackedPropertyKeys when @trackedPropertyKeys.indexOf(prop) is -1
@thang[prop] = @thang.unusedTrackedPropertyValues[propIndex]
for prop, propIndex in @trackedPropertyKeys
@thang[prop] = @props[propIndex]
@thang.partialState = false
@
restorePartial: (ratio) ->
# Don't think we need to worry about unusedTrackedPropertyValues here.
# If it's not tracked yet, it'll very rarely partially change between frames; we can afford to miss the first one.
inverse = 1 - ratio
for prop, propIndex in @trackedPropertyKeys when prop is 'pos' or prop is 'rotation'
if @hasRestored

View file

@ -11,7 +11,7 @@ WorldScriptNote = require './world_script_note'
Component = require 'lib/world/component'
System = require 'lib/world/system'
PROGRESS_UPDATE_INTERVAL = 100
DESERIALIZATION_INTERVAL = 20
DESERIALIZATION_INTERVAL = 10
ITEM_ORIGINAL = '53e12043b82921000051cdf9'
module.exports = class World
@ -291,7 +291,7 @@ module.exports = class World
# Code hotspot; optimize it
startFrame = @framesSerializedSoFar
endFrame = @frames.length
console.log "... world serializing frames from", startFrame, "to", endFrame
#console.log "... world serializing frames from", startFrame, "to", endFrame
[transferableObjects, nontransferableObjects] = [0, 0]
o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}}
o.trackedProperties[prop] = @[prop] for prop in @trackedProperties or []
@ -307,20 +307,21 @@ module.exports = class World
o.trackedPropertiesPerThangKeys = []
o.trackedPropertiesPerThangTypes = []
trackedPropertiesPerThangValues = [] # We won't send these, just the offsets and the storage buffer
o.trackedPropertiesPerThangValuesOffsets = [] # Needed to reconstruct ArrayBufferViews on other end, since Firefox has bugs transfering those: https://bugzilla.mozilla.org/show_bug.cgi?id=841904 and https://bugzilla.mozilla.org/show_bug.cgi?id=861925 # Actually, as of January 2014, it should be fixed.
o.trackedPropertiesPerThangValuesOffsets = [] # Needed to reconstruct ArrayBufferViews on other end, since Firefox has bugs transfering those: https://bugzilla.mozilla.org/show_bug.cgi?id=841904 and https://bugzilla.mozilla.org/show_bug.cgi?id=861925 # Actually, as of January 2014, it should be fixed. So we could try to undo the workaround.
transferableStorageBytesNeeded = 0
nFrames = endFrame - startFrame
streaming = nFrames < @totalFrames
for thang in @thangs
# Don't serialize empty trackedProperties for stateless Thangs which haven't changed (like obstacles).
# Check both, since sometimes people mark stateless Thangs but don't change them, and those should still be tracked, and the inverse doesn't work on the other end (we'll just think it doesn't exist then).
continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean) and not streaming
# Check both, since sometimes people mark stateless Thangs but then change them, and those should still be tracked, and the inverse doesn't work on the other end (we'll just think it doesn't exist then).
# If streaming the world, a thang marked stateless that actually change will get messed up. I think.
continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean)
o.trackedPropertiesThangIDs.push thang.id
trackedPropertiesIndices = []
trackedPropertiesKeys = []
trackedPropertiesTypes = []
for used, propIndex in thang.trackedPropertiesUsed
continue unless used# or streaming
continue unless used
trackedPropertiesIndices.push propIndex
trackedPropertiesKeys.push thang.trackedPropertiesKeys[propIndex]
trackedPropertiesTypes.push thang.trackedPropertiesTypes[propIndex]
@ -418,6 +419,7 @@ module.exports = class World
perf.t3 = now()
perf.batches = 0
perf.framesCPUTime = 0
w.frames = [] unless streamingWorld
clearTimeout @deserializationTimeout if @deserializationTimeout
@deserializationTimeout = _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf, startFrame, endFrame
@ -428,26 +430,27 @@ module.exports = class World
startTime = now()
for frameIndex in [w.frames.length ... endFrame]
w.frames.push WorldFrame.deserialize(w, frameIndex - startFrame, o.trackedPropertiesThangIDs, o.trackedPropertiesThangs, o.trackedPropertiesPerThangKeys, o.trackedPropertiesPerThangTypes, o.trackedPropertiesPerThangValues, o.specialKeysToValues, o.frameHashes[frameIndex - startFrame], w.dt * frameIndex)
if (now() - startTime) > DESERIALIZATION_INTERVAL
console.log " Deserialization not finished, let's do it again soon. Have:", w.frames.length, ", wanted from", startFrame, "to", endFrame
elapsed = now() - startTime
if elapsed > DESERIALIZATION_INTERVAL and frameIndex < endFrame - 1
#console.log " Deserialization not finished, let's do it again soon. Have:", w.frames.length, ", wanted from", startFrame, "to", endFrame
perf.framesCPUTime += elapsed
@deserializationTimeout = _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf, startFrame, endFrame
return
@deserializationTimeout = null
perf.framesCPUTime += elapsed
@finishDeserializing w, finishedWorldCallback, perf, startFrame, endFrame
@finishDeserializing: (w, finishedWorldCallback, perf, startFrame, endFrame) ->
perf.t4 = now()
nFrames = endFrame - startFrame
w.ended = true
w.getFrame(endFrame - 1).restoreState()
perf.t5 = now()
console.log 'Deserialization:', (perf.t5 - perf.t0).toFixed(0) + 'ms (' + ((perf.t5 - perf.t0) / nFrames).toFixed(3) + 'ms per frame).', perf.batches, 'batches.'
nFrames = endFrame - startFrame
totalCPUTime = perf.t3 - perf.t0 + perf.framesCPUTime
console.log 'Deserialization:', totalCPUTime.toFixed(0) + 'ms (' + (totalCPUTime / nFrames).toFixed(3) + 'ms per frame).', perf.batches, 'batches. Did', startFrame, 'to', endFrame, 'in', (perf.t4 - perf.t0).toFixed(0) + 'ms wall clock time.'
if false
console.log ' Deserializing--constructing new World:', (perf.t1 - perf.t0).toFixed(2) + 'ms'
console.log ' Deserializing--Thangs and ScriptNotes:', (perf.t2 - perf.t1).toFixed(2) + 'ms'
console.log ' Deserializing--reallocating memory:', (perf.t3 - perf.t2).toFixed(2) + 'ms'
console.log ' Deserializing--WorldFrames:', (perf.t4 - perf.t3).toFixed(2) + 'ms'
console.log ' Deserializing--restoring last WorldFrame:', (perf.t5 - perf.t4).toFixed(2) + 'ms'
console.log ' Deserializing--WorldFrames:', (perf.t4 - perf.t3).toFixed(2) + 'ms wall clock time,', (perf.framesCPUTime).toFixed(2) + 'ms CPU time'
finishedWorldCallback w
findFirstChangedFrame: (oldWorld) ->
@ -456,10 +459,11 @@ module.exports = class World
oldFrame = oldWorld.frames[i]
break unless oldFrame and ((newFrame.hash is oldFrame.hash) or not newFrame.hash? or not oldFrame.hash?) # undefined gets in there when streaming at the last frame of each batch for some reason
firstChangedFrame = i
if @frames[i]
console.log 'First changed frame is', firstChangedFrame, 'with hash', @frames[i].hash, 'compared to', oldWorld.frames[i]?.hash
else
console.log 'No frames were changed out of all', @frames.length
if @frames.length is @totalFrames
if @frames[i]
console.log 'First changed frame is', firstChangedFrame, 'with hash', @frames[i].hash, 'compared to', oldWorld.frames[i]?.hash
else
console.log 'No frames were changed out of all', @frames.length
firstChangedFrame
pointsForThang: (thangID, frameStart=0, frameEnd=null, camera=null, resolution=4) ->