diff --git a/addons/netfox/servers/data/object-snapshot.gd b/addons/netfox/servers/data/object-snapshot.gd index 4119df85..a05102b4 100644 --- a/addons/netfox/servers/data/object-snapshot.gd +++ b/addons/netfox/servers/data/object-snapshot.gd @@ -30,9 +30,8 @@ func record_property(property: NodePath) -> void: set_value(property, _object.get_indexed(property)) func apply() -> void: - for property in properties(): - var value := get_value(property) - _object.set_indexed(property, value) + for property in _data: + _object.set_indexed(property, _data[property]) func is_auth() -> bool: return _is_auth diff --git a/addons/netfox/servers/data/per-object-history.gd b/addons/netfox/servers/data/per-object-history.gd index 450a4ae9..891e1fc0 100644 --- a/addons/netfox/servers/data/per-object-history.gd +++ b/addons/netfox/servers/data/per-object-history.gd @@ -15,6 +15,9 @@ func subjects() -> Array[Object]: result.assign(_data.keys()) return result +func subjects_raw() -> Array: + return _data.keys() + func is_auth(tick: int, subject: Object) -> bool: if not _data.has(subject): return false @@ -30,7 +33,6 @@ func erase_subject(subject: Object) -> void: _data.erase(subject) func ensure_snapshot(tick: int, subject: Object, carry_forward: bool) -> _ObjectSnapshot: - var has_subject := _data.has(subject) if not _data.has(subject): _data[subject] = _HistoryBuffer.new(_history_size) var history := _data[subject] as _HistoryBuffer diff --git a/addons/netfox/servers/data/property-pool.gd b/addons/netfox/servers/data/property-pool.gd index afd822db..3f48bcd2 100644 --- a/addons/netfox/servers/data/property-pool.gd +++ b/addons/netfox/servers/data/property-pool.gd @@ -51,11 +51,17 @@ func get_properties_of(subject: Object) -> Array[NodePath]: properties.assign(_properties_by_subject.get(subject, [])) return properties +func get_properties_of_raw(subject: Object) -> Array: + return _properties_by_subject.get(subject, []) + func get_subjects() -> Array[Object]: var subjects := [] as Array[Object] subjects.assign(_properties_by_subject.keys()) return subjects +func get_subjects_raw() -> Array: + return _properties_by_subject.keys() + func is_empty() -> bool: return _properties_by_subject.is_empty() diff --git a/addons/netfox/servers/data/snapshot.gd b/addons/netfox/servers/data/snapshot.gd index 52aac846..40340090 100644 --- a/addons/netfox/servers/data/snapshot.gd +++ b/addons/netfox/servers/data/snapshot.gd @@ -102,9 +102,9 @@ func merge(snapshot: _Snapshot) -> bool: func apply() -> void: for subject in _data: - for property in _data[subject]: - var value = _data[subject][property] - (subject as Object).set_indexed(property, value) + var props := _data[subject] as Dictionary + for property in props: + (subject as Object).set_indexed(property, props[property]) func sanitize(sender: int) -> void: var invalid_subjects := [] diff --git a/addons/netfox/servers/network-history-server.gd b/addons/netfox/servers/network-history-server.gd index 6d511f02..aeb66a9a 100644 --- a/addons/netfox/servers/network-history-server.gd +++ b/addons/netfox/servers/network-history-server.gd @@ -196,7 +196,7 @@ func _record(tick: int, history: _PerObjectHistory, snapshots: _HistoryBuffer, p if not snapshots.has_at(tick): snapshots.set_at(tick, snapshot) - for subject in property_pool.get_subjects(): + for subject in property_pool.get_subjects_raw(): assert(subject is Node, "Only nodes supported for now!") # Don't record history when subject is not alive to prevent state corruption. @@ -222,10 +222,12 @@ func _record(tick: int, history: _PerObjectHistory, snapshots: _HistoryBuffer, p _logger.warning("Dropping recorded tick @%d for subject %s as out-of-bounds", [tick, subject]) continue - assert(not property_pool.get_properties_of(subject).is_empty(), "Subject present in property pool without properties! Please report a bug!") - for property in property_pool.get_properties_of(subject): - subject_snapshot.record_property(property) - snapshot.record_property(subject, property) + var properties := property_pool.get_properties_of_raw(subject) + assert(not properties.is_empty(), "Subject present in property pool without properties! Please report a bug!") + for property in properties: + var value: Variant = subject.get_indexed(property) + subject_snapshot.set_value(property, value) + snapshot.set_property(subject, property, value) snapshot.set_auth(subject, is_auth) subject_snapshot.set_auth(is_auth) @@ -238,7 +240,7 @@ func _record(tick: int, history: _PerObjectHistory, snapshots: _HistoryBuffer, p func _restore_latest(tick: int, history: _PerObjectHistory) -> bool: var any_applied := false - for subject in history.subjects(): + for subject in history.subjects_raw(): # Grab latest snapshot up to tick var snapshot := history.get_latest_snapshot(tick, subject) diff --git a/test/netfox/servers/network-history-server-rollback.perf.gd b/test/netfox/servers/network-history-server-rollback.perf.gd new file mode 100644 index 00000000..a9659617 --- /dev/null +++ b/test/netfox/servers/network-history-server-rollback.perf.gd @@ -0,0 +1,53 @@ +extends VestTest + +# Perf coverage for rollback record/restore properties + +func get_suite_name() -> String: + return "NetworkHistoryServer (rollback state)" + +func suite(): + for count in [16, 256, 4096]: + test("%d nodes x 4 properties" % count, func(): + var nodes := await get_nodes(count) + for node in nodes: + NetworkHistoryServer.register_rollback_state(node, "position") + NetworkHistoryServer.register_rollback_state(node, "rotation") + NetworkHistoryServer.register_rollback_state(node, "velocity") + NetworkHistoryServer.register_rollback_state(node, "health") + + NetworkHistoryServer._record_rollback_state(0) + + benchmark("record()", func(__): + NetworkHistoryServer._record_rollback_state(0) + ).with_duration(1.).with_batch_size(16).run() + + benchmark("restore()", func(__): + NetworkHistoryServer._restore_rollback_state(0) + ).with_duration(1.).with_batch_size(16).run() + + for node in nodes: + NetworkHistoryServer.deregister(node) + free_nodes(nodes) + ) + +func get_nodes(count: int) -> Array[Node]: + var nodes := [] as Array[Node] + + for i in count: + var node := StateNode.new() + node.name = "Node %d" % i + nodes.append(node) + Vest.get_tree().root.add_child.call_deferred(node) + + for node in nodes: + await node.ready + + return nodes + +func free_nodes(nodes: Array[Node]) -> void: + for node in nodes: + node.queue_free() + +class StateNode extends Node3D: + var velocity := Vector3.ZERO + var health := 100