-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
More informations for query errors #9041
base: main
Are you sure you want to change the base?
More informations for query errors #9041
Conversation
…mations with the Query[...]Error types)
@@ -1436,7 +1467,7 @@ pub enum QueryComponentError { | |||
/// # Example | |||
/// | |||
/// ``` | |||
/// # use bevy_ecs::{prelude::*, system::QueryComponentError}; | |||
/// # use bevy_ecs::{prelude::*, system::{QueryComponentError, QueryComponentErrorDetail}}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(comment applies to the whole QueryComponentError
rather than this specific line.
Avoid variants all holding the same data
I see that all variants now are FooBar(QueryComponentErrorDetail)
. Wouldn't it make more sense to convert QueryComponentError
into a struct
?
This would remove a lot of the boilerplate.
pub enum QueryComponentErrorKind {
MissingReadAccess,
MissingWriteAccess,
// ...
}
pub struct QueryComponentError {
pub kind: QueryComponentErrorKind,
pub entity: Entity,
pub component: &'static str,
pub query: &'static str,
}
Also QueryComponentErrorKind
is a fairly long name, it starts to trigger my java-factory detector. After a certain identifier length, it becomes difficult to read code. But I think it's fine as is right now.
Avoid the &'static str
Instead of holding the str
for the component name and query name, it's possible to make QueryComponentError
generic over Q
and C
. It would look like this:
pub struct QueryComponentError<Q, C> {
pub kind: QueryComponentErrorKind,
pub entity: Entity,
pub _component: PhantomData<fn(C)>,
pub _query: PhantomData<fn(Q)>,
// alternatively: pub _types: PhantomData<fn(C, Q)>,
}
Then, you'd use the type_name
function in the display impl. This way, it avoids bloating the error type (and therefore the result type) with 32 additional bytes, it's basically free until you display the error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First of all thank you for the review!
I want to say that most of my choice were led by "not changing the existing structure too much" : all the "QuerySomethingError" were enums and two of them were already holding the same kind of data in all variants. QuerySingleError in particular was already holding a &'static str
. I don't have a strong opinion on them and I did think about the stuff you proposed, but didn't note them all down in the PR, thank you for opening the conversation about them.
About changing enum holding struct to struct with a Kind
enum field:
- It makes sense, but I'm a bit afraid the kind of error would be "buried" in the doc a bit
- Should I do the same to
QuerySingleError
? (probably not with thePhantomData
, see bellow)
About using PhantomData
instead of &'static str
:
- This does make runtime a tiny bit faster if you don't use this data I think, and take less memory I guess, but it would probably make compile time a bit longer and binary size a bit heavier because of monomorphization,
- Why the
fn
in the PhantomData? Is it to avoid explicit lifetime? - Should I restrict Q and C with a trait constraint? What should I use for Q?
- Should I add helper functions to this type for end user to be able to get the string representation of query and component without having to know about
std::any
nor use the Display trait? - Should I do the same for
QuerySingleError
?
About get_mismatches_detail, reply here: #9041 (comment)
Also since @james7132 self-requested a review, can I have your opinion on all of this, I would appreciate a third opinion before making the changes (I'll have to change all the tests and docs and stuff).
} | ||
|
||
/// An error that occurs when retrieving a specific [`Entity`]'s query result from [`Query`](crate::system::Query) or [`QueryState`]. | ||
// TODO: return the type_name as part of this error | ||
#[derive(Debug, PartialEq, Eq, Clone, Copy)] | ||
#[derive(Debug, PartialEq, Eq, Clone)] | ||
pub enum QueryEntityError { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same remark as for QueryComponentError
. This type should be converted to a struct
, instead of having every variants hold a QueryEntityErrorDetail
.
pub struct QueryEntityErrorDetail { | ||
/// Specific entity which was requested when encountering the error. | ||
pub requested_entity: Entity, | ||
/// Representation of the query's type. | ||
pub query_type: &'static str, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the query_type
can be converted into a generic parameter here as well.
/// Either it does not have a requested component, or it has a component which the query filters out. | ||
QueryDoesNotMatch(Entity), | ||
/// The given [`Entity`]'s components do not match the query, see [`QueryEntityMismatchDetail`]. | ||
QueryDoesNotMatch(QueryEntityMismatchDetail, QueryEntityErrorDetail), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think QueryEntityMismatchDetail
is too heavy, both to compute and memory-wise to add to the return result of Query::get
. Since Query::get_mismatches_detail
exists, I think there is no need to add this to QueryEntityError
, users can always call get_mismatches_detail
if they have an Err
and need to know the exact problems encountered.
It's very common for bevy code to do
if let Ok(foo) = query.get(entity) {
// ...
}
If the Err
is a common case, and is always skipped here, the user is going to create and discard a QueryEntityMismatchDetail
multiple times per frame, while they absolutely do not care about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda hesitated a bit about this yeah.
About removing the QueryEntityMismatchDetail
in the QueryDoesNotMatch
variant (or the struct holding it) and letting people use get_mismatches_detail
:
- It would be a tiny bit more annoying to use (you have to match the variant to check that it's a
QueryDoesNotMatch
before calling the function, but still need the query itself to call the function so you can't just pass the error to a helper function handle it you need to pass the query too), but I agree that it's still better than what we have now (which is no information at all about the mismatch) and that it's a usecase too specific to calculate it and store it every time. - I would like to split the variant
QueryDoesNotMatch
intoQueryComponentsMismatch
andQueryChangeDetectionMismatch
at least, otherwise in the case of a change detection mismatch, people might useget_mismatches_detail
and not understand why it returns empty arrays (orNone
, see bellow). - I need to change
get_mismatches_detail
as it's not super user friendly right now, should I make it so it returns anOption
so it returnsNone
if there's no mismatch? This would save computing time when it's the case and make it clearer that the entity does in fact match the query (at least components-wise), but maybe people would still want information about "subqueries" in the case of anOr
?
|
||
/// Diagnosis about why an [`Entity`] and a [`Query`](crate::system::Query) or [`QueryState`] don't match. | ||
#[derive(Debug, PartialEq, Eq, Clone)] | ||
pub enum QueryEntityMismatchDetail { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the Detail
here only contributes to make the type harder to read. I'd rather see a shorter name.
pub enum QueryEntityMismatchDetail { | |
pub enum QueryEntityMismatch { |
So I see two ways someone becomes familiar with this error API.
In case (1), the user is already trying to find what can go wrong. If they don't get their answer initially from reading the In case (2), the error message will contain the details as to why a panic occured. It would also make the match in the Regarding generic parametersFairly certain there is no compiler overhead compared to the current PR. This is basically just delaying the instantiation of the type name to when it's meaningfully used, rather than before. I wrapped the types in
Whenever you need to instantiate a Regarding whether we should go forward with this.I also am worried about exposing such an intricate error API. It's something that we then have to maintain, and break user code when bevy internals change. Maybe most fields should stay private? See how I decided to make a part of an error private, as to avoid having to deal with user-facing API: #8887 (comment) Why are we exposing this? If it is to provide bevy users with a detailed message about the query error. We should rely on the If it's about giving the tools to dynamically react to missing components on entities… Well, can you give an example of what? dynamic-style queries always require exclusive world access, and we are incuring a cost in situations where this is impossible. Regarding
|
I do (1) a lot ^^'
I get that, my question was: should I enforce that by having trait constraints, and if so which one should I use for Q?
So people, particularly people making libraries, can make their own error messages. I know I would use it in
I understood that's what you wanted in your previous message and I agree with that. For me it wasn't that big of a deal not because I though the computation was trivial (yes it's just a few components but potentially inside loops etc...) but because I thought errors aren't supposed to happen that often if everything goes well, but if you're telling there are a lot of cases where you might want to call
I disagree with this, since the "mismatch" can also happen when there is a
Both of those are not good either since a mismatch can happen also with change detection, and my function returns why there is a component mismatch only. This is also why I wanted to separate the variants. |
@nicopap I tried implementing your requested changes but I encountered problems for the "getting rid of I first tried to have for example
I then thought "well no big deal, instead I will define it as
I'm not sure how to proceed, I think I can take my first approach again ( |
I'm going to submit a PR to your branch to help you out. |
The PR is ready and available at Selene-Amanita#1 |
Objective
QueryDoesNotMatch
errors #8917 : add information about query/entity mismatch forget
/get_mut
, ...QueryDoesNotMatch
errors #8917 (comment) : add information about query errors in general.Query::get_component
should mention that the compenent must be part of the query #9011 : fix documentation forget_component
/get_component_mut
Solution
QueryEntityMismatchDetail
andQueryEntityComponentMismatch
.get_mismatches_detail
to get theQueryEntityComponentMismatch
array.ChangeDetectionMismatch
, because I have no idea how to get this information.QueryComponentErrorDetail
andQueryEntityErrorDetail
, I think it's better to put a struct instead of a tuple to know what is what, but I left a singleton tuple inQuerySingleError
because there's only one information to get here.std::any
to fill the type representations of the query/component.For 3 and 4, just update the documentation.
Summary of the new structures
(I didn't put all the comments/derive, and didn't put
QuerySingleError
since it didn't change)For
QueryComponentError
:For
QueryEntityError
:Code to test quickly
If you think of other scenarios to test please tell me or test yourself.
Drawbacks
This gives more information about why an error happened, and can allow easier generic logic to handle them, but at three costs:
Result
can take more memory if the components requested take less space than the error structAll of this even if the end user doesn't really need that information.
I don't think it's a really big deal since the error struct is basically pointers to
Vec
andString
, errors are not supposed to happen very often, and functions likecontains
can be used to just check if an entity is fetched by a query.Maybe the error could be computed in debug mode only or be behind a feature or something?
Notes
QueryEntityMismatchDetail
is just an enum, that could be merged withQueryEntityError
(inQueryEntityError
, have a variantQueryDoesNotMatchComponents
andQueryDoesNotMatchChangeDetection
for example), but I decided against it to avoid a too big breaking change, and to allow for potential additional "mismatch" causes in the future.get_mismatches_detail
public but it maybe doesn't need to be, also to not make it unsafe I put specialized arguments instead of taking anUnsafeWorldCell
. Maybe a more user-friendlyget_mismatches_detail
can be created alongside this one.get_mismatches_detail
isn't very "rusty" (for loops inside of iterators), you can make it better if you have suggestions to change it.QueryState
(filter_state
?) and how to retrieve it.Changelog
QueryEntityError
's variants to add more information on the error.QueryComponentError
's variants to add more information on the error.Migration Guide
QueryEntityError
's variants now have aQueryEntityErrorDetail
singleton instead ofEntity
, andQueryDoesNotMatch
also haveQueryEntityMismatchDetail
, you can ignore them in your pattern matching, or get the Entity fromQueryEntityErrorDetail
:QueryEntityError::QueryDoesNotMatch(entity) => entity
becomesQueryEntityError::QueryDoesNotMatch(_, error_details) => error_details.requested_entity
.QueryComponentError
's variants now have aQueryComponentErrorDetail
singleton, you can just ignore it in your pattern matching:QueryComponentError::MissingReadAccess => "missing read access"
becomesQueryComponentError::MissingReadAccess(_) => "missing read access"