PerformanceWarningTracer crash on large view models (DotVVM 5.0.1)

Hi again team, my ‘error logs’ page which might be a sizeable viewmodel threw an error upon the DotVVM 5.0.1 update and claude code again helped provide a work around along with the below information which hopefully makes sense and might help address it. Here’s the details:

PerformanceWarningTracer crashes the whole request with “The stream factory function is being invoked too late” on any view model >= 5MB (DotVVM 5.0.1)

Environment

  • DotVVM: 5.0.1
  • DotVVM.Controls.Bootstrap4: 5.0.0
  • DotVVM.Contrib.Select2 / LoadablePanel / BootstrapColorpicker: 5.0.0
  • DotVVM.HotReload.AspNetCore: 5.0.1
  • Target framework: net10.0
  • .NET SDK: 10.0.400
  • Hosting: ASP.NET Core (Microsoft.NET.Sdk.Web)
  • config.Diagnostics.PerfWarnings.IsEnabled at its default (true), BigViewModelBytes at its default (5 MB)

Summary

Any request whose serialized view model is at or above the BigViewModelBytes threshold (default 5 MB) throws instead of just logging the intended “large view model” performance warning:

System.InvalidOperationException: The stream factory function is being invoked too late.

at DotVVM.Framework.Runtime.Tracing.RequestTracingExtensions.<>c__DisplayClass1_0.<TracingSerialized>b__0()

at DotVVM.Framework.Diagnostics.PerformanceWarningTracer.ViewModelSerialized(IDotvvmRequestContext context, Int32 viewModelSize, Func`1 viewModelBuffer)

at DotVVM.Framework.Runtime.Tracing.RequestTracingExtensions.TracingSerialized(IEnumerable`1 requestTracers, IDotvvmRequestContext context, Int32 viewModelSize, MemoryStream stream)

at DotVVM.Framework.ViewModel.Serialization.DefaultViewModelSerializer.SerializeViewModel(IDotvvmRequestContext context, Object commandResult, IEnumerable`1 postbackUpdatedControls, Boolean serializeNewResources)

at DotVVM.Framework.Hosting.DotvvmPresenter.ProcessRequestCore(IDotvvmRequestContext context)

We hit this loading a report page with a moderately large list (a few thousand rows), which pushed the serialized view model over 5 MB - but the bug isn’t specific to that page or to any particular data shape, just to view model size.

Root cause

RequestTracingExtensions.TracingSerialized (src/Framework/Framework/Runtime/Tracing/RequestTracingExtensions.cs):

public static void TracingSerialized(this IEnumerable<IRequestTracer> requestTracers, IDotvvmRequestContext context, int viewModelSize, MemoryStream stream)

{

bool calledSynchronously = false;

foreach (var tracer in requestTracers)

{

tracer.ViewModelSerialized(context, viewModelSize, () => {

if (!calledSynchronously) // make sure we can optimize this later to use buffers from ArrayPool

throw new InvalidOperationException("The stream factory function is being invoked too late.");

return stream.CloneReadOnly();

});

}

calledSynchronously = true; // <-- set AFTER the loop finishes

}

calledSynchronously is only set to true after the foreach loop over all tracers completes. But PerformanceWarningTracer.ViewModelSerialized (src/Framework/Framework/Diagnostics/PerformanceWarningTracer.cs) calls the factory synchronously, from inside that same loop, whenever the view model is big enough to warrant a warning:

public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Func<Stream> viewModelBuffer)

{

if (viewModelSize >= config.BigViewModelBytes)

{

WarnLargeViewModel(context, viewModelSize, viewModelBuffer()); // <-- calls the factory right here, inside TracingSerialized's loop

}

}

At the moment viewModelBuffer() is invoked, calledSynchronously is still false (the loop hasn’t finished, so line calledSynchronously = true; hasn’t run yet) - so the guard’s if (!calledSynchronously) check trips and throws, even though the factory genuinely is being called synchronously, exactly as the comment on the guard intends to allow. The flag is just set one statement too late relative to where it’s read.

This means the “large view model” warning - a diagnostics-only feature meant to log a warning - instead throws an unhandled exception that aborts the entire request whenever it would have fired. Anything that pushes a view model over BigViewModelBytes (default 5 MB) breaks completely, with no way to keep the warning without also breaking the request.

Minimal repro

Any DotVVM view model whose serialized JSON is >= config.Diagnostics.PerfWarnings.BigViewModelBytes (5 MB by default) will hit this on every request. For example, a GridViewDataSet/list-backed view model with enough rows to push the payload past 5 MB, with default Diagnostics.PerfWarnings configuration (enabled, default threshold).

Workaround

Disable the tracer entirely until this is fixed:

config.Diagnostics.PerfWarnings.IsEnabled = false;

This is a bit of a shame since it also disables the (working) slow-request warning, not just the buggy large-view-model one - there’s no way to disable just the view-model-size check without disabling the whole tracer.

Suggested fix

Move calledSynchronously = true; to before the foreach loop (or, better, restructure so each tracer’s synchronous call window is tracked correctly per-invocation rather than with one flag shared across the whole loop) - the intent seems to be “the factory can only be called while ViewModelSerialized is still on the stack for this tracer”, and the flag should reflect that per-call, not “before/after the entire loop over all tracers”.

I created an issue PerformanceWarningTracer crash on large view models (DotVVM 5.0.1) · Issue #2109 · riganti/dotvvm · GitHub for this. In the future, please file bugs directly on GitHub, it is easier for us to track.