HN user

teh_cmc

215 karma

https://github.com/teh-cmc

Posts9
Comments11
View on HN

Revy already works with snapshot deltas (see other comments scattered around this section for more details, but basically we only sync components that changed during the previous frame -- Rerun stitches everything back together at runtime)... but at 21k entities, I'm afraid you'll be facing much bigger issues on the Rerun-side of things :D

Rerun was originally designed for few (i.e. dozens up to hundreds) massive entities (e.g. it's common for a single entity to have a few million 3D points and color values attached to it).

While we're slowly working towards improving the many-entities use-case, the correct thing to do in this case would probably be for Revy to identify that all these entities are really just different instances of the same batch (either automatically, or by exposing a marker component or something).

So, say, you'd set a marker component on all your tiles, Revy would then snapshot them as a single batch of 144^2 instances, and then in Rerun you'd see a single entity `/tiles` which would be a batch of 144^2 instances (each with their own set of components, that's fine!). From Rerun's point-of-view, this would be similar to a point cloud, and at 21k instances you'd be easily running at your monitor refresh rate with a lot of margin.

But by any means, try it! Not the web version though, you're definitely going to need multithreading :D

Nice project btw; I'll keep an eye on it and probably use it as a benchmark for the many-entities use-case!

Oh for sure, there's a lot of overlap between traditional relational databases and ECS designs. As always, in the end the hard part is to match the performance requirements.

If you squint enough, most ECS out there are pretty much very specialized relational databases that trade off flexibility in favor of performance for common gamedev use cases (very wide joins, very deep hierarchies (e.g. transform trees), full-table filters, etc).

Rerun's ECS goes one step further and makes time a first-class citizen, allowing for efficient joins across different components across different timestamps.

This is what makes it possible to only log diffs in Revy (we only snapshot the components that were modified during the last frame), rather than having to full snapshots every frame, which would be prohibitively expensive (both time and space). Rerun then stitches back everything together during visualization, in real-time!

Whether the physics engine is deterministic or not doesn't matter here -- Revy (and more importantly, Rerun) doesn't replay anything: it just stores state, every single frame, and then visualizes that state at every timestamp available.

Check out e.g. the live demo of the breakout example for example [1]: if you click on the pallet and then go to its parent node, you'll see that we just store that node's final transform (i.e. post-physics) every frame.

Happy gamedev!

[1] https://app.rerun.io/version/0.14.1/index.html?url=https://s...

Revy is frame-based: it runs as the last system at the end of the frame, with exclusive access to the `World`, and synchronizes the state of the Bevy database with the state of the Rerun database at that point in time (it keeps track of 3 timelines during that process: the wall-clock time given by the OS, and the frame number and simulation time given by Bevy itself).

So non-deterministic scheduling is just not an issue by default.

You could of course access the Revy logger from any system (it's just a `Resource` after all) and log arbitrary data to Rerun from there (the resource is basically a handle to the Rerun SDK). This still wouldn't be a problem. The data would once again be logged to the 3 same timelines (wall-clock, frame number and sim_time) and you would be able to visualize in which order the different systems doing the logging were scheduled during each frame.

As mentioned in the README, Revy is not meant to be a polished / properly maintained project -- it's just a proof-of-concept. I've talked more about how and why it came to exist in the first place in this thread [1], if you're interested.

That being said, I do intend to publish updates when new versions of either Rerun or Bevy land; if only to experiment with new APIs as they come online.

Now, to answer your question, I've been using Bevy since the 0.1 release and, in my experience, keeping up with the changes upstream has always been pretty painless. Their organization nand release process is top-notch, with some of the most high quality changelogs and migration guides I've ever seen in any project, and releases are rare enough (~about once a quarter) to just not be an issue.

The community maintains compatibility matrices such as this one [2], and things generally just work :tm:.

[1] https://www.reddit.com/r/rust/comments/1b6bqv1/revy_proofofc...

[2] https://github.com/rerun-io/revy?tab=readme-ov-file#compatib...

Yes, these benchmarks use forced GC calls (i.e. all phases are STW) because it's the only (good) way I can think of to make theses tests deterministic (maybe you know of a better way? I'd definitely be interested).

Of course, I don't have such calls on production systems; and while concurrent collections greatly reduce the time spent in STW phases, latency spikes are still a very real issue, as I explained here [1]. The monitoring on my production systems leaves absolutly no doubt that those latency spikes are due to garbage collections: once the GC was out of the picture, every thing was flat and fast again.

[1] https://news.ycombinator.com/item?id=10650885

You're certainly right that it's a possible and viable alternative. The reason I didn't go that way is quite simple: I always try and do my best to avoid external dependencies. One of the reason I really love Go is its ease of deployment thanks to its lack of dependencies: I love the idea of being just one scp away from running in staging/production; no more, no less.

I know many people won't agree with that, and there are definitely good reasons not to; and still, as far as I'm concerned, minimizing the complexity of my software stack means there's one less thing that I'll have to worry about, and at the end of the day, that is really quite the upside.

Yes, as I said in this comment [1]; my real issue here is lengthy GC pauses, which cause peaks in response times.

Regarding overall performances, I'm not really convinced that the reflect calls affect them that much (there's 2 reflect calls per Read() or Write() call), but I honestly just don't know, I'd have to bench to be sure. Although, if it turns out to actually be an issue, you can still use Pointer() to keep references to your unmanaged heap and you'll be able to work with your data without GC nor reflection overhead.

[1] https://news.ycombinator.com/item?id=10650885

I couldn't have said it better. This is almost word for word why I decided to build mmm: I maintain a few high-load RPC services in Go, each of which stores millions, if not tens of millions of items in their cache.

In this configuration, each incoming request means allocations inside Go's RPC package [1], which in turn means that a GC pass will be triggered if GC_PERCENT [2] has been reached, which in turn means that the GC will have to scan all of those long lived pointers (Go's GC is not generational), which in turn means a huge peak in response time.

This basically leaves me with three possible solutions:

- hack into Go's RPC package to minimize allocations, which is a huge price to pay just to delay the inevitable

- build my caches in a language that offers manual memory management, then query those via RPC from my Go services; but I don't want to add a new language into the mix

- provide a generic solution for manual memory management in Go, which is where we are now

[1] https://golang.org/pkg/net/rpc/

[2] https://golang.org/pkg/runtime/debug/#SetGCPercent