Various codebase questions

I’ve had a few random questions about the codebase that have been piling up, and I haven’t had a good chance to ask them so… I’m going to dump them all here and hopefully get at least some of them answered. :slight_smile:

  1. What do cached entity queries cache? Is it just the index of the component given its type?
    • When should they not be used?
    • Does it matter if they’re stored as a field on a system, or just as a local variable? Presumably the former is better, but I see a lot of cases where it’s just grabbed every single update loop and many others where it’s a field.
  2. Why don’t cached entity queries have stuff like EnsureComp or RemComp?
  3. Is there a difference between a query on XComp, Transform versus just querying for XComp and getting the transform via Transform(uid) inside the loop?
  4. When should you use MapInit versus ComponentInit versus ComponentStartup? (I’m especially curious about MapInit versus ComponentInit; I know ComponentStartup is where an entity’s other component’s have been been initialized. See also next question, though.)
  5. Why can component startup be called “multiple times during its life, and at any time”? Ditto for ComponentShutdown.
  6. Wawa?
  7. Is it possible to take and view serverside ProfManager snapshots?
  8. Why is this a sandbox violation?
  9. What does EUI stand for I’m begging you I cannot figure this out
  10. How deep should UI abstraction go? Concrete example: consider disposal tagged versus disposal router. Should these ultimately be the same xaml file, with a bit of logic on their shared xaml.cs state to change their behavior? Or should it be different xamls but the same BUI, which handles differentiating the behavior? Or should it be different BUIs, but well abstracted BUI message types that both BUIs use, with the differing logic handled by the BUI event listener?
  11. For integration tests, when should logic and assertions be versus not be inside your WaitAssert blocks? Why is it dangerous (in some/all cases?) to heavily mix threads if everything is awaited? (Specifically looking at this PR.)
  12. Not a question but I found Significant lock contention with parallel JIT compilation via expression trees · Issue #107197 · dotnet/runtime · GitHub and now I’m sad…

For some questions I’m not entirely sure myself about the technical aspects, so I’ll only answer some of these to make sure I’m not spreading false information here.

    1. What do you mean exactly? If you mean the GetEntityQuery variant of TryComp/HasComp that is mostly a performance improvement for cases where you need a lot of them. A normal TryComp is basically two dictionary lookups, whereas the query version is only one.
    1. If you mean cases like EntityQueryEnumerator<XComponent, TransformComponent> it’s often better for performance to keep both inside the query itself. There might be cases where getting the TransformComponent is only needed sometimes and it could be better for performance to do it outside the query. But I’ve recently seen forks blindly copy paste this pattern everywhere even in cases where it will be simply worse for performance.
      Also according to Sloth this will likely have much larger impact once we switch to Arch.
    1. MapInit is run if the map the entity is spawned on is initialized and after ComponentInit and ComponentStartup. So for example a when a mapper is editing a map it has not been initialized yet. This is mostly used for stuff like item spawners, which you want to be different each time you run the map. But also for entities you don’t need to be part of the map yml file. Solution entities are only spawned on map init. Container fills are only spawned on map init as well, this allows us to edit the contents of a closet prototype and the map will automatically have the new contents without us having to adjust every single mapped instance.
    1. Wawa!
    1. Found an old answer on discord
1 Like

Amazing—this helps a lot. To follow-up:

    1. Yeah, I’m specifically curious about what using a GetEntityQuery saves over TryComp/HasComp, and what cost using it has. That is, why don’t we have a generated query for every single component, or at least, conventions that basically always use queries? (Disregarding the fact that in terms of big picture performance I have to imagine it’s small potatoes one way or the other; I more mean in the hypothetical ideal environment.)
    1. Okay cool, that’s what I suspected!
    1. Great; that helps clear things up.
    1. Wa wawa!
    1. what the frick PJB (I’ve probably spent like thirty minutes trying to search for an answer to that.)
  1. I’m not too familiar with the technical details but Sloth mentioned before that the GetEntityQuery version of TryComp/HasComp is about twice as fast, so it is recommended whenever you need a lot of them. You could basically always use them, but at that point it’s microoptimization.
    Sloth has suggested adding a source generator that would automatically convert them before. But all that will change a lot with Arch.
    In general TryComps are somewhat expensive so they should not be used every single frame to check something when you can avoid it by instead using events only when something changes.

In the current implementation of the ECS, components are stored in a FrozenDictionary<Type, Dictionary<EntityUid, IComponent>>. In the simplest form, the entity queries just allow you to cache that first-level dictionary lookup.

In the future, if we ever got an archetype-based ECS, entity queries would store more data related to what archetypes intersect the query.

They’re quite complicated operations, the perf saving of entity queries isn’t relevant for these. Also they’re queries not anything else, I guess.

It used to be that components could be stopped and started individually and multiple times without actually removing them. I think that was removed at some point as part of de-OOP-ing the ECS, so it’s probably just outdated comments at this point.

Waaaaaaaaaaaaa

Yeah no way to view them right now AFAIK.

When you do it like this, the C# emits a bunch of unsafe code to stack-allocate the data before passing it to the FrozenSet<T> constructor. This is unsafe and therein lies the problem, really. You can see this pretty easily on SharpLab.

It’s opposed to BUIs (bound user interfaces) and I liked the way EUIs sounds. Yes it doesn’t stand for anything.

These probably shouldn’t share code at all. It’s true that right now they just act like “disposals machine with text field” but I think the UI could and should do more to distinguish the two and explain its own functioning, which would make sharing probably more hassle than it’s worth.

Instead of thinking “how can we make these two machines use the same BUI code”, the question should be “how can we cut down on the amount of shared boilerplate.”

The worst thing that should realistically be able to happen is some stray IoCManager. or Logger. call breaking everything, but IMO it’s just a hygiene thing: you’re interacting with the server’s stuff, run on the server’s thread. There’s also like no reason not to just put everything in a WaitAssertion block, so just do it.

I’m still not sure this is 100% a real thing or not but shrug. Realistically the proper fix to this issue is to remove the tens of thousands of expression trees we create with the serialization system.

2 Likes

That is amazingly helpful; no follow-up questions! :saluting_face: