Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign upRemove blocking GetHeight #4689
Conversation
8c15239
to
454c6aa
I'm not keen on an ad-hoc introduction of optional. It would be better if we prevented things that are ticked under the chunkmap lock from calling back into world. For example, the farmland handler by rights should be able to determine if its wet solely from the cChunk it gets. Ideally GetHeight would simply return 0 when the chunk isn't loaded, as IsWeatherWetAt should return the global weather state. This is both consistent with what GetBlock does, as well as the physical interpretation that if the chunk is empty it's gonna be exposed to the sky and have no height. |
I don't think that's relevant to
Clearly not, or
This comes back to @madmaxoft's point in #4680 (comment). Failing silently is not good because we can get incorrect behaviours as a result. e.g. a block might fail a As for |
In response to not failing silently, GetBlock returns optional etc: Why? It's practically ingrained into the fabric of Minecraft that chunks might be unloaded. If it's absolutely required that you need the data, use a chunkloader. And what do unloaded chunks contain, if not air? About incorrect CanBeAt, I think it would suffice to stop ticking chunks with unloaded neighbours, or better yet when getting neighbours directly from a cChunk, simply "failing safe" and doing nothing. Certainly the farmland handler is strange though, (unless I'm totally blind xD) the call to IsWeatherWetAt doesn't seem to modify a_RelPos at all, and yet it couldn't find its own chunk? wat? Agreed with the point about going through cWorld in the general case; given that it's not feasible to remove the lock right now GetHeight has to change. |
Returning
That's not the same at all: a chunk stay gives you the accurate data 100% of the time without failing.
That's exactly what this change allows. You can now fail safe when going through
You're right. The original code was using |
I disagree that getting the height of, or a block at an unloaded chunk and receiving 0/air is a failure. The concept is meaningless when for any typical server, GetBlock would 'fail' for most of its inputs. I argue that, if one needs something more descriptive:
To be clear I'm absolutely not against something like TryGetHeight or GetBlockTypeMeta which return bools. My main concern is the introduction of an optional type and the inconsistency this brings with regards to everything that already exists. |
I disagree with the premise. Failure doesn't have to be rare. A compiler for example has an infinite number of input files that would fail to parse. That doesn't make it's any less of a failure. Whether you want to call it failure or not, distinguishing a loaded from an unloaded chunk is useful. Having to call a separate
The |
About the last point, surely not. Migrating piecemeal to optional in getheight, when things like getblock will effectively never change doesn't seem right to me. (also I believe xoft strongly dislikes underscores, so value_or is questionable :p). We should strongly prefer to maintain consistency. If they are equivalent, I would really urge that GetHeight be made not to block its caller and load chunks, TryGetHeight left as-is, and we should figure out why the farmland call tried to load any chunks in the first place. |
I am hugely concerned about consistency since this is still essentially a small project. A few years ago the cWorldInterface/ChunkInterfaces were introduced, but there was a lack of developer manpower to drive adoption or maintain them. This means that now multiple places have cWorldInterface and a cWorld, or both world and chunk interfaces etc. I feel keeping things simple is our best bet. |
I'm reminded of "a foolish consistency"
I agree that consistency is good, but only if the pattern you're following is consistently good. We already have several different ways of encoding "this operation might fail". Many follow the pattern of Many callers forget to check the return value. When the function does fail, you end up using an unassigned value. If we step outside of So, there is currently very little consistency anyway. And we should not just aim for consistency, but to consistently use the best option available to us. Not just the one that's most common now. I propose we use |
The consistency of naming, or whether some function returns success in the first place can be discussed on another day, and besides, getblocktypemeta on a chunk cannot fail, I thought? It's also not like our caller can't call value directly on the optional and end up with a bogus read in Release. Plus what is and isn't caught in code review is a complete unknown quantity, there's no evidence either way to suggest which is more visible. I think the discussion has boiled down to a philosophical debate about the merits of bool vs optional, since from a technical perspective they both work. The main problem to be fixed was making getheight non-blocking, which you've done. |
But wait.... What about: returning -1 for a failed getheight? This neatly sidesteps you concern about 0 being overloaded! |
I find this argument completely absurd. If you see
That has the exact same problem. It isn't obvious it can fail, so people will forget to check for failure. |
Please bear with me, I like a technical debate :) I was suggesting that if it gets past review, optional does nothing for the "failure" case where an empty chunk is interpreted as containing air. About visibility during review, I argue that just as you know .value implies optional, another hypothetical reviewer knows GetBlockTypeMeta implies a boolean return value, by virtue of knowing the signature of one of the most used class of functions. And about the status quo not working, I haven't seen evidence of this. The deadlock was caused by GetHeight improperly blocking. And your final point gets back to the definition of failure, which we couldn't agree on. hm, what about something like
This way, if you know that your call always will succeed - no overhead, no checks, no visually jarring |
Then, ensure in the main server code that entities/blocks on chunk boundaries aren't ticked, so they can never make a failing call. |
@madmaxoft as benevolent dictator, do you have any preference on |
I'm in favor of So, basically, two questions:
|
The only problem I could think of with |
I think build servers are the only reason why we aren't using C++17. It should not be too difficult to change though, all that's required is to manually create a new platform definition and cross-compiler pack for the raspberry pi builds. In terms of the actual substance, I strongly dislike introducing |
One other concern (trustme is guaranteed to always be passed not 0): #include <optional>
std::optional<int> get(int x)
{
if (x == 0)
{
return {};
}
return 1;
}
int trustme(int x) noexcept
{
return *get(x);
} compiles to trustme(int):
mov eax, 1
test edi, edi
jne .L5
xor eax, eax
mov BYTE PTR [rsp-1], 0
mov BYTE PTR [rsp-4], 0
mov WORD PTR [rsp-3], ax
mov eax, DWORD PTR [rsp-8]
.L5:
ret and calling .value instead #include <optional>
std::optional<int> get(int x)
{
if (x == 0)
{
return {};
}
return 1;
}
int trustme(int x) noexcept
{
return get(x).value();
} is even worse trustme(int):
sub rsp, 8
test edi, edi
je .L11
mov eax, 1
add rsp, 8
ret
trustme(int) [clone .cold]:
.L11:
mov edi, 8
call __cxa_allocate_exception
mov edx, OFFSET FLAT:std::bad_optional_access::~bad_optional_access() [complete object destructor]
mov esi, OFFSET FLAT:typeinfo for std::bad_optional_access
mov QWORD PTR [rax], OFFSET FLAT:vtable for std::bad_optional_access+16
mov rdi, rax
call __cxa_throw Compare with #include <cassert>
int get(int x)
{
if (x == 0)
{
assert(!"you goofed");
}
return 1;
}
int trustme(int x) noexcept
{
return get(x);
} on Release trustme(int):
mov eax, 1
ret Are you sure you want to pay this penalty for every Get, even those you either know will absolutely succeed, in Debug and Release? Not to mention littering every call with either an asterisk or .value() Paying only for what you use is a core C++ principle after all. |
I would love to move to C++17. I believe, as @bearbin said, the main reason was that the raspberry pi toolchain we use is too old. If that's something we can fix, then absolutely go for that.
I was going by the precedent of @tigerw that's not a fair comparison. You compile an assert in release mode and it compiles to nothing. That's obviously true by the definition of |
xoft: yeah Lua might need to be broken as peterbell points out, unless there's a way to detect the condition and unlock the mutex twice? peterbell: I was thinking of the hot-path plain int returning GetHeight/GetBlock; of course a similar TryGetX would incur the same penalty as the return value needs to be checked. Wouldn't you agree that it would be good for callers who know the call will always succeed (loaded chunk, chunkstay, player is close, previous call succeeded) to have the choice of using the direct version, instead of checking optional? |
Here is a fairer comparison: The additional cost is 3 extra instructions. Accessing the chunk storage arrays in the first place is quite likely to miss the CPU cache, in which case memory accesses can be 100x more expensive than a normal instruction. So, I genuinely don't think this will be important performance-wise. If you do really want direct access with minimal overhead (say for example to implement In general, it's also very tricky to know for sure that a chunk will be valid. For example, a player being close means nothing if the server is under high load and the chunk generator is going slowly. Chunk stays also don't quite guarantee the chunk will be present, because of #3572 (comment). |
The Lua API docs for cWorld:GetHeight needs to be updated, too. |
None of the optionals are in such a hot path that any of this would matter greatly. I'd say merge this PR in, then work on getting C++17 work, then remove the |
d13b72b
to
56ad5c0
93008fa
to
8a968b5
a94905e
to
bf44e26
51b79dd
to
0590ce4
0590ce4
to
98b778c
|
||
// Call the implementation: | ||
auto IsWet = self->IsWeatherWetAtXYZ(BlockPos); | ||
L.Push(IsWet.value_or(true)); |
This comment has been minimized.
This comment has been minimized.
bearbin
Aug 5, 2020
Member
not raining strikes me as a more useful default, in any case this should be documented in the API docs.
This comment has been minimized.
This comment has been minimized.
peterbell10
Aug 9, 2020
Author
Member
It only fails if it's raining in the chunk but the terrain is not loaded so it can't tell where the rain is falling. On the client that would render as rain falling all the way to void i.e. wet everywhere.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
bearbin
Aug 10, 2020
Member
I guess it doesn't really matter that much, the issue shouldn't come up that often. It's just that everywhere else that IsWeatherWetAtXYZ is used, the default on nullopt is false, rather than true.
peterbell10 commentedApr 22, 2020
Fixes #4688
As the issue shows, blocking
GetHeight
is very dangerous because we can't reliably unlock the chunk map mutex. Instead, we should returnoptional<int>
and figure out how to respond when the chunk isn't loaded at the call site.Since we don't have c++17, I'm just imitating the
std::optional
API withcpp17::optional
.WIP to figure out what to do with block handlers, entities and lua api which all use
GetHeight
either directly or indirectly.IsWeatherWetAtXYZ
which doesn't really matter that much. We can probably get away with using a default value there.cChunkMap::GenerateChunk
with a callback that sets the spawn. That might leave a gap where the spawn isn't set though which wouldn't be great.