我正在制作一个第一人称模拟游戏,其中你可以建造和运营一个云提供商:rack硬件、连接网络、保持客户在线并随着规模的增长。它在屏幕上显示大量的实时状态,这就是这个项目的起源。

这个模拟是一个无头Rust引擎,独立于自己的进程;渲染器(Godot)是单独的,并不拥有游戏状态,所以每次引擎都会向渲染器发送世界的视图以绘制,通过IPC。对于测试模拟无头很好,但这意味着可见的世界每次都会跨越一个进程边界,而我想要的规模下,天真地全量快照每秒会达到数百GB。

显而易见的一半显而易见:发送一个为渲染器准备的视图,而不是原始引擎状态的快照,仅仅是自上一次tick以来发生的变化。然后,字节大部分停止成为问题。

实际上花费时间的是wire格式。 我最初使用msgpack,因为它很快设置,易于检查。一旦我有了实际的每次tick的载荷来测量,我就不满意大小,所以我对capnp进行了基准测试,capnp比我更快。

但是,Godot不支持capnp,解码它在GDScript中会抛弃我选择的原因,所以这变成了手动编写一个Rust GDExtension,客户端以原生的方式读取消息。这最终花费了比格式选择本身更多的时间:格式是测量,绑定是实际构建。

我很好奇其他人在高频率引擎↔渲染器IPC时会选择什么,特别是那些在Godot下运行Rust核心的人。您是否选择了Godot本身处理的东西,吃了成本,还是写了自己的GDExtension?

注意,这仅仅是100k主机的性能测试,不显示实际流量/模拟数据,因此快照大小很低。

cargo test --release -p sim-render --test scale_snapshot_perf snapshot_scale_1m -- --ignored --nocapture --test-threads=1

  Finished `release` profile [optimized] target(s) in 0.06s
  Running tests/scale_snapshot_perf.rs (target/release/deps/scale_snapshot_perf-73ba02726368dd0f)

running 1 test
test snapshot_scale_1m ... 
=== Snapshot scale: 25000 switches, 1000000 hosts, ~1000001 cables ===
  hosts=1000000, switches=25000, cables=1000000, ports=2200000
  build_snapshot ×10: 12.330956125s (1233.10 ms / snap)
  serialize to JSON:    1.239139875s (1673.73 MB)
  serialize to msgpack: 662.526125ms (1336.06 MB, 80% of JSON)
  delta build (steady): 2.458µs / 20 = 0.1 µs/delta
  delta build (idle):   6.792µs
  delta JSON (idle):    15µs (0.001 MB, 0.00% of full)
  delta msgpack (idle): 20.375µs (0.001 MB, 0.00% of full)
  delta capnp (idle):   13.875µs (0.001 MB, 0.00% of full)
  dirty delta (2000 hosts) JSON:    1.420583ms (1.859 MB)
  dirty delta (2000 hosts) msgpack: 693.167µs (1.528 MB, 82% of JSON)
  dirty delta (2000 hosts) capnp:   301.208µs (0.929 MB, 50% of JSON)
ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 17.93s