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

NullReferenceExceptions randomly thrown in HandleSizeAllocated when modifying table #2181

Closed
Ararem opened this issue Apr 7, 2022 · 4 comments

Comments

@Ararem
Copy link
Contributor

Ararem commented Apr 7, 2022

Expected Behavior

No exceptions are thrown

Actual Behavior

System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
---> System.NullReferenceException: Object reference not set to an instance of an object.
at void Eto.GtkSharp.Forms.GtkControl<TControl, TWidget, TCallback>+GtkControlConnector.HandleSizeAllocated(object o, SizeAllocatedArgs args)
--- End of inner exception stack trace ---
at object RuntimeMethodHandle.InvokeMethod(object target, in Span<object> arguments, Signature sig, bool constructor, bool wrapExceptions)
at object System.Reflection.RuntimeMethodInfo.Invoke(object obj, BindingFlags invokeAttr, Binder binder, object[] parameters, CultureInfo culture)
at object Delegate.DynamicInvokeImpl(object[] args)
at void GLib.Signal.ClosureInvokedCB(object o, ClosureInvokedArgs args)
at void GLib.SignalClosure.MarshalCallback(IntPtr raw_closure, IntPtr return_val, uint n_param_vals, IntPtr param_values, IntPtr invocation_hint, IntPtr marshal_data)

Steps to Reproduce the Problem

(Specific to my project)

  1. Call UpdateStatsTable()
  2. Wait some time (anywhere from instantly to half a minute)
  3. See that errors are being thrown and logged

Context:

I'm working on a RayTracer, and I'm making a realtime display for the progress of the render. I have a task running on a worker thread that intermittently updates the UI.

private async Task UpdatePreviewWorker()
{
       while(true)
       {
			await Application.Instance.InvokeAsync(Update);
			await Task.Delay(1000);
	}

	void Update()
	{
		UpdateImagePreview();
		UpdateStatsTable();

		Invalidate();
		Verbose("Invalidated for redraw");
	}
}

Whenever UpdateStatsTable() is called, the error starts to appear. Removing the call seems to remove all errors.
The error occurs when the app is running (not long after Start Render is clicked - see full project). No interaction is performed (no mouse, no resizing, no minimizing etc). The issue seems to happen more consistently when i maximize/resize the app window. See comment below, I think it's the calls to table.Add(...). Also only happens when resizing the window, I think before my logs were just getting delayed due to the sheer amount of errors and the console not being fast enough

Code that Demonstrates the Problem

It's a full project, so you might want to find it here, but I'll put the UpdateStatsTable code here

private void UpdateStatsTable()
{
	const string timeFormat     = "h\\:mm\\:ss"; //Format string for Timespan
	const string dateTimeFormat = "G";           //Format string for DateTime
	const string percentFormat  = "p1";          //Format string for percentages
	const string numFormat      = "n0";
	const int    numAlign       = 15;
	const int    percentAlign   = 8;
	int           totalTruePixels = renderJob.TotalTruePixels;
	ulong         totalRawPix     = renderJob.TotalRawPixels;
	ulong         rayCount        = renderJob.RayCount;
	RenderOptions options         = renderJob.RenderOptions;
	int           totalPasses     = options.Passes;
	TimeSpan      elapsed         = renderJob.Stopwatch.Elapsed;
	float    percentageRendered = (float)renderJob.RawPixelsRendered / totalRawPix;
	ulong    rawPixelsRemaining = totalRawPix - renderJob.RawPixelsRendered;
	int      passesRemaining    = totalPasses - renderJob.PassesRendered;
	TimeSpan estimatedTotalTime;
	//If the percentage rendered is very low, the division results in a number that's too large to fit in a timespan, which throws
	try
	{
		estimatedTotalTime = elapsed / percentageRendered;
	}
	catch (OverflowException)
	{
		estimatedTotalTime = TimeSpan.FromDays(69.420); //If something's broke at least let me have some fun
	}
	Verbose("Updating stats table");
	Stopwatch stop = Stopwatch.StartNew();
	(string Title, string[] Values)[] stats =
	{
			("Time", new[]
			{
					$"{elapsed.ToString(timeFormat),numAlign} elapsed",
					$"{(estimatedTotalTime - elapsed).ToString(timeFormat),numAlign} remaining",
					$"{estimatedTotalTime.ToString(timeFormat),numAlign} total",
					$"{(DateTime.Now + (estimatedTotalTime - elapsed)).ToString(dateTimeFormat),numAlign} ETC"
			}),
			("Pixels", new[]
			{
					$"{FormatU(renderJob.RawPixelsRendered, totalRawPix)} rendered",
					$"{FormatU(rawPixelsRemaining,          totalRawPix)} remaining",
					$"{totalRawPix.ToString(numFormat),numAlign}          total"
			}),
			("Image", new[]
			{
					$"{totalTruePixels.ToString(numFormat),numAlign}          pixels total",
					$"{options.Width.ToString(numFormat),numAlign}          pixels wide",
					$"{options.Height.ToString(numFormat),numAlign}          pixels high"
			}),
			("Passes", new[]
			{
					$"{FormatI(renderJob.PassesRendered, totalPasses)} rendered",
					$"{FormatI(passesRemaining,          totalPasses)} remaining",
					$"{totalPasses.ToString(numFormat),numAlign}          total"
			}),
			("Rays", new[]
			{
					$"{FormatU(renderJob.RaysScattered,       rayCount)} scattered",
					$"{FormatU(renderJob.RaysAbsorbed,        rayCount)} absorbed",
					$"{FormatU(renderJob.BounceLimitExceeded, rayCount)} exceeded",
					$"{FormatU(renderJob.SkyRays,             rayCount)} sky",
					$"{rayCount.ToString(numFormat),numAlign}          total"
			}),
			("Scene", new[]
			{
					$"Name:		{renderJob.Scene.Name}",
					$"Obj Count:	{renderJob.Scene.Objects.Length}",
					$"Camera:		{renderJob.Scene.Camera}",
					$"SkyBox:		{renderJob.Scene.SkyBox}"
			})
	};
	//Due to how the table is implemented, I can't rescale it later
	//So if the size doesn't match our array, we need to recreate it
	if (statsTable.Dimensions.Height != stats.Length)
	{
		statsTable             = new TableLayout(2, stats.Length) { ID = "Stats Table" };
		statsContainer.Content = statsTable;
	}
	for (int i = 0; i < stats.Length; i++)
	{
		(string? title, string[]? strings) = stats[i];
		string values = StringBuilderPool.BorrowInline(static (sb, vs) => sb.AppendJoin(Environment.NewLine, vs), strings);
		statsTable.Add(title,  0, i);
		statsTable.Add(values, 1, i);
	}
	Verbose("Finished updating stats in {Elapsed}", stop.Elapsed);
	static string FormatU(ulong val, ulong total)
	{
		return $"{val.ToString(numFormat),numAlign} {'(' + ((float)val / total).ToString(percentFormat) + ')',percentAlign}";
	}
	static string FormatI(int val, int total)
	{
		return $"{val.ToString(numFormat),numAlign} {'(' + ((float)val / total).ToString(percentFormat) + ')',percentAlign}";
	}
}

The code that seems to be throwing the exception is:

//From Eto.GtkSharp.Forms.GtkControl[TControl, TWidget, TCallback].GtkControlConnector.HandleSizeAllocated
public void HandleSizeAllocated(object o, SizeAllocatedArgs args)
{
    if (!(this.Handler.asize != args.Allocation.Size.ToEto()))
        return;
    this.Handler.asize = args.Allocation.Size.ToEto();
    this.Handler.Callback.OnSizeChanged((Control) this.Handler.Widget, EventArgs.Empty);
}

When I was debugging at ClosureInvokedCB, I managed to find out it's calling HandleSizeAllocated() on a GtkControl<LabelHandler.EtoLabel, Label, TextControl.ICallback>.GtkControlConnector. The problem is that the GtkControlConnector has a .Handler property set to null, so that when it tries to access this.Handler.asize it gets a NullReferenceException because this.Handler is null. This exception then gets propagated back to my code. I'm not really sure how to fix this since the methods are being called from native code, sorry.

Specifications

  • Version: Eto.Forms 2.6.1
  • Platform(s): Gtk (Haven't tried others), Net 6.0
  • Operating System(s): Pop OS 21.10
@Ararem
Copy link
Contributor Author

Ararem commented Apr 8, 2022

New update; It's something to do with the for loop that adds the cells to the table. Whenever I comment the statsTable.Add(...) lines the issue goes away, so I think that's the root of the issue

@cwensley
Copy link
Member

Hey @EternalClickbait, this is likely due to creating/destroying many labels often, where Gtk is still sending events to some of them after they have been garbage collected on the .NET side. That method need to do what (most) of the others do, where it bails if the Handler is null, such as:

	var handler = Handler;
	if (handler == null)
		return;

@cwensley
Copy link
Member

cwensley commented Apr 12, 2022

One way that might get around this is to always create a new TableLayout, and only call statsContainer.Content = statsTable; after it is fully created. I have not tested this though.

A more efficient way would be to keep references to each of the labels and update their text instead of recreating this table and the labels each time.

@Ararem
Copy link
Contributor Author

Ararem commented Apr 14, 2022

Yeah I switched to modifying the table each tick instead of recreating the labels (unless necessary). I also added a load of calls to Dispose to ensure that everything was disposed properly. The issue seems to be fixed now, so I'll close it.

What Solved It

  1. Not creating a new Label every update
  2. Disposing of old controls and other objects when removing them from the table

@Ararem Ararem closed this as completed Apr 14, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants