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

Replace manual dynamic compilation with .NET Hot Reload #5041

Merged
merged 5 commits into from
Feb 21, 2022

Conversation

smoogipoo
Copy link
Contributor

@smoogipoo smoogipoo commented Feb 18, 2022

Closes #5039
Closes #1270
Closes #4699
Closes #4387
Closes ppy/osu#9225

Notes:

  • With Rider, you'll get a popup that you have to click "Apply Changes" in every time a change is made. It also seems to break every now and then and the popup just doesn't show, requiring a restart.
    image
  • As suggested by @peppy, I recommend adding a keybind to Apply Hot Reload Changes in Rider (defaults to Alt+F10).
  • Using dotnet watch from CLI in osu.Framework.Tests directory will continuously hot-reload.
  • I haven't removed the assembly dropdown from the test browser in this PR. Doing so seems to be a larger change than it is at face value.

Self-contained publish size:

  • Before: 134M
  • After: 116M

Breaking changes

Visual test projects now use native .NET 6 "Hot reload"

Over the years we have maintained our own version of hot reload, affectionately named "dynamic compilation". Even after multiple complete rewrites of the system, there are still edge cases where it will unexpectedly fall over due to being too greedy in including what it considers required for the recompile.

The startup cost when running in debug was considerable (around 10-30s initialisation) and the first-compile overhead could also be high (5-60s). In addition, the dependencies required to make it work increased the final assembly size of osu!framework, even for release deploys.

With the introduction of cross-platform support for hot reload in .NET 6 we have made the decision to switch to the natively supported version.

To use this new version:

  • From Rider, you'll get a popup that you have to click "Apply Changes" when a change is made to code while running. You can bind a key to Apply Hot Reload Changes in Rider (defaults to Alt+F10) to make it quicker to apply changes.
  • Running dotnet watch from CLI on your test project will automatically watch files and recompile when a hange is seen.

New limitations:

  • The limitations of hot reload are listed here.
  • Notably, adding new override methods or classes are not supported. You can workaround this for the common scenario of prototyping new components by creating subclasses and adding the override methods before the initial hot reload.

We are interested in hearing feedback on this change, especially troubling cases where the previous behaviour worked better for you. Hope is that the limitations of the new method are outweighed by the leaner assembly, better performance, and (in general) up-front error when a change can't be applied, rather than an error that can be delayed longer than it would take to run a full recompile/restart.

@peppy
Copy link
Member

peppy commented Feb 18, 2022

Honestly I think this is better than what we have, even with the limitations. When working on new components, you can even add override methods as long as they are done as part of the introduction of the new class (which is usually how we write them anyway).

I usually hit cmd-s to trigger a framework dynamic compile, so adjusting rider shortcuts to instead to a hot reload removes any manual overhead that may be incurred:

JetBrains Rider-EAP 2022-02-18 at 08 33 49

peppy
peppy previously approved these changes Feb 18, 2022
@smoogipoo
Copy link
Contributor Author

Eh, I disagree with the override sentiment. I don't usually do that when it comes to input for example.

But I don't think there is another way to resolve issues like #5039 without rethinking the current DCC and I just don't have time for that right now.

@smoogipoo
Copy link
Contributor Author

Merge at your discretion, I haven't removed the assembly dropdown in the test browser yet.

@peppy
Copy link
Member

peppy commented Feb 18, 2022

I still run into cases with the current dynamic compilation falls over, I'd say around 10-20% of the time I use it. I'd much rather know up front that it's not going to work, as the overhead of waiting for it to initialise then fail can be 15s (on a good PC) or over 1 minute (as reported by @frenzibyte).

#if NET6_0_OR_GREATER
using System.Reflection.Metadata;
using osu.Framework.Testing;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't ask about this newline

@smoogipoo smoogipoo removed the blocked label Feb 18, 2022
Copy link
Member

@frenzibyte frenzibyte left a comment

Choose a reason for hiding this comment

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

I've thoroughly tested this and it's working pretty amazingly so far for me.

To summarize the limits of it:

  • Can't add new classes.
  • Can't override methods.
  • Can't modify code in generic-typed classes.
  • Can't modify value of constant fields.

And I would say I'm fine with all these limits in favour of the performance of .NET Hot Reload.

Also added to the OP that this closes ppy/osu#9225 upon checking.


However, just to note, one thing I noticed while I was testing this is that apparently, applying this arbitrary change to BeatmapCardNormal while viewing TestSceneBeatmapCard results in InvalidProgramException: Common Language Runtime detected an invalid program:

diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
index 08befd5340..4d7792697b 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
@@ -168,8 +168,8 @@ private void load()
                                     Name = @"Bottom content",
                                     RelativeSizeAxes = Axes.X,
                                     AutoSizeAxes = Axes.Y,
-                                    Anchor = Anchor.BottomLeft,
-                                    Origin = Anchor.BottomLeft,
+                                    Anchor = Anchor.CentreLeft,
+                                    Origin = Anchor.CentreLeft,
                                     Children = new Drawable[]
                                     {
                                         idleBottomContent = new FillFlowContainer

I'm not yet sure why, nor do I have the time to look into it, so gonna leave it here for now.

@@ -29,7 +29,7 @@ namespace osu.Framework.Testing
{
[ExcludeFromDynamicCompile]
Copy link
Member

@frenzibyte frenzibyte Feb 18, 2022

Choose a reason for hiding this comment

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

This attribute is now no-op with the .NET Hot Reload implementation. I think we no longer need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, but I'm going to leave it as is for the time being (not even obsoleted), at least until one of us works on a new component and sees how it is in practice.

@smoogipoo
Copy link
Contributor Author

I can't repro the InvalidProgramException, is it 100% for you?

@frenzibyte
Copy link
Member

Yeah, perhaps it is a platform-specific issue? In fact, I can repro when changing anything in that class apparently. Here's the relevant logs with the exception stack trace from the patch I sent above, in case useful:

[runtime] 2022-02-18 11:32:09 [verbose]: 🔸 Step #2 Normal
[runtime] 2022-02-18 11:32:09 [verbose]: 💨 BeatmapCard(TestSceneBeatmapCard) Normal
[runtime] 2022-02-18 11:32:09 [verbose]: 🔸 Step #3 register request handling
[runtime] 2022-02-18 11:32:10 [verbose]: 🔸 Step #4 ensure manager loaded
[runtime] 2022-02-18 11:32:10 [verbose]: 🔸 Step #5 remove map
[runtime] 2022-02-18 11:32:10 [verbose]: 🔸 Step #6 set Red scheme
[runtime] 2022-02-18 11:32:10 [verbose]: 💥 Failed
[runtime] 2022-02-18 11:32:10 [verbose]: ⏳ Currently loading components (0)
[runtime] 2022-02-18 11:32:10 [verbose]: 🧵 Task schedulers
[runtime] 2022-02-18 11:32:10 [verbose]: LoadComponentsAsync (standard) concurrency:4 running:0 pending:0
[runtime] 2022-02-18 11:32:10 [verbose]: LoadComponentsAsync (long load) concurrency:4 running:0 pending:0
[runtime] 2022-02-18 11:32:10 [verbose]: 🎱 Thread pool
[runtime] 2022-02-18 11:32:10 [verbose]: worker:          min 8      max 32,767 available 32,766
[runtime] 2022-02-18 11:32:10 [verbose]: completion:      min 8      max 1,000  available 1,000
[runtime] 2022-02-18 11:32:10 [verbose]: Error on step: System.InvalidProgramException: Common Language Runtime detected an invalid program.
[runtime] 2022-02-18 11:32:10 [verbose]: at osu.Game.Beatmaps.Drawables.Cards.BeatmapCardNormal.<>c__DisplayClass16_0.<load>b__0(BeatmapCardContent c)
[runtime] 2022-02-18 11:32:10 [verbose]: at osu.Framework.Graphics.DrawableExtensions.With[T](T drawable, Action`1 adjustment) in /Users/salman/Desktop/osu-framework/osu.Framework/Graphics/DrawableExtensions.cs:line 23
[runtime] 2022-02-18 11:32:10 [verbose]: at osu.Game.Beatmaps.Drawables.Cards.BeatmapCardNormal.load()
[runtime] 2022-02-18 11:32:10 [verbose]: --- End of stack trace from previous location ---
[runtime] 2022-02-18 11:32:10 [verbose]: at osu.Framework.Allocation.BackgroundDependencyLoaderAttribute.<>c__DisplayClass6_1.<CreateActivator>b__4(Object target, IReadOnlyDependencyContainer dc) in /Users/salman/Desktop/osu-framework/osu.Framework/Allocation/BackgroundDependencyLoaderAttribute.cs:line 76
[runtime] 2022-02-18 11:32:10 [verbose]: at osu.Framework.Allocation.DependencyActivator.activate(Object obj, IReadOnlyDependencyContainer dependencies) in /Users/salman/Desktop/osu-framework/osu.Framework/Allocation/DependencyActivator.cs:line 75
[runtime] 2022-02-18 11:32:10 [verbose]: at osu.Framework.Allocation.DependencyActivator.Activate(Object obj, IReadOnlyDependencyContainer dependencies) in /Users/salman/Desktop/osu-framework/osu.Framework/Allocation/DependencyActivator.cs:line 50
[runtime] 2022-02-18 11:32:10 [verbose]: at osu.Framework.Allocation.DependencyContainer.Inject[T](T instance) in /Users/salman/Desktop/osu-framework/osu.Framework/Allocation/DependencyContainer.cs:line 186
[runtime] 2022-02-18 11:32:10 [verbose]: at osu.Framework.Graphics.Drawable.InjectDependencies(IReadOnlyDependencyContainer dependencies) in /Users/salman/Desktop/osu-framework/osu.Framework/Graphics/Drawable.cs:line 299
[runtime] 2022-02-18 11:32:10 [verbose]: at osu.Framework.Graphics.Containers.CompositeDrawable.InjectDependencies(IReadOnlyDependencyContainer dependencies) in /Users/salman/Desktop/osu-framework/osu.Framework/Graphics/Containers/CompositeDrawable.cs:line 82
[runtime] 2022-02-18 11:32:10 [verbose]: at osu.Framework.Graphics.Drawable.load(IFrameBasedClock clock, IReadOnlyDependencyContainer dependencies) in /Users/salman/Desktop/osu-framework/osu.Framework/Graphics/Drawable.cs:line 273
[runtime] 2022-02-18 11:32:10 [verbose]: at osu.Framework.Graphics.Drawable.Load(IFrameBasedClock clock, IReadOnlyDependencyContainer dependencies, Boolean isDirectAsyncContext) in /Users/salman/Desktop/osu-framework/osu.Framework/Graphics/Drawable.cs:line 252
[runtime] 2022-02-18 11:32:10 [verbose]: at osu.Framework.Graphics.Containers.CompositeDrawable.loadChild(Drawable child) in /Users/salman/Desktop/osu-framework/osu.Framework/Graphics/Containers/CompositeDrawable.cs:line 288
[runtime] 2022-02-18 11:32:10 [verbose]: at osu.Framework.Graphics.Containers.CompositeDrawable.load(ShaderManager shaders, Nullable`1 cancellation) in /Users/salman/Desktop/osu-framework/osu.Framework/Graphics/Containers/CompositeDrawable.cs:line 263

@peppy
Copy link
Member

peppy commented Feb 18, 2022

To summarize the limits of it:

  • Can't add new classes.

Notably, nested classes do work.

@bdach
Copy link
Collaborator

bdach commented Feb 18, 2022

Just going to note that this is going to require osu!-side changes too, because there is one reference to the dynamic class compiler that is no longer valid after these.

I was going to do some UI work this weekend anyway, so I'll give this a proper spin and see how it behaves. Generally mostly on board with this from the testimonials above, since I can corroborate that the existing setup was pretty slow and flakey for me too, so if this is both faster and has more consistent rules about what will and what won't reload cleanly then that's a big improvement in my eyes.

@bdach
Copy link
Collaborator

bdach commented Feb 18, 2022

Also, for other ideavim users that like to use ex mode, you can do:

:command R action RiderDebuggerApplyEncChagnes

(note - that's not a typo above, that's what it's actually called), and then you can do :R from ex mode to reload changes. I got used to doing :w to reload my changes, and that doesn't work even if I bind ctrl-s to hot reload.

@peppy
Copy link
Member

peppy commented Feb 19, 2022

How did you find that? I searched :actionlist about 5 times to no avail.

@bdach
Copy link
Collaborator

bdach commented Feb 19, 2022

I changed key mappings to bind hot reload to ctrl-s, and then opened the keymap config in a text editor and looked at the action ID there 😅

@bdach
Copy link
Collaborator

bdach commented Feb 20, 2022

So I've spent these two past days of UI dev with a local checkout of this branch. To be quite honest my feelings have cooled off quite a bit over these two days, because it's less of an improvement than I'd hoped it would be. Warning, essay incoming.

In general, yes the failures are more consistent in general, but with hot reload you can get away with a lot less than the previous setup would allow. When it works it works incredibly well, but when it doesn't, it forces a full rebuild - and I don't know if it's just me, but I don't remember those being as slow as I've found them this weekend.

It may be some .NET 6 thing, it may be due to having to build framework every time, it may also be due to me having resharper build fully off (because it sometimes will fail to invalidate some cache and I will see stale code running) - but either way, my full build times were about a minute every time. Which is worse in some cases than the previous state because while yes, the first dynamic compilation was always very slow, the following ones would be much quicker. So in a way, quick failures are preferable to slow surefire compilations, in some situations.

Also, sometimes on things that we have in framework, failures will be silent. Examples include: adding a new [Resolved] property to an existing class at runtime, or adding a new [Cached] property to an existing class at runtime; I've had those take no effect. I can understand this for sure - there is a very loose and indirection-y connection there, so the runtime can't possibly know what else it has to recompile, but it's unfortunate as it doesn't result in a loud error like all of the other surefire hot reload failures.

All that said, the previous setup was somewhat of a maintenace nightmare, and I've started adapting to hot reload somehow by running through a mental checklist of what I may need in advance, and then adding stubs for the initial compilation that I will then use with hot reload. I think once I get a handle on being better at that, this has the potential to be better than the previous state of affairs, but I'm not personally there yet. Unfortunately that doesn't hold for general "protectedness" and inheritance usage that we have a lot of in framework and game - there a recompilation hit will need to happen almost every time.

In summary I still think it's better to merge this and test this over a longer period of time. At worst we can revert to the previous state of affairs; at best, after some adapting this may turn out to be better than what we had. And given that this solution has .NET platform buy-in, maybe hot reload can itself also improve over time as the .NET team works on it?

@jai-x
Copy link
Member

jai-x commented Feb 20, 2022

Just adding in that dotnet watch has some (poorly) documented config options for excluding files and references from watch, which might result in compile speedup for certain cases: https://docs.microsoft.com/en-us/aspnet/core/tutorials/dotnet-watch?view=aspnetcore-6.0#opt-out-of-files-to-be-watched

@@ -21,7 +21,6 @@
<ItemGroup Label="Code Analysis">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0" PrivateAssets="All" />
Copy link
Contributor Author

@smoogipoo smoogipoo Feb 21, 2022

Choose a reason for hiding this comment

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

I'm not sure about this one. Can someone without .NET 7 SDK installed try undoing this change and testing with dotnet watch specifically?

Copy link
Member

Choose a reason for hiding this comment

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

Works fine for me

~/Projects
❯ dotnet --version
6.0.200


[performance] 2022-02-21 03:54:12 [verbose]: TextureAtlas initialised (1024x1024)
[runtime] 2022-02-21 03:54:13 [verbose]: 🔸 Step #2 HardwareDecode
[runtime] 2022-02-21 03:54:13 [verbose]: 💨 Video(TestSceneVideo) HardwareDecode
[runtime] 2022-02-21 03:54:13 [verbose]: 🔸 Step #3 Reset clock
watch : File changed: /Users/dean/Projects/osu-framework/osu.Framework.Tests/Visual/Sprites/TestSceneVideo.cs.
watch : Hot reload of changes succeeded.
[runtime] 2022-02-21 03:54:28 [verbose]: 🔸 Step #2 HardwareDecode
[runtime] 2022-02-21 03:54:28 [verbose]: 💨 Video(TestSceneVideo) HardwareDecode
[runtime] 2022-02-21 03:54:28 [verbose]: 🔸 Step #3 Reset clock
watch : File changed: /Users/dean/Projects/osu-framework/osu.Framework.Tests/Visual/Sprites/TestSceneVideo.cs.
watch : Hot reload of changes succeeded.
[runtime] 2022-02-21 03:54:35 [verbose]: 🔸 Step #2 HardwareDecode
[runtime] 2022-02-21 03:54:35 [verbose]: 💨 Video(TestSceneVideo) HardwareDecode
[runtime] 2022-02-21 03:54:35 [verbose]: 🔸 Step #3 Reset clock

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, will just have to revert this then, and I'll uninstall .NET 7 locally.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems like this package is not required since .NET 5: https://docs.microsoft.com/en-us/visualstudio/code-quality/migrate-from-fxcop-analyzers-to-net-analyzers?view=vs-2022, so should be okay to leave it removed as per this PR.

@peppy peppy enabled auto-merge February 21, 2022 04:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
5 participants