Editor/Inspector/InfiniteLoopInspectorWidget.cs
#if DEBUG
using Sandbox.Reactivity.Internals.Runtimes;
using Margin = Sandbox.UI.Margin;
namespace Sandbox.Reactivity.Editor.Inspector;
// ReSharper disable once UnusedType.Global
[Inspector(typeof(InfiniteLoopException))]
internal sealed class InfiniteLoopInspectorWidget : InspectorWidget
{
private readonly InfiniteLoopException _exception;
private readonly Layout _rootLayout;
static InfiniteLoopInspectorWidget()
{
Runtime.OnFlushInfiniteLoop += e => Log.Error($"{e} - click to inspect");
}
public InfiniteLoopInspectorWidget(SerializedObject so)
: base(so)
{
_exception = so.Targets.OfType<InfiniteLoopException>().First();
Layout = new Column
{
Children =
[
new ScrollArea(this)
{
Canvas = new Widget
{
HorizontalSizeMode = SizeMode.Flexible,
Layout = _rootLayout = new Column(),
},
},
],
};
Rebuild();
}
[EditorEvent.Hotload]
private void Rebuild()
{
_rootLayout.Clear(true);
_rootLayout.Children =
[
new Label
{
Margin = 8,
WordWrap = true,
TextColor = Theme.TextControl.WithAlpha(0.7f),
Text =
"An infinite loop occurred while flushing effects at the end of a fixed update.<br/><br/>This usually means an effect is both reading and writing to the same reactive value. Inspect your effects for any unintended dependencies.",
},
new InspectorHeader
{
Title = "Effect Runs",
IsCollapsable = false,
AutoBuild = true,
},
];
if (_exception.EffectExecutions is { Count: > 0 })
{
foreach (var (effect, executions) in _exception.EffectExecutions.OrderByDescending(x => x.Value))
{
var color = executions switch
{
> 900 => Color.Red.Desaturate(0.33f),
> 500 => Color.Orange,
> 100 => Color.Yellow,
_ => Theme.TextControl,
};
var controlWidget = ControlWidget.Create(new SerializedReactiveObjectProperty(effect));
controlWidget.ToolTip = null;
controlWidget.TextColor = Color.White;
_rootLayout.Add(new Widget
{
Cursor = CursorShape.Finger,
ToolTip =
$"This effect ran <span style='font-weight: 600; color: {color.ToString(false, true)}'>{executions:N0}</span> time{(executions != 1 ? "s" : "")} during a reactivity flush.",
MouseClick = () => EditorUtility.InspectorObject = effect,
MouseRightClick = () =>
{
var menu = new ContextMenu();
controlWidget.OnLabelContextMenu(menu);
menu.OpenAtCursor();
},
Layout = new Row
{
Margin = new Margin(6, 4, 6, 0),
Spacing = 4,
Children =
[
new Label
{
Text = $"x{executions:N0}",
FixedWidth = 60f,
TextColor = color,
},
controlWidget,
],
},
});
}
}
_rootLayout.AddStretchCell();
}
}
#endif