Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Queue approach] Fix entity destruction in unticking chunks #2986

Merged
merged 1 commit into from Feb 19, 2016
Merged

[Queue approach] Fix entity destruction in unticking chunks #2986

merged 1 commit into from Feb 19, 2016

Conversation

SafwatHalaby
Copy link
Member

Closes #2731
Closes #2973

@tigerw Is that the vision you had in mind?

@SafwatHalaby
Copy link
Member Author

I had to have a single new entity variable: m_ParentChunk. Because the parentChunk is needed for efficient queueing.

@SafwatHalaby
Copy link
Member Author

This can be used as a basis for a problem @madmaxoft once mentioned: Entities may tick twice when moving between two chunks. But I want the basic thing to be merged first.

@@ -129,7 +129,6 @@ cClientHandle::~cClientHandle()
World->RemovePlayer(m_Player, true); // Must be called before cPlayer::Destroy() as otherwise cChunk tries to delete the player, and then we do it again
m_Player->Destroy();
}
delete m_Player;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You sure about this? In the current system, the ClientHandle manages the player pointer's life.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, no, I am not sure. m_Player->Destroy(); will add the player destruction to the queue. The problem I can see is that m_Player->Destroy(); is inside a conditional "if" so it's not guaranteed to free it. Guess I'll change this and make the player destruct here as usual.

@tigerw
Copy link
Member

tigerw commented Feb 7, 2016

Could you not use DoWithChunk() as you have done before, without storing a parent chunk?

bool m_FreeMemory;
};

std::list<EntityRemovalQueueItem> m_EntityRemovalQueue;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cWorld already has a task system, I was thinking of something like:

cWorld::QueueTask([...](...)
{
    DoWithChunk(..., ..., [...](...)
    {
        a_Chunk->RemoveEntity(); // New function to be added; delete if not player
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the task guaranteed to be executed just after the mob tick and before the next tick cycle?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just checked. The tasks are done before the mobs are ticked.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, mobs are ticked separately (ugh). In that case, move the TickMobs call to after m_ChunkMap->Tick, and now its guaranteed :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TickMobs is already after m_ChunkMap->Tick. How does that help?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in directly afterwards/before TickQueuedTasks.

@SafwatHalaby
Copy link
Member Author

Could you not use DoWithChunk()

@tigerw That technique was dangerous. It returns the chunk the player is currently standing at. At the start of each tick this is exactly the same as the parent chunk, but this will change if the entity crosses a border mid-tick, (and will re-converge to the same chunk when cChunk::tick calls MoveEntityToNewChunk.) So, if a player teleports while walking, it may do nasty things.

The only thing guaranteed to do it right is the parent chunk (the curretly owning chunk). I am actually considering removing the a_Chunk parameter in the cEntity::tick method and its overrides, as this is always equal to m_ParentChunk. Note that using a_Chunk directly is impossible, because sometimes teleportation /destruction is called outside of a tick and therefore a_Chunk is not available.

@SafwatHalaby
Copy link
Member Author

Hmm, There's a chance I'm wrong in my last post. I'll need to re-check this.

@SafwatHalaby
Copy link
Member Author

Not wrong.

@SafwatHalaby
Copy link
Member Author

Also, regardless of my last argument, I think it'd be wise to have the chunk stored for convenience, rather than passing around a_Chunk all over the place.

@tigerw
Copy link
Member

tigerw commented Feb 7, 2016

Not wrong.

Are you certain? GetChunkX/Z() returns the current chunk based on the entity's position, DoWithChunk gets that chunk. The task will execute after Tick (after our fix); and Tick will have ensured that the entity is in the chunk that its position says it is in.

@tigerw
Copy link
Member

tigerw commented Feb 7, 2016

(this is all so confusing, I've already forgotten why this whole thing is needed about four times, and had to re-examine your explanation of it)

@SafwatHalaby
Copy link
Member Author

Are you certain? GetChunkX/Z() returns the current chunk based on the entity's position, DoWithChunk gets that chunk. The task will execute after Tick (after our fix); and Tick will have ensured that the entity is in the chunk that its position says it is in.

Oh, yes, you're correct, I forgot that this will happen after the tick now that there's a fix. Confusing indeed!

But my guts tell me this is dangerous futurwise. It's the Cuberite equavilant of tinkering with undefined compiler behavior. Any future change may break this guarantee. m_ParentChunk seems a lot safer to me.

@tigerw
Copy link
Member

tigerw commented Feb 7, 2016

But then again, some future change may accidentally try to update ParentChunk instead of comparing it or some future chunk refactor may completely miss the fact that there's an additional variable which needs maintenance.

I personally would do the "as little amount of variables as possible" route but I think both have pitfalls, so you decide.

@SafwatHalaby
Copy link
Member Author

Well, I've explicitly mentioned in the docs that only cChunk::addEntity has the right to update it.
I can't deny there are still dangers, but I think the dangers of your proposals are more real. For instance, the code that moves entities to neighboring chunks might one day be offloaded to the world task system to solve @madmaxoft 's double-tick issue. This will instantly break the following guarantee: and Tick will have ensured that the entity is in the chunk that its position says it is in., there are probably a million more ways this could break when doing seemingly unrelated changes. On the other hand, the only way m_ParentChunk could only break if someone sets it explicitly which is a much more verbose, much less sneaky error.

@tigerw
Copy link
Member

tigerw commented Feb 7, 2016

Very well, go for it then :)

@SafwatHalaby
Copy link
Member Author

One more question first: Should I really go through the cWorld's existing task system? How will it fare performance-wise? Perhaps it'd be wise to have a second, entity-specific task system instead? Entities will produce a lot of tasks if chunk switching is ever made a task, and maybe it'd be significantly faster without those capture lambdas? Just wondering.

@SafwatHalaby
Copy link
Member Author

My guess is that the current roll-your-own entity destructor queue is faster than the cWorld task system. I don't have any real numbers though.

@tigerw
Copy link
Member

tigerw commented Feb 7, 2016

Dunno. Maybe, maybe not, maybe the compiler does magic to it, @worktycho knows. Everything else is using QueueTask though - imagine what it would be like if they all added their own queue structures to the world! Entities shouldn't be an exception.

@SafwatHalaby
Copy link
Member Author

Done conversion to cWorld's tasks. Rebased and squashed. I like this.

@@ -139,6 +139,7 @@ cChunk::~cChunk()
{
if (!(*itr)->IsPlayer())
{
(*itr)->SetParentChunk(nullptr); // To prevent the entity from queueing a future chunk removal.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change comment

@SafwatHalaby
Copy link
Member Author

Doubletick prevention seems to be working :)

@SafwatHalaby
Copy link
Member Author

I got a "moving to same chunk" error. I guess I'll leave the doubletick thing to a seperate PR ^_^

@SafwatHalaby
Copy link
Member Author

When you move an entity to a new chunk which hasn't yet ticked, that entity will tick twice. The solution is moving it post-tick, preventing such doubleticks. Hence the cChunk::tick task queue. But it seems to be messing something.

@SafwatHalaby
Copy link
Member Author

Rebased and removed doubletick prevention attempt.

LOGD("Warping player (%s) from world (%s) to (%s). Source chunk: (%d, %d) ",
this->GetName().c_str(),
a_OldWorld.GetName().c_str(), a_World->GetName().c_str(),
ParentChunk->GetPosX(), ParentChunk->GetPosZ()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is markDirty required for players?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better call it, so that it's consistent with the other entities.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I see if entity is not a player then call markDirty everywhere.

@SafwatHalaby
Copy link
Member Author

I realized this commit name is misleading. It's not about unloaded chunks, it's about loaded, nonticking chunks. There's no trivial way to access entities in unloaded chunks. (You'd need to load and check all unloaded chunks from storage).

@SafwatHalaby
Copy link
Member Author

@sphinxc0re done.

@SafwatHalaby
Copy link
Member Author

Please do not merge yet, I want to be completely sure I got all pointers right in all cases first.

@SafwatHalaby SafwatHalaby added this to the Stable world travel milestone Feb 11, 2016
// Mark as dirty if it was a server-generated entity:
if (!(*itr)->IsPlayer())
{
MarkDirty();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this line will save everyone thousands of false dirties a day. It seems that previously, anywhere you walk into would be marked dirty because the if player condition was missing.

@SafwatHalaby
Copy link
Member Author

I have replaced IsDestroyed with IsTicking. IsDestroyed returns true only when an entity is about to be deleted. !IsTicking() returns true if the entity is either about to teleport or about to be deleted.

@SafwatHalaby
Copy link
Member Author

I put all the player destruction logic in cClientHandle::~cClientHandle:. Previously, it was dispersed into 3 functions for no apparent reason: cClientHandle::Destroy(), cClientHandle::ClosedSocket(), cClientHandle::~cClientHandle(). Was there a reason for that? Did I miss anything here?

void Destroy(bool a_ShouldBroadcast = true);

/** Destroy the entity without scheduling memory freeing. This should only be used by cChunk for internal memory management. */
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by cChunk and cClientHandler

@SafwatHalaby
Copy link
Member Author

What is wrong with the CI?

@sphinxc0re
Copy link
Contributor

Nothing. GCC is throwing so many errors, that it stops counting and aborts

@SafwatHalaby
Copy link
Member Author

It is throwing errors in unchanged code.

if (m_Player != nullptr)
{
cWorld * World = m_Player->GetWorld();
ASSERT(World != nullptr);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there really be an assert here?

@tigerw
Copy link
Member

tigerw commented Feb 13, 2016

great, IsTicking came back...

@SafwatHalaby
Copy link
Member Author

@tigerw IsDestroyed must be replaced. We need something which returns true for either destruction or teleportation, not just destruction.

@tigerw
Copy link
Member

tigerw commented Feb 13, 2016

Could you explain a bit more the problem? Why does anything need to distinguish between these states?

@SafwatHalaby
Copy link
Member Author

@tigerw Okay.

Why does anything need to distinguish between these states?

Actually, we do NOT need to distinguish between teleportation and destruction, and this is why I made this change.

For instance, cChunk previously had this:

for each entity
  if not IsDestroyed() then tick

The problem here is that teleporting entities will get ticked. So I replaced it with

for each entity
  if IsTicking() then tick

IsTicking() plays the role of the previous !IsDestroyed(), but now it returns a false for teleporting entities too, not just dying ones. In other words, I eliminated distinguishing between the two states.

/** Returns true if the entity is valid and ticking. Returns false if the entity is not ticking.
When the entity is not ticking, it is either scheduled for teleportation or for destruction.
Either way, using it is dangerous. */
bool IsTicking(void) const;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed IsDestroyed. How do I deal with the LUA API here?

Perhaps I should export IsTicking, and make IsDestroyed a deprecated alias for !IsTicking()?.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...don't bother...

(don't tell anyone I said that)

@SafwatHalaby
Copy link
Member Author

Todo: Document the new API functionIsTicking, mark IsDestroyed as deprecated, and that's it, I guess.

@SafwatHalaby
Copy link
Member Author

Done.

@tigerw
Copy link
Member

tigerw commented Feb 18, 2016

Looks good.

I shall, of course, take every available opportunity to tease you about ParentChunk :D

@SafwatHalaby
Copy link
Member Author

@tigerw is evil!

SafwatHalaby pushed a commit that referenced this pull request Feb 19, 2016
[Queue approach] Fix entity destruction in unticking chunks
@SafwatHalaby SafwatHalaby merged commit a776337 into cuberite:master Feb 19, 2016
@SafwatHalaby SafwatHalaby deleted the entityDestroy_approach4 branch March 23, 2016 08:45
@lkolbly lkolbly mentioned this pull request Sep 5, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants