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

Update active items periodically instead of constantly. #10599

Merged
merged 13 commits into from Dec 23, 2014

Conversation

Projects
None yet
6 participants
@kevingranade
Copy link
Member

commented Dec 22, 2014

The short version is this change lets the game spend up to 600 turns (1 hour) between rot checks for food, and up to 100 turns (10 minutes) between raise checks for zombies and similar.
There's also a fix in there for a bug where every comestible was being set to active, this pretty much wrecked my attempts to make this work properly, because all food everywhere was being added to the lists for processing, and worse, many of them were being added to the default "check every turn" list.

The longer version is that I split the active item list into a potentially arbitrary number of sub-lists, each of which guarantees only that the items on it will be processed at least once every n turns. In reality we're only using three at the moment (with ns of 1, 100 and 600)
It does this by building a list of item references from its lists that contains the first list.size() / n items rounded up and returning that list for processing. When being processed, the items are removed from the lists and then pushed onto the back of the list, so after processing the items that were at the start of each list are now at the end of each list.

Another optimization is item_stack::push_back_fast(), if we know an item can and will fit in a square, such as if we just removed it while processing it, we can skip the checks imposed by map::add_item_or_charges and go straight to map::add_item. The check for inserting items into vehicles isn't as expensive, so I didn't bother with it for now.

Fixes #9926 and should fix #10269

struct list_iterator_hash
{
size_t operator()(const std::list<item>::iterator &i) const {
return (size_t)&*i;

This comment has been minimized.

Copy link
@BevapDin

BevapDin Dec 22, 2014

Contributor

This might be problematic on invalid iterators (maybe only with debug iterators). I know that the logic here does not actually read/write the invalid item, so it might be fine, but have you considered using plain old pointers?

Also, I thing for safety, the map functions like add_item and i_rem etc. should just forward to the submap and not access submap::itm directly. That way one can ensure that every change of the item list is correctly

This comment has been minimized.

Copy link
@kevingranade

kevingranade Dec 22, 2014

Author Member

I'm not sure what you mean by use a pointer, my guess is you mean make active_item_set a std::unordered_set<item *> and insert into it with active_item_set.insert( &*it ).
While that would work, I'm not sure what the benefit would be. Let me know if I'm missing something.

By forwarding to the submap, I assume you mean adding setters/getters etc to the submap and making the actual container private? That's the kind of thing I'm generally working toward, but I'm running out of time right now so I don't want to embark on another set of API changes in this PR unless it's totally necessary.

This comment has been minimized.

Copy link
@BevapDin

BevapDin Dec 22, 2014

Contributor

my guess is you mean make active_item_set a std::unordered_set<item *>

Yes.

While that would work, I'm not sure what the benefit would be

I'm pretty sure the standard forbids dereferencing invalid iterators, so the std iterators could have a check in the them which triggers an assertion or similar.

Judging from the latest crash report http://smf.cataclysmdda.com/index.php?topic=8998.0 especially the "attempt to copy-construct an iterator from a singular iterator" indicates this.

This comment has been minimized.

Copy link
@kevingranade

kevingranade Dec 22, 2014

Author Member

If we inserted an invalid pointer into the container, it might avoid the assert, but will lead to undefined behavior if the pointer is later dereferenced. We must store the iterator in active_items anyway, right?

This comment has been minimized.

Copy link
@BevapDin

BevapDin Dec 23, 2014

Contributor

But the pointer isn't dereferenced. The pointer is stored, but before dereferencing it, has_active_item is called and looks for the item pointer (only pointer comparison) in the active item set, it does not find it and the processing of that item is skipped.

But there was a discussion on stackoverflow that reading a deleted pointer (not the target, just the pointer itself) invokes undefined behavior http://stackoverflow.com/questions/4990462

This comment has been minimized.

Copy link
@kevingranade

kevingranade Dec 23, 2014

Author Member

Yea, on balance I think it's better to just use the iterator and ensure that it is always removed from active_item_cache before we remove the item, pointer or iterator that's the only safe thing to do.
There might be something fancy we could do with linked smart pointers or something, but the requirement that we be able to move the object around to different containers with potentially different semantics (player/monster/NPC inventory, possibly things I'm not thinking of) makes me skeptical that we can beat schlepping around the iterators. If you have something I'm all ears though.

This comment has been minimized.

Copy link
@BevapDin

BevapDin Dec 23, 2014

Contributor

You will end up with an invalid iterator anyway in map::process_items_in_submap, after the active_items (which contains iterators) is copied.

Any of the iterators in the copied set can become invalid when the matching item is removed from the map before it's actually processed. That's the point where has_active_item would return false. Invoking that function invokes a lookup in the active item set which invokes hashing of the iterator which invokes dereferencing it.

This comment has been minimized.

Copy link
@kevingranade

kevingranade Dec 23, 2014

Author Member

Ok, you are correct. The set needs to store raw pointers, item_reference needs a (hidden) raw pointer, and we use that to check validity of an item_reference before we dereference it in any way.
Thanks for showing me the error of my ways ;)

@@ -4371,7 +4358,7 @@ std::list<item>::iterator vehicle::remove_item( int part, std::list<item>::itera
std::list<item>& veh_items = parts[part].items;

if( it->needs_processing() ) {

This comment has been minimized.

Copy link
@BevapDin

BevapDin Dec 22, 2014

Contributor

I believe this is the reason for the recent crashes. You usually remove an item when it has stopped being active. But in that case the item is not removed from the active item set, it stays there and is processed again in the next turn. Edit: Nope, it isn't.

This comment has been minimized.

Copy link
@kevingranade

kevingranade Dec 22, 2014

Author Member

Could be, makes more sense to check if the iterator is in the container rather than if it should be in the list. There might be somewhere flipping the active flag that I didn't notice.

@@ -11465,7 +11464,8 @@ void map::rotate(int turns)
std::swap( cosmetics_rot[old_x][old_y], new_sm->cosmetics[new_lx][new_ly] );
auto items = i_at(new_x, new_y);
itrot[old_x][old_y].reserve( items.size() );
std::move( items.begin(), items.end(), std::back_inserter(itrot[old_x][old_y]) );

This comment has been minimized.

Copy link
@kevingranade

kevingranade Dec 23, 2014

Author Member

This move invalidated the iterators in the active item cache, then the i_clear would crash trying to remove them.
A copy followed by a i_clear() should preserve the expected behavior.

@KA101

This comment has been minimized.

Copy link
Contributor

commented Dec 23, 2014

Anything else?

@macrosblackd

This comment has been minimized.

Copy link
Contributor

commented Dec 23, 2014

Seems to fix the crashes and freezes I was experiencing.

@DavidKeaton

This comment has been minimized.

Copy link
Contributor

commented Dec 23, 2014

Would using std::stack and std::shared_ptr help with a stack infrastructure and invalid pointers?

Use a raw pointer to identify an item_reference.
Used when we need to determine whether an item_reference has been
invalidated by item processing side effects, it's safe to compare
invalidated pointers to each other, iterators not so much.
@kevingranade

This comment has been minimized.

Copy link
Member Author

commented Dec 23, 2014

I'm not sure if that's going to make things any simpler, and would involve I think a sizeable overhaul of the map and vehicle code to wedge shared_ptr instances in all over the place instead of items simply living in lists. I'd love it if you could take a weak_ptr to an object owned by a container, but that's not supported, the owner has to be a shared_ptr.
AFAIK the outstanding issues with this have been resolved, and I'm fairly happy with how simple it is.

@KA101 KA101 self-assigned this Dec 23, 2014

@DavidKeaton

This comment has been minimized.

Copy link
Contributor

commented Dec 23, 2014

Ah fair enough my man. I think auto_ptr makes life easy on ownership, so long as an instance of it lives.

KA101 added a commit that referenced this pull request Dec 23, 2014

Merge pull request #10599 from kevingranade/i_at-overhaul
Update active items periodically instead of constantly.

@KA101 KA101 merged commit 0289312 into CleverRaven:master Dec 23, 2014

1 check passed

default This has been rescheduled for testing as the 'master' branch has been updated.

// get() only returns the first size() / processing_speed() elements of each list, rounded up.
// It relies on the processing logic to remove and reinsert the items to they
// move to the back of their respective lists (or to new lists).

This comment has been minimized.

Copy link
@BevapDin

BevapDin Dec 25, 2014

Contributor

Now that I read this, that behavior will mess up the index of the items on the field. The zombie corpse (active) is at index 0, but once it's processed, it will be at the end of the item list, having the wrong index.

This comment has been minimized.

Copy link
@macrosblackd

macrosblackd Dec 25, 2014

Contributor

That might be what causes tiles with active items to 'rotate' the topmost item.

This comment has been minimized.

Copy link
@KA101

KA101 Dec 26, 2014

Contributor

I still have a revert button on this. Might pull the plug and see if that changes anything.

@DavidKeaton

This comment has been minimized.

Copy link
Contributor

commented Dec 26, 2014

Perhaps this is that segfault bug some of us constantly get?

@kevingranade

This comment has been minimized.

Copy link
Member Author

commented Dec 26, 2014

Are the crashes happening while you're crafting or doing other long actions
that interact with items? If not it's unlikely the cause of the segfault.
That having been said, changing the index will cause issues with crafting
and such, so we need to figure out something to address it. Unfortunately
there is no solution in the general case due to potential for explosions
destroying items, but in that case I'm guessing invalidating crafting isn't
really a problem.
Hmm, I might actually have a decent solution, instead of popping the active
item off the list, we can just make a copy of it, and then overwrite it
with the copy if it's still around after processing, and remove it if it's
not and it destroys itself. That will render the list stable, and possibly
more perform ant.
Please don't revert unless we decide the entire approach is unworkable,
it's going to be a huge huge mess if you revert this set of changes, and it
gets worse the further back you go.

@KA101

This comment has been minimized.

Copy link
Contributor

commented Dec 26, 2014

Crashes are when active items (re)enter the reality bubble. We've had active foodstuffs disappear when losing (fresh), etc tags too.

@kevingranade

This comment has been minimized.

Copy link
Member Author

commented Dec 26, 2014

Hmm, the first is probably a problem with the deserialization code, the
second is likely some interaction with the rot code where there's a bit
flipped or something.
I'm poking at making the active item processing code re-insert items at
their former locations, but I have almost no time to test it, so going is
slow.

@narc0tiq

This comment has been minimized.

Copy link
Contributor

commented Jan 1, 2015

Ah, @kevingranade, that 'bot you have doesn't understand the difference between mentioning a PR or issue and posting a fix for an issue to make it auto-close.

@KA101 KA101 removed their assignment Jan 1, 2015

@KA101

This comment has been minimized.

Copy link
Contributor

commented Jan 1, 2015

And you were working on it, Narc, which is why Kevin put you on the unit. Let's not mistake the merger for a code-author.

@narc0tiq

This comment has been minimized.

Copy link
Contributor

commented Jan 1, 2015

Sort of. This isn't an issue, so it's not getting auto-closed -- and in #10700 I was only referencing it as being a related PR (for background info). Pretty sure it doesn't make sense to assign me this PR, where assignees for PRs tend to be used as a "I'm about to merge this" flag.

@kevingranade kevingranade deleted the kevingranade:i_at-overhaul branch Mar 21, 2015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.