spectre.console icon indicating copy to clipboard operation
spectre.console copied to clipboard

Markup is not properly rendered (when chunked) while a status is displayed

Open 0xced opened this issue 4 years ago • 13 comments

Information

  • OS: macOS 10.15.7
  • Version: 0.42.0
  • Terminal: macOS Terminal version 2.10 (433)

Describe the bug While a status is being displayed, markup may not be fully rendered. It works fine with a single call to console.MarkupLine() but several calls to console.Markup() followed by a call to console.MarkupLine("") renders only the last chunk.

To Reproduce Here's a minimal sample code that demonstrates the issue:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Spectre.Console" Version="0.42.0" />
  </ItemGroup>

</Project>
using System;
using System.Linq;
using System.Threading.Tasks;
using Spectre.Console;

var console = AnsiConsole.Console;
try
{
    var totalDuration = TimeSpan.FromSeconds(4);
    const int maxStep = 10;

    var task = console.Status().StartAsync("Performing a long running operation...", async _ => await Task.Delay(totalDuration / 2));

    var split = args.Contains("--split");
    foreach (var i in Enumerable.Range(1, maxStep))
    {
        var chunks = new[]
        {
            $"[grey][[[/][silver]{DateTime.Now:hh:mm:ss.ff}[/][grey]]][/] ",
            "Step ",
            $"[olive]{i,2}[/]",
            " / ",
            $"[purple]{maxStep}[/]"
        };

        if (split)
        {
            foreach (var chunk in chunks)
            {
                console.Markup(chunk);
            }
            console.MarkupLine("");
        }
        else
        {
            console.MarkupLine(string.Join("", chunks));
        }

        await Task.Delay(totalDuration / maxStep);
    }

    await task;
}
catch (Exception exception)
{
    console.WriteException(exception);
}

Then run the program with dotnet run -- --split

Expected behavior

The 10 steps should be fully printed, but step 2 to 5 are not properly rendered.

Screenshots Here's a screen recording of running the sample app with dotnet run (executing a single console.MarkupLine()) and dotnet run -- --split (executing several calls to console.Markup())

Additional context I'm currently trying to port Serilog.Sinks.Console to use Spectre.Console and writing to the console in several chunks is pretty much required in that situation.


Please upvote :+1: this issue if you are interested in it.

0xced avatar Oct 05 '21 21:10 0xced

Think you are running into a threading issue, not an issue with markup. The status is running side-by-side with the rendering which isn't supported out of the box.

Combining the two would work though, something like this

var console = AnsiConsole.Console;
try
{
    var totalDuration = TimeSpan.FromSeconds(4);
    const int maxStep = 10;

    await console.Status().StartAsync("Performing a long running operation...", async _ =>
    {
        
        var split = args.Contains("--split");
        foreach (var i in Enumerable.Range(1, maxStep))
        {
            var chunks = new[]
            {
                $"[grey][[[/][silver]{DateTime.Now:hh:mm:ss.ff}[/][grey]]][/] ",
                "Step ",
                $"[olive]{i,2}[/]",
                " / ",
                $"[purple]{maxStep}[/]"
            };

            if (split)
            {
                foreach (var chunk in chunks)
                {
                    console.Markup(chunk);
                }
                console.MarkupLine("");
            }
            else
            {
                console.MarkupLine(string.Join("", chunks));
            }

            await Task.Delay(totalDuration / maxStep);
        }
        
        await Task.Delay(totalDuration / 2);
    });
}
catch (Exception exception)
{
    console.WriteException(exception);
}

phil-scott-78 avatar Oct 05 '21 21:10 phil-scott-78

Hmmm, I just tried your suggestion but that doesn't solve the rendering issue at all. Here's the result that I get when running the chunked version (--split):

[11:50:24.92] Step  1 / 10
10                                                    
10                                                    
10                                                    
10                                                    
10                                                    
10                                                    
10                                                    
10                                                    
10

0xced avatar Oct 05 '21 21:10 0xced

just had a chance to get on my pc, and you are right. at least it consently isn't working though rather than failing half the time.

Curious issue. Seems the same thing happens to Write too so it isn't just the markup parsing or anything like that.

phil-scott-78 avatar Oct 05 '21 22:10 phil-scott-78

@0xced @phil-scott-78 I suspect that we're not locking the console properly when rendering and that we're only locking the render pipeline. I can take a look at this tomorrow evening.

patriksvensson avatar Oct 05 '21 22:10 patriksvensson

Actually, I think that's because the cursor is periodically repositioned while the status is active and it looks like this is problematic on a line that has not (yet) been terminated by a newline.

0xced avatar Oct 05 '21 22:10 0xced

@0xced That is part of the LiveRenderable that hooks up to the render pipeline which is synchronized so shouldn't affect interlocking calls. Need to investigate if there is any call NOT using the render pipeline, or if we need to lock the console.

patriksvensson avatar Oct 05 '21 22:10 patriksvensson

I had a few moments tonight after the kiddo went to bed. Simplified the demo to something simpler

var console = AnsiConsole.Console;
const int maxStep = 10;

await console.Status().AutoRefresh(false).StartAsync("Performing a long running operation...", async ctx =>
{
    foreach (var i in Enumerable.Range(1, maxStep))
    {
        for (var j = 0; j < 1000; j++)
        {
            console.Write(j.ToString());
        }
        
        ctx.Refresh();
    }
});

First sequence of numbers gets fully printed to the screen, then I manually call Refresh(). After that first refresh it goes off the rails. Basically moving the cursor back to the start after every Write.

Another thing I noticed poking around that perhaps could be related - AnsiConsoleCursor is going right at the backend to write rather than using the AnsiConsoleFacade. Could this be causing some of the contention?

Some, my midnight brain dump before bed:

  1. We are resetting the cursor WAY more than we need to be for some reason after the first call to Refresh()
  2. The cursor reset doesn't respect the renderlock, but should it?

phil-scott-78 avatar Oct 06 '21 03:10 phil-scott-78

Do you have any update on this? I would love to continue my port of Serilog.Sinks.Console to Spectre.Console and this issue is a blocker.

0xced avatar Feb 03 '22 13:02 0xced

Same problem here.

await AnsiConsole.Status()
    .Spinner(Spinner.Known.Dots2)
    .SpinnerStyle(Style.Parse("yellow"))
    .StartAsync("Standby...", async ctx =>
    {
        // Check if online
        AnsiConsole.Markup("Connecting to [bold yellow bold]SpaceTraders[/]...");
        
        var status = await spaceTraders.GetStatusAsync();
        AnsiConsole.MarkupLine("Done");

       // ...
    });

When calling MarkupLine("Done"), the text 'Connecting to' disappears from the same line.

XanderStoffels avatar May 29 '23 03:05 XanderStoffels