Skip to content

Remove configurable_error_handler feature #18801

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

Conversation

SpecificProtagonist
Copy link
Contributor

@SpecificProtagonist SpecificProtagonist commented Apr 11, 2025

Objective

Bevy allows globally overwriting the default error handler. However, this requires taking a common exclusive lock, leading to some performance penalties. Because of these, this ability was gated behind the configurable_error_handler feature.

Solution

Because error handlers are function pointers, we do not need to take a lock, we can use atomics; because it is only written to beforehand, we can use relaxed loads which generally are equivalent in performance to normal loads (e.g. compiles down to movq on amd64).

Testing

Fallible params example.

@SpecificProtagonist SpecificProtagonist added A-ECS Entities, components, systems, and events D-Unsafe Touches with unsafe code in some way S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Apr 11, 2025
@alice-i-cecile alice-i-cecile added the S-Needs-Benchmarking This set of changes needs performance benchmarking to double-check that they help label Apr 11, 2025
@alice-i-cecile
Copy link
Member

Can you do a quick set of benchmarks on the commands benches to verify that this is just about as fast? I trust your intuition here but it will help me sleep better.

If that works out we should seriously consider shipping this for 0.16: it's a better experience and removes a perf foot gun.

@alice-i-cecile alice-i-cecile added this to the 0.16 milestone Apr 11, 2025
@hukasu
Copy link
Contributor

hukasu commented Apr 11, 2025

SystemInfo { os: "Linux (NixOS 24.11)", kernel: "6.6.85", cpu: "Intel(R) Core(TM) i7-14700KF", core_count: "20", memory: "31.2 GiB" }

this is just one run of each though

24baf32

empty_commands/0_entities
                        time:   [4.0490 ns 4.0601 ns 4.0703 ns]
spawn_commands/2000_entities
                        time:   [142.03 µs 143.08 µs 144.56 µs]
spawn_commands/4000_entities
                        time:   [295.78 µs 302.49 µs 308.31 µs]
spawn_commands/6000_entities
                        time:   [445.00 µs 453.65 µs 461.37 µs]
spawn_commands/8000_entities
                        time:   [581.25 µs 586.46 µs 591.54 µs]
insert_commands/insert  time:   [491.52 µs 493.86 µs 496.14 µs]
insert_commands/insert_or_spawn_batch
                        time:   [202.26 µs 203.12 µs 203.89 µs]
insert_commands/insert_batch
                        time:   [180.46 µs 181.64 µs 182.90 µs]
fake_commands/2000_commands
                        time:   [11.891 µs 11.912 µs 11.935 µs]
fake_commands/4000_commands
                        time:   [23.827 µs 23.923 µs 24.049 µs]
fake_commands/6000_commands
                        time:   [36.517 µs 36.685 µs 36.831 µs]
fake_commands/8000_commands
                        time:   [48.459 µs 48.751 µs 49.026 µs]
sized_commands_0_bytes/2000_commands
                        time:   [10.227 µs 10.275 µs 10.323 µs]
sized_commands_0_bytes/4000_commands
                        time:   [20.651 µs 20.691 µs 20.736 µs]
sized_commands_0_bytes/6000_commands
                        time:   [31.325 µs 31.385 µs 31.456 µs]
sized_commands_0_bytes/8000_commands
                        time:   [41.675 µs 41.975 µs 42.252 µs]
sized_commands_12_bytes/2000_commands
                        time:   [10.684 µs 10.765 µs 10.839 µs]
sized_commands_12_bytes/4000_commands
                        time:   [23.065 µs 23.093 µs 23.123 µs]
sized_commands_12_bytes/6000_commands
                        time:   [34.574 µs 34.656 µs 34.737 µs]
sized_commands_12_bytes/8000_commands
                        time:   [45.958 µs 46.111 µs 46.283 µs]
sized_commands_512_bytes/2000_commands
                        time:   [35.049 µs 35.206 µs 35.369 µs]
sized_commands_512_bytes/4000_commands
                        time:   [77.825 µs 78.153 µs 78.484 µs]
sized_commands_512_bytes/6000_commands
                        time:   [129.34 µs 129.91 µs 130.53 µs]
sized_commands_512_bytes/8000_commands
                        time:   [175.54 µs 176.17 µs 176.84 µs]

68c1362

empty_commands/0_entities
                        time:   [3.3094 ns 3.3466 ns 3.3888 ns]
spawn_commands/2000_entities
                        time:   [128.62 µs 131.20 µs 133.57 µs]
spawn_commands/4000_entities
                        time:   [262.22 µs 268.33 µs 273.63 µs]
spawn_commands/6000_entities
                        time:   [402.36 µs 411.46 µs 419.34 µs]
spawn_commands/8000_entities
                        time:   [524.04 µs 529.19 µs 533.77 µs]
insert_commands/insert  time:   [422.94 µs 423.85 µs 424.90 µs]
insert_commands/insert_or_spawn_batch
                        time:   [201.62 µs 203.21 µs 204.59 µs]
insert_commands/insert_batch
                        time:   [185.47 µs 187.03 µs 188.63 µs]
fake_commands/2000_commands
                        time:   [11.601 µs 11.632 µs 11.662 µs]
fake_commands/4000_commands
                        time:   [23.343 µs 23.412 µs 23.473 µs]
fake_commands/6000_commands
                        time:   [36.104 µs 36.278 µs 36.439 µs]
fake_commands/8000_commands
                        time:   [47.954 µs 48.059 µs 48.181 µs]
sized_commands_0_bytes/2000_commands
                        time:   [9.9470 µs 10.080 µs 10.221 µs]
sized_commands_0_bytes/4000_commands
                        time:   [20.914 µs 20.970 µs 21.032 µs]
sized_commands_0_bytes/6000_commands
                        time:   [30.963 µs 31.256 µs 31.525 µs]
sized_commands_0_bytes/8000_commands
                        time:   [41.722 µs 41.852 µs 41.985 µs]
sized_commands_12_bytes/2000_commands
                        time:   [10.976 µs 11.002 µs 11.030 µs]
sized_commands_12_bytes/4000_commands
                        time:   [23.013 µs 23.070 µs 23.129 µs]
sized_commands_12_bytes/6000_commands
                        time:   [34.193 µs 34.403 µs 34.590 µs]
sized_commands_12_bytes/8000_commands
                        time:   [46.558 µs 46.707 µs 46.877 µs]
sized_commands_512_bytes/2000_commands
                        time:   [41.686 µs 41.804 µs 41.923 µs]
sized_commands_512_bytes/4000_commands
                        time:   [88.591 µs 89.045 µs 89.458 µs]
sized_commands_512_bytes/6000_commands
                        time:   [145.35 µs 145.85 µs 146.43 µs]
sized_commands_512_bytes/8000_commands
                        time:   [197.83 µs 199.57 µs 201.62 µs]

@NthTensor
Copy link
Contributor

Oh this is very unsafe! I love it.

The review standard for this needs to be set very high, I think. A bad function pointer transmute is the worst kind of UB. But it looks pretty sane to me so far.

/// [`default_error_handler`]: super::default_error_handler
pub fn set_global_default_error_handler(handler: fn(BevyError, ErrorContext)) {
if HANDLER_ADDRESS
.compare_exchange(core::ptr::null_mut(), handler as *mut (), AcqRel, Acquire)
Copy link
Contributor

Choose a reason for hiding this comment

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

Might we wan this to point to panic by default instead of null, allow it to be set multiple times, and get rid of the branch in default_error_handler and the nullability in get? I'm fine either way.

Copy link
Contributor Author

@SpecificProtagonist SpecificProtagonist Apr 11, 2025

Choose a reason for hiding this comment

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

Might we wan this to point to panic by default instead of null

Good idea, done.

allow it to be set multiple times

I didn't do that because then we would either have to use Acquire loads, or have a potentially more confusing situation if the user changes it while bevy is running. Both cases are probably fine, but I'd want better benchmarks beforehand. I'm also thinking of a cfg(debug_assertions) check that the handler hasn't been read (when setting the handler).

Copy link
Contributor

Choose a reason for hiding this comment

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

Just to double check, can you walk me through why we would need Acquire here? Function pointers are effectively 'static right, is that not strong enough?

My other thought is, if we only want to write to this once then we don't actually need atomics do we? There are never conflicting updates, the problem is entirely memory ordering. So we can use an unsafe cell and fences, can't we? That would alleviate the concern about the function pointer size on exotic platforms.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if we only want to write to this once then we don't actually need atomics do we?

We can skip them if we make set_global_default_error_handler unsafe (with the precondition that all threads that one must establish a happens-before relationship with all threads that call default_error_handler). Otherwise we need atomics in order to remain sound.

// SAFETY: We only ever store null or a valid handler function.
// `null` is guaranteed to be represent `None`:
// https://rust-lang.github.io/unsafe-code-guidelines/layout/function-pointers.html#representation
unsafe { core::mem::transmute(ptr) }
Copy link
Contributor

Choose a reason for hiding this comment

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

The rust docs have this to say on the topic of raw-pointer to function-pointer transmutation:

Note that all of this is not portable to platforms where function pointers and data pointers have different sizes.

Is this something we are concerned about? If not, it would probably be good to mention it in or around the safety comment there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a comment. I haven't added a workaround for those platforms yet because that would need a cfg and not just a branch on a constant, and I don't have a list of those platforms (if they exist yet).

Copy link
Contributor

Choose a reason for hiding this comment

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

There's a crate called atomic_fn that does gross int-to-pointer casting but which seems to solve the size issue. I don't think we need to use it, merely mentioning it for posterity in case this becomes an issue later.

@SpecificProtagonist
Copy link
Contributor Author

SpecificProtagonist commented Apr 11, 2025

In theory, the only performance effects of this PR vs having the flag turned off would be non-local effects from not being able to inline the panic if LLVM doesn't recognize that the static variable is never written to – and of course the ever present possibility that LLVM happens to end up optimizing some things differently that any change can have.

I did some benchmarking against the baseline of main with the configurable_error_handler feature turned off (rustc 1.88.0-nightly, LLVM 20.1.1, Ryzen 5 5600H, kernel version 5.15.179, frequency governor set to performance, no unnecessary programs open). There were some statistically significant performance improvements and regressions.

Comparing that baseline to main with configurable_error_handler turned on, however, there also were both improvements and regressions. But there sometimes were large changes between runs: E.g. sized_commands_512_bytes/6000_commands changed from a 4% regression to a 28% regression despite running the same binary.

I got the following results just from comparing the baseline to a re-run of itself:

empty_commands/0_entities
                        time:   [5.6350 ns 5.6410 ns 5.6475 ns]
                        change: [-4.4805% -3.6390% -2.7429%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 6 outliers among 100 measurements (6.00%)
  3 (3.00%) high mild
  3 (3.00%) high severe

spawn_commands/2000_entities
                        time:   [242.69 µs 243.61 µs 244.53 µs]
                        change: [+2.1105% +3.2486% +4.6301%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 10 outliers among 100 measurements (10.00%)
  7 (7.00%) low mild
  1 (1.00%) high mild
  2 (2.00%) high severe
spawn_commands/4000_entities
                        time:   [490.00 µs 491.52 µs 492.88 µs]
                        change: [+1.6069% +2.3999% +3.2302%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high mild
spawn_commands/6000_entities
                        time:   [747.42 µs 751.49 µs 755.37 µs]
                        change: [+0.9901% +1.8686% +2.7971%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) high mild
  1 (1.00%) high severe
spawn_commands/8000_entities
                        time:   [984.25 µs 986.87 µs 989.09 µs]
                        change: [+0.3780% +1.2004% +2.0440%] (p = 0.01 < 0.05)
                        Change within noise threshold.

insert_commands/insert  time:   [629.01 µs 629.68 µs 630.42 µs]
                        change: [+0.0373% +1.0394% +1.8877%] (p = 0.02 < 0.05)
                        Change within noise threshold.
Found 13 outliers among 100 measurements (13.00%)
  4 (4.00%) high mild
  9 (9.00%) high severe
insert_commands/insert_or_spawn_batch
                        time:   [255.02 µs 257.24 µs 259.62 µs]
                        change: [-1.0135% +0.2194% +1.4342%] (p = 0.73 > 0.05)
                        No change in performance detected.
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) high mild
insert_commands/insert_batch
                        time:   [243.04 µs 244.00 µs 245.04 µs]
                        change: [+0.3170% +1.4962% +2.6241%] (p = 0.01 < 0.05)
                        Change within noise threshold.
Found 4 outliers among 100 measurements (4.00%)
  2 (2.00%) high mild
  2 (2.00%) high severe

fake_commands/2000_commands
                        time:   [17.937 µs 17.954 µs 17.973 µs]
                        change: [-1.6120% -1.2189% -0.8583%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 10 outliers among 100 measurements (10.00%)
  7 (7.00%) high mild
  3 (3.00%) high severe
fake_commands/4000_commands
                        time:   [36.094 µs 36.342 µs 36.735 µs]
                        change: [-0.2918% +0.2628% +0.9349%] (p = 0.46 > 0.05)
                        No change in performance detected.
Found 7 outliers among 100 measurements (7.00%)
  2 (2.00%) high mild
  5 (5.00%) high severe
fake_commands/6000_commands
                        time:   [53.832 µs 53.878 µs 53.928 µs]
                        change: [+0.1662% +0.4918% +0.7899%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 8 outliers among 100 measurements (8.00%)
  4 (4.00%) high mild
  4 (4.00%) high severe
fake_commands/8000_commands
                        time:   [72.540 µs 73.142 µs 73.725 µs]
                        change: [-8.2177% -7.4964% -6.8561%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 15 outliers among 100 measurements (15.00%)
  3 (3.00%) high mild
  12 (12.00%) high severe

sized_commands_0_bytes/2000_commands
                        time:   [15.749 µs 15.764 µs 15.782 µs]
                        change: [-4.5598% -4.0298% -3.4470%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 10 outliers among 100 measurements (10.00%)
  4 (4.00%) high mild
  6 (6.00%) high severe
sized_commands_0_bytes/4000_commands
                        time:   [31.695 µs 31.783 µs 31.910 µs]
                        change: [+0.6547% +1.8938% +3.5612%] (p = 0.01 < 0.05)
                        Change within noise threshold.
Found 12 outliers among 100 measurements (12.00%)
  7 (7.00%) high mild
  5 (5.00%) high severe
sized_commands_0_bytes/6000_commands
                        time:   [47.120 µs 47.160 µs 47.200 µs]
                        change: [-1.3460% -0.9543% -0.5918%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 7 outliers among 100 measurements (7.00%)
  2 (2.00%) low mild
  4 (4.00%) high mild
  1 (1.00%) high severe
sized_commands_0_bytes/8000_commands
                        time:   [62.899 µs 63.169 µs 63.554 µs]
                        change: [-1.3681% -0.8614% -0.3893%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 8 outliers among 100 measurements (8.00%)
  5 (5.00%) high mild
  3 (3.00%) high severe

sized_commands_12_bytes/2000_commands
                        time:   [17.742 µs 17.821 µs 17.962 µs]
                        change: [-9.5400% -9.3157% -8.9956%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 8 outliers among 100 measurements (8.00%)
  1 (1.00%) low mild
  4 (4.00%) high mild
  3 (3.00%) high severe
sized_commands_12_bytes/4000_commands
                        time:   [35.635 µs 35.689 µs 35.741 µs]
                        change: [-0.9335% -0.4364% +0.0196%] (p = 0.08 > 0.05)
                        No change in performance detected.
Found 3 outliers among 100 measurements (3.00%)
  1 (1.00%) high mild
  2 (2.00%) high severe
sized_commands_12_bytes/6000_commands
                        time:   [53.011 µs 53.047 µs 53.088 µs]
                        change: [-1.4273% -0.4784% +0.2183%] (p = 0.31 > 0.05)
                        No change in performance detected.
Found 7 outliers among 100 measurements (7.00%)
  1 (1.00%) low mild
  4 (4.00%) high mild
  2 (2.00%) high severe
sized_commands_12_bytes/8000_commands
                        time:   [70.593 µs 70.658 µs 70.748 µs]
                        change: [-0.3041% +0.2311% +0.6102%] (p = 0.38 > 0.05)
                        No change in performance detected.
Found 11 outliers among 100 measurements (11.00%)
  1 (1.00%) low severe
  1 (1.00%) low mild
  4 (4.00%) high mild
  5 (5.00%) high severe

sized_commands_512_bytes/2000_commands
                        time:   [52.841 µs 54.126 µs 55.477 µs]
                        change: [+8.6015% +10.872% +13.083%] (p = 0.00 < 0.05)
                        Performance has regressed.
sized_commands_512_bytes/4000_commands
                        time:   [101.33 µs 103.75 µs 106.44 µs]
                        change: [+2.9383% +5.1953% +7.6506%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 5 outliers among 100 measurements (5.00%)
  5 (5.00%) high mild
sized_commands_512_bytes/6000_commands
                        time:   [151.37 µs 154.24 µs 157.40 µs]
                        change: [+7.0107% +9.4103% +11.714%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 8 outliers among 100 measurements (8.00%)
  8 (8.00%) high mild
sized_commands_512_bytes/8000_commands
                        time:   [205.36 µs 211.01 µs 217.38 µs]
                        change: [+11.738% +14.262% +17.043%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high mild

So even here, where there should be no differences, there were >10% differences for some benchmarks; I suspect this difference comes simply from random changes in memory layout¹.

Another thing: The benchmarks are single-threaded. And while OnceLock is a lot more complicated, the relevant difference is that it performs an additional Acquire load, so there shouldn't be performance differences between main and this PR in the benchmarks.

In conclusion, uhh, I don't really know man. I don't think we can claim (in the single threaded case) that configurable_error_handler being off improves performance, nor can we claim that it being on doesn't hurt performance. I'd propose that we both remove the flag, and use the new implementation from this PR to safeguard against possible perf regressions under multithreading (though maybe someone with better knowledge of cache coherency protocols can chime in here).

¹: Here's a nice talk that, among other things, explains how much of an effect this can have

@NthTensor
Copy link
Contributor

This benchmark is not totally unsurprising. The inlined branchless uncontested acquire load in this pr should have very good performance on most systems, at least on paper, and it's pretty obviously sound.

My only concern would be support for fn pointer atomics on exotic platforms, but if that does become an issue I think there's some more unsafe stuff we can do with fences.

I'm in favor of removing the flag, for better UX and more consistent performance.

@@ -48,8 +47,7 @@
//! }
//!
//! fn main() {
//! // This requires the "configurable_error_handler" feature to be enabled to be in scope.
//! GLOBAL_ERROR_HANDLER.set(my_error_handler).expect("The error handler can only be set once.");
//! set_global_error_handler(my_error_handler);
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be set_global_default_error_handler right? Though maybe we should go with set_global_error_handler anyways for brevity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should be set_global_default_error_handler right?

Yup, thanks!

Though maybe we should go with set_global_error_handler anyways for brevity.

Brevity isn't that important for a function that's only called once, but no strong opinion from me.

@SpecificProtagonist
Copy link
Contributor Author

inlined branchless uncontested acquire load in this pr

(relaxed load)

more unsafe stuff we can do with fences

We can go back to a OnceLock on those platforms, if they turn up.

@chescock
Copy link
Contributor

I'm a little surprised that there is that much of a performance impact on looking up the error handler. The default behavior is to panic, so I would expect it to be needed at most once! ... Ah, I see, the calls to default_error_handler() are all made unconditionally before we have an error.

Would it work to just.. move the default_error_handler() calls to right before we invoke it? That should have zero overhead in the case where we don't have an error.

Alternately, could we store it on the World instead of a global? I think we have a World nearby whenever we call default_error_handler(). That would avoid the need for atomics or locks, since we could do ordinary reads and writes to the World.

@SpecificProtagonist
Copy link
Contributor Author

I'm a little surprised that there is that much of a performance impact on looking up the error handler.

Well, it's not that clear whether/how much overhead it caused in the first place.

Alternately, could we store it on the World instead of a global?

Yes, we totally can, great call. I'll update this PR to do that.

@NthTensor
Copy link
Contributor

Alternately, could we store it on the World instead of a global?

This will make configuring the error handling in the render world and other sub-apps more difficult.

@mockersf
Copy link
Member

and also let us have different error handling depending on each app... but I would prefer to do that in another PR if we want to get this one in the 0.16

@SpecificProtagonist
Copy link
Contributor Author

SpecificProtagonist commented Apr 11, 2025

PR for moving the default error handler into World: #18810

Also rebased on main to resolve conflict.

@SpecificProtagonist SpecificProtagonist force-pushed the atomic-default-error-handler branch from b3f8707 to d5304dc Compare April 12, 2025 03:02
Copy link
Contributor

@chescock chescock left a comment

Choose a reason for hiding this comment

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

Looks good! The API is simpler, the atomics look sound, and the transmute to a function pointer is literally the first example of valid ways to use transmute.

// The error handler must have been already set from the perspective of this thread,
// otherwise we will panic. It will never be updated after this point.
// We therefore only need a relaxed load.
let ptr = HANDLER.load(Relaxed);
Copy link
Contributor

Choose a reason for hiding this comment

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

Using Relaxed operations on a pointer makes me nervous, but I think this is okay for anything but edge cases.

The danger would be that a user wants some run-time configuration for their global error handler, and writes to a global variable before calling set, which then doesn't get synchronized with the load. But writes to a global variable will require their own synchronization, so it's hard to come up with a situation where that leads to a data race, and doing so would require unsafe code elsewhere. (It might also be possible to get into trouble using runtime code generation? But nobody would ever generate an error handler function that way.)

I'd still be inclined to use Acquire here (and Release for the store) to be safer. Acquire loads aren't usually any slower than relaxed ones. In particular, all loads on x86 have Acquire semantics, so they'll compile to the same machine instruction.

@chescock
Copy link
Contributor

Would it work to just.. move the default_error_handler() calls to right before we invoke it? That should have zero overhead in the case where we don't have an error.

To expand on this in case it's worth considering as an alternative: If we add a layer of indirection, then I think this could be as simple as:

#[inline]
pub fn default_error_handler() -> fn(BevyError, ErrorContext) {
    default_error_handler_inner
}

fn default_error_handler_inner(err: BevyError, context: ErrorContext) {
    GLOBAL_ERROR_HANDLER.get_or_init(|| panic)(err, context);
}

(Although I think we'd still want to introduce a set_global_default_error_handler function!)

@cart
Copy link
Member

cart commented Apr 14, 2025

I'd feel more comfortable moving this to 0.17:

  1. As mentioned above, doing this globally could have an impact on exotic platforms. Given all of the work that went in to improving our support in this category, I'd prefer to not throw a wrench in that right before release.
  2. The performance of this is almost certainly good enough, but the current impl (for the default hard-coded case) is guaranteed to be good enough (optimal in fact).
  3. Custom error handlers will be a reasonably uncommon case. Breaking that API across releases will be low-impact, and easily migrate-able.
  4. There are open interface questions.

@NthTensor
Copy link
Contributor

NthTensor commented Apr 15, 2025

Pretty much agreed. I don't its crucial to ship this change in 0.16, though I do still think it's an improvement.

@mockersf mockersf modified the milestones: 0.16, 0.17 Apr 15, 2025
@musjj
Copy link
Contributor

musjj commented Apr 15, 2025

Custom error handlers will be a reasonably uncommon case.

Custom error handlers is going to be pretty niche, but wanting errors to be logged as warnings instead of crashing (usually in a release build) is a pretty commonly raised complaint in the community. This is essentially the one single purpose the custom error handler feature enables (I'm not aware of any other real-world use-cases AFAIK)

Could the warn-dont-crash behavior be enabled via something like a feature flag?

@NthTensor
Copy link
Contributor

Could the warn-dont-crash behavior be enabled via something like a feature flag?

Currently you have to set a feature flag (which may cause a small performance regression in commands) and then put a function pointer in a once-lock. Cart is proposing shipping this behavior in 0.16 instead of the new behavior introduced in these PRs.

@SpecificProtagonist
Copy link
Contributor Author

SpecificProtagonist commented Apr 15, 2025

I wanted to avoid adding this feature flag since I can't prove that it's necessary, but given that this PR won't be in 0.16, I prefer closing it in favor of moving the global state into world. Feel free to reopen should we decide against that :)

@NthTensor
Copy link
Contributor

Thanks for all your hard work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events D-Unsafe Touches with unsafe code in some way S-Needs-Benchmarking This set of changes needs performance benchmarking to double-check that they help S-Needs-Review Needs reviewer attention (from anyone!) to move forward
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants