Skip to content

First set of active profile change fixes#8050

Closed
david-c-kline wants to merge 22 commits into
microsoft:mrtk_developmentfrom
david-c-kline:changeProfile250_part1
Closed

First set of active profile change fixes#8050
david-c-kline wants to merge 22 commits into
microsoft:mrtk_developmentfrom
david-c-kline:changeProfile250_part1

Conversation

@david-c-kline

@david-c-kline david-c-kline commented Jun 12, 2020

Copy link
Copy Markdown

This change is the first in a series of changes that will addresses the issues encountered when changing the active mixed reality toolkit configuration profile while the application is running.

The actual number of future PRs has not yet been determined. Test collateral and documentation will be included in the series as fixes enable them.

  • Failure to recreate the UIRaycastCamera

Change from Destroy to DestroyImmediate as the service locator expects the UIRaycastCamera to me immediately removed in order to correctly re-create it.

  • Handle data provider collection change during {Late]Update

This change handles the InvalidOperationException that is thrown when a collection is changed during enumeration, This prevents the code from incorrectly aborting.

  • Handle service collection change during ExecuteOnAllServices<In|Reverse>Order

This change replaces for with foreach and handles the InvalidOperationException that is thrown the services collection is changed during enumeration. This prevents the code from incorrectly aborting.

In this PR, the behavior of runtime profile change is modified to first take the request, then destroy current profile services after all LateUpdate()s of current profile services have been called, and finally initialize the new services associated with the new profile in the next frame before any Update() for a service is called. This solves the problem of potentially performing the profile change in the middle of an Update or LateUpdate of a service which causes a variety of problems. Delaying the initialization of new services to the next frame also helps to resolve the problems related to Destroy().

Testing

  • In editor using simulated hands
  • Toggle between two profiles on a click event
  • Confirm that interactable objects continue to function after re-acquiring the hand.
  • Attempt to use UnityUI
    • Fails with far interaction
    • Works with near interaction

@david-c-kline

Copy link
Copy Markdown
Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@david-c-kline

Copy link
Copy Markdown
Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

using UnityEngine;
using UnityEngine.TestTools;

namespace Microsoft.MixedReality.Toolkit.Tests

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A test that I would like to see, going forward:

  1. set the hololens 2 profile
  2. Press a button, make sure it presses via poke
  3. Set profile to be a hololens 1 profile
  4. verify that poking does not press a button
  5. Use head + airtap to press a button, verify that works.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Definitely! We should have extensive tests for ensuring this works once all of the issues have been uncovered.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just to check @julenka you mean a test within this PR right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I will see if that scenario can work with just the changes in this PR. It may require additional changes to fully support the requested test.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Looking at the profiles, there does not appear to be anything in the Default HoloLens1 profile that expressly disables near interaction.

I will update the profile as part of getting the above described test implemented.

Comment thread Assets/MRTK/Core/Services/MixedRealityToolkit.cs Outdated
@david-c-kline

Copy link
Copy Markdown
Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

}
catch(InvalidOperationException)
{
Debug.Log("Data provider collection was changed during Update.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It feels like try/catching exceptions due to the collection changing instead of more completely managing the lifecycle could lead to unexpected behavior.

For example (but please correct me if I'm wrong here!):

  1. In update, the profile changes
  2. This exception is thrown, and (as far as I can tell), the new providers don't get Update called on them and instead this method bails
  3. LateUpdate is called on all (now, the new) data providers

If a data provider does something in LateUpdate to follow up on something that's expected to be done in Update, and I think there's a fairly strong promise that system lifecycle happens in a specific order, the provider could get into a weird state.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also it looks like only Update and LateUpdate are wrapped. What if I try to change the profile when a system is enabled / disabled / reset (maybe not a common scenario, but I'm not sure)?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To follow up on this, I remember we had a conversation a while back about more explicitly handling where / when the profiles are torn down and started up. Something like:

  1. New profile is set
  2. Old services are torn down
  3. All remaining service lifecycle code is cancelled for this frame
  4. Next frame, new services are spun up
  5. Normal service lifecycle resumes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All this to say I really think handling the lifecycle of all this more explicitly and completely might lead to not having to spot-fix specific issues one at a time (for example, if we wait until the next frame to try to recreate the UIRaycastCamera, would we have to make the DestroyImmediate race condition change? or does the frame boundary complete the clean-up and leave the scene fresh for the new services?).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

if we wait until the next frame to try to recreate the UIRaycastCamera, would we have to make the DestroyImmediate race condition change?

that appears to be an issue based on Unity indicating the object exists after Destroy and before cleanup. MRTK is checking the next frame and, seeing that the object exisst, does not recreate it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Super appreciate the feedback here!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

All remaining service lifecycle code is cancelled for this frame

I'll see what i can do to ensure complete update/cleanup between changes. It may introduce an extra frame, or so, between shutdown and restart.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

What if I try to change the profile when a system is enabled / disabled / reset.

The [Late]Update try/catch is only for data providers. Services are covered by the handling in ExecuteOnAllServices* methods, which are called in most (very few exceptions) situations.

An earlier incarnation of this change had exception handing for all data provider methods (enable, disable, etc) and that was originally deemed unneccessary.

Happy to revisit!

Comment thread Assets/MRTK/Core/Services/MixedRealityToolkit.cs Outdated
Comment thread Assets/MRTK/Core/Services/MixedRealityToolkit.cs Outdated
Comment thread Assets/MRTK/Core/Services/MixedRealityToolkit.cs Outdated
Comment thread Assets/MRTK/Core/Services/BaseDataProviderAccessCoreSystem.cs Outdated
@david-c-kline

Copy link
Copy Markdown
Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@MaxWang-MS

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@MaxWang-MS

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

/// <inheritdoc/>
public override void Initialize()
{
ReadProfile();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It feels like this order is a bit off - ReadProfile needs BoundaryProfile (i.e. if it's null, it will crash right?)

Or is the check of BoundaryProfile == null on line 59 not needed?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@davidkline-ms

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Let me double check. I believe all of the systems that have this method may behave the same.

}
else
{
ResetConfiguration(value);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If I happened to pass the same profile in that matches the current one, we'd still want to reset in that case right?

Comment thread Assets/MRTK/Core/Services/MixedRealityToolkit.cs Outdated
Co-authored-by: Will <wiwei@microsoft.com>
}
else
{
ResetConfiguration(value);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do you know why in the else case were and synchronously resetting things within this call, but for the new thing we do that async on the next update?

It does feel a bit weird that we have a divergence of behavior between these two (it feels like we should be consistent with when we chose to tear things down and restart things).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Suggest adding detailed comment describing the scenarios.

Comment thread Assets/MRTK/Core/Services/MixedRealityToolkit.cs
if (IsActiveInstance)
{
LateUpdateAllServices();
if (newProfile != null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you add some comments for why we're doing RemoveCurrentProfile here and then InitializeNewProfile in Update?

It's obvious to folks working on this right now but the order actually is worth documenting.

/// <remarks>
/// When setting the ActiveProfile, all currently running services will be destroyed and those specified in
/// the new profile will be instantiated and initialized. A noticable application hesitation may occur
/// during this process.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's going to be time between LateUpdate and Update where MRTK is destroyed across calls to update the ActiveProfile - it woudl be good to capture that in the remarks.

Also would be good to mention that any scripts that have higher priority than the MRTK object will run before this has a chance to set things, so to be careful with accessing MRTK state if you happen to have such a thing.


private void Update()
{
if ((CoreServices.InputSystem == null) ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you add a comment on this (i.e. designed to avoid running in cases where the MRTK instance is reinitializing)


base.Initialize();

if (!Application.isPlaying || !XRDevice.isPresent) { return; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you explain a bit why we swapped the order here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@davidkline-ms

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I will address in a separate pr

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

initialization needs to happen right away, otherwise profiles do not get read correctly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

going to open a separate PR for the boundary system issues

/// <inheritdoc />
private void OnDestroy()
{
if (GazeCursor != null && !GazeCursor.Equals(null))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you leave a comment about why we both check against null and also against null?

execute(services[i]);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change

@david-c-kline david-c-kline left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

minor comment

}
else
{
ResetConfiguration(value);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Suggest adding detailed comment describing the scenarios.

david-c-kline pushed a commit that referenced this pull request Aug 5, 2020
@wiwei wiwei closed this Aug 6, 2020
@david-c-kline david-c-kline deleted the changeProfile250_part1 branch August 7, 2020 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants