Skip to content

Commit 7bc0ffb

Browse files
committed
Make XAML hot reload JIT on UI load
This means we don't have to JIT a bunch of UIs that you might not open, reducing memory usage and startup overhead. One (1) UI is always JITed in another thread before prototype UIs are loaded, so as to warm up the JIT machinery. Said type is DropDownDebugConsole which always gets used anyways so there's no harm in it. In total, these changes save more than a second of startup time for me.
1 parent e495159 commit 7bc0ffb

File tree

6 files changed

+71
-10
lines changed

6 files changed

+71
-10
lines changed

RELEASE-NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ END TEMPLATE-->
6565
* ComponentNameSerializer will now ignore any components that have been ignored via `IComponentFactory.RegisterIgnore`.
6666
* Add pure to some SharedTransformSystem methods.
6767
* `Control.Stylesheet` does not do any work if assigning the value it already has.
68+
* XAML hot reload now JITs UIs when first opened rather than doing every single one at client startup. This reduces dev startup overhead significantly and probably helps with memory usage too.
6869

6970
### Internal
7071

Robust.Client/GameController/GameController.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,12 @@ internal bool StartupContinue(DisplayMode displayMode)
216216

217217
_loadscr.LoadingStep(_reload.Initialize, _reload);
218218
_loadscr.LoadingStep(_reflectionManager.Initialize, _reflectionManager);
219+
_loadscr.LoadingStep(_xamlProxyManager.Initialize, _xamlProxyManager);
220+
_loadscr.LoadingStep(_xamlHotReloadManager.Initialize, _xamlHotReloadManager);
219221
_loadscr.BeginLoadingSection(_prototypeManager);
220222
_prototypeManager.Initialize();
221223
_prototypeManager.LoadDefaultPrototypes();
222224
_loadscr.EndLoadingSection();
223-
_loadscr.LoadingStep(_xamlProxyManager.Initialize, _xamlProxyManager);
224-
_loadscr.LoadingStep(_xamlHotReloadManager.Initialize, _xamlHotReloadManager);
225225
_loadscr.LoadingStep(_userInterfaceManager.Initialize, "UI init");
226226
_loadscr.LoadingStep(_eyeManager.Initialize, _eyeManager);
227227
_loadscr.LoadingStep(_entityManager.Initialize, _entityManager);

Robust.Client/UserInterface/UserInterfaceManager.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ namespace Robust.Client.UserInterface
2929
{
3030
internal sealed partial class UserInterfaceManager : IUserInterfaceManagerInternal
3131
{
32+
/// <summary>
33+
/// A type that will always be instantiated anyways.
34+
/// </summary>
35+
public static readonly Type XamlHotReloadWarmupType = typeof(DropDownDebugConsole);
36+
3237
[Dependency] private readonly IDependencyCollection _rootDependencies = default!;
3338
[Dependency] private readonly IInputManager _inputManager = default!;
3439
[Dependency] private readonly IFontManager _fontManager = default!;

Robust.Client/UserInterface/XAML/Proxy/XamlImplementationStorage.cs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
using System;
33
using System.Collections.Generic;
44
using System.Reflection;
5+
using System.Threading;
56
using Robust.Shared.Log;
7+
using Robust.Shared.Timing;
8+
using Robust.Shared.Utility;
69
using Robust.Xaml;
710

811
namespace Robust.Client.UserInterface.XAML.Proxy;
@@ -36,6 +39,8 @@ internal sealed class XamlImplementationStorage
3639
/// </summary>
3740
private readonly Dictionary<string, Type> _fileType = new();
3841

42+
private readonly Dictionary<Type, string> _fileTypeReverse = new();
43+
3944
/// <summary>
4045
/// For each type, store the JIT-compiled implementation of Populate.
4146
/// </summary>
@@ -50,6 +55,8 @@ internal sealed class XamlImplementationStorage
5055
private readonly ISawmill _sawmill;
5156
private readonly XamlJitDelegate _jitDelegate;
5257

58+
private readonly Lock _compileLock = new();
59+
5360
/// <summary>
5461
/// Create the storage.
5562
/// </summary>
@@ -102,6 +109,8 @@ public XamlImplementationStorage(ISawmill sawmill, XamlJitDelegate jitDelegate)
102109
/// <param name="assembly">an assembly</param>
103110
public void Add(Assembly assembly)
104111
{
112+
using var _ = _compileLock.EnterScope();
113+
105114
foreach (var (type, metadata) in TypesWithXamlMetadata(assembly))
106115
{
107116
// this can fail, but if it does, that means something is _really_ wrong
@@ -132,6 +141,8 @@ public void Add(Assembly assembly)
132141
$"{fileName}. ({type.FullName} and {_fileType[fileName].FullName}). this is a bug in XamlAotCompiler"
133142
);
134143
}
144+
145+
_fileTypeReverse.Add(type, fileName);
135146
}
136147
}
137148

@@ -145,6 +156,8 @@ public void Add(Assembly assembly)
145156
/// </remarks>
146157
public void ForceReloadAll()
147158
{
159+
using var _ = _compileLock.EnterScope();
160+
148161
foreach (var (fileName, fileContent) in _fileContent)
149162
{
150163
SetImplementation(fileName, fileContent, true);
@@ -161,9 +174,19 @@ public void ForceReloadAll()
161174
/// <returns>true if not a no-op</returns>
162175
public bool CanSetImplementation(string fileName)
163176
{
177+
using var _ = _compileLock.EnterScope();
164178
return _fileType.ContainsKey(fileName);
165179
}
166180

181+
public MethodInfo? CompileType(Type type)
182+
{
183+
if (_fileTypeReverse.TryGetValue(type, out var fileName))
184+
return SetImplementation(fileName, _fileContent[fileName], quiet: true);
185+
186+
_sawmill.Warning($"Type {type} has no XAML file!");
187+
return null;
188+
}
189+
167190
/// <summary>
168191
/// Replace the implementation of <paramref name="fileName"/> by JIT-ing
169192
/// <paramref name="fileContent"/>.
@@ -174,12 +197,14 @@ public bool CanSetImplementation(string fileName)
174197
/// <param name="fileName">the name of the file whose implementation should be replaced</param>
175198
/// <param name="fileContent">the new implementation</param>
176199
/// <param name="quiet">if true, then don't bother to log</param>
177-
public void SetImplementation(string fileName, string fileContent, bool quiet)
200+
public MethodInfo? SetImplementation(string fileName, string fileContent, bool quiet)
178201
{
202+
using var _ = _compileLock.EnterScope();
203+
179204
if (!_fileType.TryGetValue(fileName, out var type))
180205
{
181206
_sawmill.Warning($"SetImplementation called with {fileName}, but no types care about its contents");
182-
return;
207+
return null;
183208
}
184209

185210
var uri =
@@ -190,12 +215,14 @@ public void SetImplementation(string fileName, string fileContent, bool quiet)
190215
{
191216
_sawmill.Debug($"replacing {fileName} for {type}");
192217
}
218+
193219
var impl = _jitDelegate(type, uri, fileName, fileContent);
194220
if (impl != null)
195221
{
196222
_populateImplementations[type] = impl;
197223
}
198224
_fileContent[fileName] = fileContent;
225+
return impl;
199226
}
200227

201228
/// <summary>
@@ -210,8 +237,12 @@ public bool Populate(Type t, object o)
210237
{
211238
if (!_populateImplementations.TryGetValue(t, out var implementation))
212239
{
213-
// pop out if we never JITed anything
214-
return false;
240+
// JIT if needed.
241+
implementation = CompileType(t);
242+
243+
// pop out if we never JITed anything/couldn't JIT
244+
if (implementation == null)
245+
return false;
215246
}
216247

217248
implementation.Invoke(null, [null, o]);

Robust.Client/UserInterface/XAML/Proxy/XamlProxyManager.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
using System;
33
using System.Collections.Generic;
44
using System.Reflection;
5+
using System.Threading;
6+
using Robust.Shared;
7+
using Robust.Shared.Configuration;
58
using Robust.Shared.IoC;
69
using Robust.Shared.Log;
710
using Robust.Shared.Reflection;
@@ -17,6 +20,7 @@ public sealed class XamlProxyManager: IXamlProxyManager
1720
ISawmill _sawmill = null!;
1821
[Dependency] IReflectionManager _reflectionManager = null!;
1922
[Dependency] ILogManager _logManager = null!;
23+
[Dependency] private readonly IConfigurationManager _cfg = null!;
2024

2125
XamlImplementationStorage _xamlImplementationStorage = null!;
2226

@@ -31,8 +35,21 @@ public void Initialize()
3135
_sawmill = _logManager.GetSawmill("xamlhotreload");
3236
_xamlImplementationStorage = new XamlImplementationStorage(_sawmill, Compile);
3337

34-
AddAssemblies();
38+
var preload = _cfg.GetCVar(CVars.UIXamlJitPreload);
39+
40+
AddAssemblies(reload: preload);
3541
_reflectionManager.OnAssemblyAdded += (_, _) => { AddAssemblies(); };
42+
43+
if (!preload)
44+
{
45+
// Compile any type at all on another thread, so we don't hold up main thread init with loading
46+
// the entire XAML compiler machinery.
47+
// In my testing, it took like 0.5s on debug to run the first XAML compile. Yeah.
48+
ThreadPool.QueueUserWorkItem(_ =>
49+
{
50+
_xamlImplementationStorage.CompileType(UserInterfaceManager.XamlHotReloadWarmupType);
51+
});
52+
}
3653
}
3754

3855
/// <summary>
@@ -61,7 +78,7 @@ public void SetImplementation(string fileName, string fileContent)
6178
/// Add all the types from all known assemblies, then force-JIT everything
6279
/// again.
6380
/// </summary>
64-
private void AddAssemblies()
81+
private void AddAssemblies(bool reload = true)
6582
{
6683
foreach (var a in _reflectionManager.Assemblies)
6784
{
@@ -74,8 +91,8 @@ private void AddAssemblies()
7491
}
7592
}
7693

77-
// Always use the JITed versions on debug builds
78-
_xamlImplementationStorage.ForceReloadAll();
94+
if (reload)
95+
_xamlImplementationStorage.ForceReloadAll();
7996
}
8097

8198
/// <summary>

Robust.Shared/CVars.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1925,6 +1925,13 @@ internal static readonly CVarDef<string>
19251925
public static readonly CVarDef<string> XamlHotReloadMarkerName =
19261926
CVarDef.Create("ui.xaml_hot_reload_marker_name", "SpaceStation14.sln", CVar.CLIENTONLY);
19271927

1928+
/// <summary>
1929+
/// If true, all XAML UIs will be JITed for hot reload on client startup.
1930+
/// If false, they will be JITed on demand.
1931+
/// </summary>
1932+
public static readonly CVarDef<bool> UIXamlJitPreload =
1933+
CVarDef.Create("ui.xaml_jit_preload", false, CVar.CLIENTONLY);
1934+
19281935
/*
19291936
* FONT
19301937
*/

0 commit comments

Comments
 (0)