Name: Persistence Ids
Summary: Get rid of chunk/player data that needs to be saved even if the chunk/player is unloaded
Scope: Engine
Current Goal: Getting rid of chunk/player data that needs to be saved even if the chunk/player is unloaded
Phase: Design
Curator: Florian
Related: Continous saving of the game
How it is currently done?
When a chunk or player gets saved, the game rembers in memory which external entity references the chunk/player is making.
This is done in a map from player/chunk id to a set of valid external references. See field storeMetadata in StorageManagerInternal.
When now a referenced entity gets destroyed, the game / main thread removes the id from all sets of valid external references.
Other entity may reuse the id, but since it remains absent from the set of valid references of all previously stored chunks/players.
When the entities of the chunk or player get restored, the game checks if external references are still in the in memory set of valid external references
which had been maintained even while the chunk was unloaded. For the valid external references it creates inactive EntityRefs.
See field validRefs in EntityRestorer which gets used via EntityRestorer#loadingRef by EntityRefTypeHandler#deserialize and EntityRefTypeHandler#deserializeCollection
Each time the game gets saved the external reference sets of all loaded and unloaded chunks/players get stored in a global store file.
At the start of a game, all external reference sets of all chunks and players get loaded in memory and stay there.
Thus, larger worlds:
- Load slower, as all external references of unloaded chunks and players need to be loaded too
- Need more memory, as all external references of unloaded chunks and players are kept in memory
- Have longer save breaks
Even if you have only a small part of the large world loaded, the performance impact is there never the less.
My suggestion:
Let have entities have a second id, with a so large number room so that it's id never needs to be reused. For example it could be a 64 bit long.
The second id gets only used for persistence, and will thus not impact the game performance otherwise.
This persistence id will be stored in the EntityRef, as there are not always components loaded when it is needed.
In addition the entity manager will maintain a map from persistence id to inactive EntityRef.
The map uses weak values so that it will automatically shrink when the inactive EntityRef gets no longer referenced
When the entity gets saved, only the persistence ids will be used to store relationships.
When a background task loads entities, it knows also only persistence ids.
When the entities get activated on the main thread, it checks if there is an inactive EntityRef for the given persitence id:
- If there is an inactive entity then it gets activated.
- If there is no inactive entity then a new entity with the given persitence id and a new classic ingame id will be created and it will be put in the map.
If a entity gets destroyed, it's entry from the persistence id to EntityMap map gets deleted
if an entity gets created, it will be given a new persitence id and it will be put in the persistence id map.
For this concept to work it is necessary that an active entity has always the same EntityRef.
While on the other hand it must be possible for inactive entities to be garbadge collected and removed from the persistence id map
when they are no longer in use. To ensure this I suggest that we add a map from entity id to active entity. All EntiyRefs get currently stored in a weak values map from id to EntityRef. By adding a second map for the active entties those will be able to survive garbadge colletions.
Example 1: A good case that shows how it is supposed to work:
Entity A refrences Entity B
The entities A and B get saved and unloaded.
The game garbadge collects the inactive EntityRefs of both A and B
Entity A gets activated/loaded: The main thread creates an active EntityRef for A and an inactive one for B and let A reference it.
Entity B gets activated/loaded: The main thread activates the EntityRef of B and "fills it with data"
Example 2: The simple case that also works:
Entity A refrences Entity B
The entities A and B get saved and unloaded.
The game garbadge collects the inactive EntityRefs of both A and B
Entity B gets activated/loaded: The main thread creates an active EntityRef for B and "fills it with data"
Entity A gets activated/loaded: The main thread creates an active EntityRef for A and "fills it with data"
Example 3: A case where no garbadge collection happens:
The entities A and B get saved and unloaded.
Entity A refrences Entity B
Entity B gets unloaded and saved
No garbadge collection happens
Entity B gets activated/loaded: The main thread activates the EntityRef of B and "fills it with data"
Example 4: A not so ideal case:
Entity A references Entity B
Entity A gets saved and unloaded.
Entity B gets destroyed
When Entity A loads it creates an inactive Entity B which will never become active.
Code can handle inactive entities not much different from destroyed entities, as both have no components.
So I think we are fine in the fourth example too.
With this approach no external reference sets needs to be stored and thus unloaded players and chunks have no performance impact. In addition with this approach it is no longer necesssary to store a free id list. Instead the entities can be numerated from 0 again after a load of an existing world. Thus as a small bonus, entity numbers are kept with this approach small and readable.
This approach does not fully solve the issue, that inactive entity refs do not free their id when they get garbadge collected. However it would be possible to have a background task check for free ids if that turns out to be an issue. Or we could let have inactive entities have only a persistence id. They have no components anyway till they are loaded. Although that might require further changes to other code that relies on the entity to have an int as id.
Do you agree to this suggestion or do you have another idea to solve it better?
Summary: Get rid of chunk/player data that needs to be saved even if the chunk/player is unloaded
Scope: Engine
Current Goal: Getting rid of chunk/player data that needs to be saved even if the chunk/player is unloaded
Phase: Design
Curator: Florian
Related: Continous saving of the game
How it is currently done?
When a chunk or player gets saved, the game rembers in memory which external entity references the chunk/player is making.
This is done in a map from player/chunk id to a set of valid external references. See field storeMetadata in StorageManagerInternal.
When now a referenced entity gets destroyed, the game / main thread removes the id from all sets of valid external references.
Other entity may reuse the id, but since it remains absent from the set of valid references of all previously stored chunks/players.
When the entities of the chunk or player get restored, the game checks if external references are still in the in memory set of valid external references
which had been maintained even while the chunk was unloaded. For the valid external references it creates inactive EntityRefs.
See field validRefs in EntityRestorer which gets used via EntityRestorer#loadingRef by EntityRefTypeHandler#deserialize and EntityRefTypeHandler#deserializeCollection
Each time the game gets saved the external reference sets of all loaded and unloaded chunks/players get stored in a global store file.
At the start of a game, all external reference sets of all chunks and players get loaded in memory and stay there.
Thus, larger worlds:
- Load slower, as all external references of unloaded chunks and players need to be loaded too
- Need more memory, as all external references of unloaded chunks and players are kept in memory
- Have longer save breaks
Even if you have only a small part of the large world loaded, the performance impact is there never the less.
My suggestion:
Let have entities have a second id, with a so large number room so that it's id never needs to be reused. For example it could be a 64 bit long.
The second id gets only used for persistence, and will thus not impact the game performance otherwise.
This persistence id will be stored in the EntityRef, as there are not always components loaded when it is needed.
In addition the entity manager will maintain a map from persistence id to inactive EntityRef.
The map uses weak values so that it will automatically shrink when the inactive EntityRef gets no longer referenced
When the entity gets saved, only the persistence ids will be used to store relationships.
When a background task loads entities, it knows also only persistence ids.
When the entities get activated on the main thread, it checks if there is an inactive EntityRef for the given persitence id:
- If there is an inactive entity then it gets activated.
- If there is no inactive entity then a new entity with the given persitence id and a new classic ingame id will be created and it will be put in the map.
If a entity gets destroyed, it's entry from the persistence id to EntityMap map gets deleted
if an entity gets created, it will be given a new persitence id and it will be put in the persistence id map.
For this concept to work it is necessary that an active entity has always the same EntityRef.
While on the other hand it must be possible for inactive entities to be garbadge collected and removed from the persistence id map
when they are no longer in use. To ensure this I suggest that we add a map from entity id to active entity. All EntiyRefs get currently stored in a weak values map from id to EntityRef. By adding a second map for the active entties those will be able to survive garbadge colletions.
Example 1: A good case that shows how it is supposed to work:
Entity A refrences Entity B
The entities A and B get saved and unloaded.
The game garbadge collects the inactive EntityRefs of both A and B
Entity A gets activated/loaded: The main thread creates an active EntityRef for A and an inactive one for B and let A reference it.
Entity B gets activated/loaded: The main thread activates the EntityRef of B and "fills it with data"
Example 2: The simple case that also works:
Entity A refrences Entity B
The entities A and B get saved and unloaded.
The game garbadge collects the inactive EntityRefs of both A and B
Entity B gets activated/loaded: The main thread creates an active EntityRef for B and "fills it with data"
Entity A gets activated/loaded: The main thread creates an active EntityRef for A and "fills it with data"
Example 3: A case where no garbadge collection happens:
The entities A and B get saved and unloaded.
Entity A refrences Entity B
Entity B gets unloaded and saved
No garbadge collection happens
Entity B gets activated/loaded: The main thread activates the EntityRef of B and "fills it with data"
Example 4: A not so ideal case:
Entity A references Entity B
Entity A gets saved and unloaded.
Entity B gets destroyed
When Entity A loads it creates an inactive Entity B which will never become active.
Code can handle inactive entities not much different from destroyed entities, as both have no components.
So I think we are fine in the fourth example too.
With this approach no external reference sets needs to be stored and thus unloaded players and chunks have no performance impact. In addition with this approach it is no longer necesssary to store a free id list. Instead the entities can be numerated from 0 again after a load of an existing world. Thus as a small bonus, entity numbers are kept with this approach small and readable.
This approach does not fully solve the issue, that inactive entity refs do not free their id when they get garbadge collected. However it would be possible to have a background task check for free ids if that turns out to be an issue. Or we could let have inactive entities have only a persistence id. They have no components anyway till they are loaded. Although that might require further changes to other code that relies on the entity to have an int as id.
Do you agree to this suggestion or do you have another idea to solve it better?