terminal icon indicating copy to clipboard operation
terminal copied to clipboard

The terminal is buffering in continuous printing

Open donno2048 opened this issue 3 years ago • 28 comments

Windows Terminal version

1.11.3471.0

Windows build number

10.0.19043.1466

Other Software

No response

Steps to reproduce

// poc.c
#include <stdio.h>
#include <windows.h>
int main(void) {
    CONSOLE_SCREEN_BUFFER_INFO csbi;
    GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
    int width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
    int height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
    char f[height][width];
    memset(f, ' ', height * width);
    f[height / 2][width / 2] = '*';
    while(1) {
        puts(*f);
    }
}
>gcc poc.c -o main
>main

Expected Behavior

One "*" character is supposed to appear in the middle of the terminal

Actual Behavior

The terminal is buffering just like I previously mentioned here

donno2048 avatar Feb 02 '22 22:02 donno2048

Where are you compiling that code and with what?

eabase avatar Feb 03 '22 00:02 eabase

Where are you compiling that code and with what?

Compiling using gcc

donno2048 avatar Feb 03 '22 00:02 donno2048

@donno2048 after building poc.c with clang on Windows this is the output:

https://user-images.githubusercontent.com/3933920/152261532-3e87d9af-65e6-47a7-b8cd-a614d859496a.mp4

After canceling the test there are some characters left over, not sure what they are:

poc_test

Is this the output you're seeing too? Thx!

elsaco avatar Feb 03 '22 00:02 elsaco

That's odd, using gcc in cmd I get this (expected behavior):

https://user-images.githubusercontent.com/61805754/152262671-27acfcbe-049c-46d5-86af-7404a949d27f.mp4

And in the terminal this:

https://user-images.githubusercontent.com/61805754/152262642-cbc810f9-043a-4462-b5ea-f385c56bc79e.mp4

donno2048 avatar Feb 03 '22 00:02 donno2048

Compiling using gcc but it doesn't really matter

I suspect it matters a lot. If your compiler are making some default assumption of using windows vs gui builds and libraries etc. So seeing gcc -v might be useful. Or if you're doing it from within WSL.

Then clearly the buffer sizes are probably different in CMD vs WT/pwsh. $Host.UI.RawUI and compare to your cmd.


To me it looks as he 2 last videos are ding the same thing, only that you're lucky that the screen form feed just happen to match up in one, but not the other.

eabase avatar Feb 03 '22 01:02 eabase

Compiling using gcc but it doesn't really matter

I suspect it matters a lot. If your compiler are making some default assumption of using windows vs gui builds and libraries etc. So seeing gcc -v might be useful. Or if you're doing it from within WSL.

I compile on Windows, on Linux it's working perfectly as I mentioned microsoft/vscode#142001

Then clearly the buffer sizes are probably different in CMD vs WT/pwsh. $Host.UI.RawUI and compare to your cmd.

To me it looks as he 2 last videos are ding the same thing, only that you're lucky that the screen form feed just happen to match up in one, but not the other.

It isn't "luck" the code is literally reading the height of the console to do that...

donno2048 avatar Feb 03 '22 02:02 donno2048

The terminal is buffering

I did not compile your example code. Just run simple while()

https://user-images.githubusercontent.com/78153320/152358146-11826aba-be23-46be-9a0b-067e787c0fbb.mp4

237dmitry avatar Feb 03 '22 14:02 237dmitry

@237dmitry So? You're not actually printing the same thing...

donno2048 avatar Feb 03 '22 14:02 donno2048

The terminal is buffering in continuous printing

You're not actually printing the same thing...

???

237dmitry avatar Feb 03 '22 14:02 237dmitry

I mean I print continuously such that each print covers the entire terminal the excepted behavior is for the terminal to show that string continuously

donno2048 avatar Feb 03 '22 14:02 donno2048

I suspect this is a duplicate of #11794, in which case it should be improved by PR #11890 (once that's released).

j4james avatar Feb 03 '22 15:02 j4james

Alright, I don't know how this thread got so off the rails. Gonna minimize half the comments here.

Alas, this didn't get better with #11890. The key here is that OP is just constantly throwing new lines into the buffer, so conpty needs to flush the new line to the terminal side each newline. IIRC I had tried avoiding that in the initial conpty implementation, but it had some bug that forced the immediate flushing. Something about InvalidateCircling. (I idly wonder if I made that call before deciding that conpty needs to be the same size as the viewport. Maybe that isn't necessary anymore.)

#1173 & #10001 might be a potential solution here. Get ConPTY out of the equation.

OP could probably remove a lot of pain by doing a \e[H at the start of each frame, by returning the cursor to the top of the screen at each frame. That would prevent them from loading up the scrollback with previous frames. There's still the underlying optimization that could be made here, so let's investigate.

cpp version of OPs code, for my reference

#include <stdio.h>
#include <windows.h>

// This wmain exists for help in writing scratch programs while debugging.
int __cdecl wmain(int /*argc*/, WCHAR* /*argv[]*/)
{
    CONSOLE_SCREEN_BUFFER_INFO csbi;
    GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
    int width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
    int height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
    char* f = new char [height * width];
    memset(f, ' ', height * width);
    f[(height / 2) * width + (width / 2)] = '*';
    f[height * width - 1] = 0;
    while(1) {
        puts(f);
    }
}

zadjii-msft avatar Feb 03 '22 16:02 zadjii-msft

The code posted here has a bug where the null terminator for f is missing when passed to puts which expects one. Use this instead:

fwrite(f, sizeof(char), height * width, stdout);

The immediate flushing of ConPTY is a significant performance issue. vtebench is a really nice tool and it shows that Windows Terminal is pretty alright in almost all areas... except for newline performance, which is 500-900x slower than other popular Terminals. See #10563.

I suspect that fixing the scrolling performance will simultaneously fix this issue more or less. \e[H, etc. should be used for your CLI application regardless though.

  • At the start of your animation hide your cursor with "\x1b[?25l"
  • Use "\x1b[H" or "\x1b[1;1H" to jump back to the top-left of the viewport each frame

lhecker avatar Feb 04 '22 16:02 lhecker

Using any type of Unicode special sequence is problematic when running the code from CMD because then it's actually printed...

donno2048 avatar Feb 05 '22 17:02 donno2048

@donno2048 Maybe I'm misunderstanding you, but if you're referring to my (and other's) suggestion to use "\x1b[H": This isn't unicode, but rather a so called "virtual terminal sequence", or "escape sequence". You can read more about those here: https://en.wikipedia.org/wiki/ANSI_escape_code#Description \x1b is the ESC character in hexadecimal and the same as \033 in octal. Some applications also accept \e as a synonym.

Modern terminals will parse such escape sequences and not show them on the screen to the user. That way you can for instance tell your terminal to jump anywhere on the screen, or change the text color, with just a bit of hidden text.

In order to use such sequences in CMD you can simply enable the ENABLE_VIRTUAL_TERMINAL_PROCESSING flag. This flag is always enabled when you run applications in Windows Terminal. Here's some example code: https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#samples

lhecker avatar Feb 05 '22 17:02 lhecker

Yeah, I know what an escape sequence I just didn't remember the term 😅 (yeah it isn't Unicode but you know what I mean), in my original issue (microsoft/vscode#142001) I mentioned the possibility to use them to resolve this issue partially, the problem as I said is that when not running in a compatible terminal but rather a "plain" CMD those characters will actually be printed which is problematic

donno2048 avatar Feb 05 '22 17:02 donno2048

@donno2048 I probably continue to misunderstand you. I'm sorry in advance. 😅 CMD isn't incompatible with escape sequences. It's just disabled by default. I mean maybe you meant to say with "plain CMD" that you explicitly don't want to use escape sequences. Reading your issue on vscode however implies to me that you actually want to use escape sequences, but found that they don't work in CMD, right?

If I'm right, does it work if you try to add this at the very start of your application?

SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);

ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT are enabled by default, but ENABLE_VIRTUAL_TERMINAL_PROCESSING isn't. This one-liner will enable the flag and allow you to use \x1b[H in CMD. 🙂

lhecker avatar Feb 05 '22 17:02 lhecker

SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);

You just blew my mind

donno2048 avatar Feb 05 '22 17:02 donno2048

@lhecker

Use this instead:

I tried that, but I'm getting weird results. What code are you referring to, can you show the whole thing, and with the compiler options?

eabase avatar Feb 05 '22 21:02 eabase

Use this instead:

I tried that, but I'm getting weird results. What code are you referring to, can you show the whole thing, and with the compiler options?

The code is the code from the first comment here.

donno2048 avatar Feb 05 '22 22:02 donno2048

@lhecker

Use this instead:

I tried that, but I'm getting weird results. What code are you referring to, can you show the whole thing, and with the compiler options?

Try this:

#include <stdio.h>
#include <windows.h>
int main(void) {
	int height, width;
	{
		CONSOLE_SCREEN_BUFFER_INFO csbi;
		handle_t h = GetStdHandle(STD_OUTPUT_HANDLE);
		SetConsoleMode(h, ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
		GetConsoleScreenBufferInfo(h, &csbi);
		width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
		height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
	}
	char f[height][width];
	memset(f, ' ', height * width);
	f[height / 2][width / 2] = '*';
	while (TRUE) {
		printf("\x1b[H");
		fwrite(f, sizeof(char), height * width, stdout);
	}
}

donno2048 avatar Oct 30 '22 22:10 donno2048

A thought I had: Perhaps I could reuse the "buffer but don't flush" from #14677. Flushing is expensive, buffering... less so.

zadjii-msft avatar Mar 09 '23 12:03 zadjii-msft

dev/migrie/f/12336-let-it-mellow has an experimentation into that thought, but it's still only 8MB/s (up from ~6MB/s) compared to the like, 98MB/s of conhost. Though, the passthrough that's in main (admittedly busted to heck) does get that up to ~70MB/s (up from ~60)

zadjii-msft avatar Jul 07 '23 11:07 zadjii-msft

showerthought - does that just queue the frame to get flushed on the render thread, but ultimately, the render thread still ends up blocking on WriteFile. Like, I'm worried that we end up in a place where the flush is still just blocking on I/O instead of properly flushing async.

74181014cfc97a4357b416b2622f73558756357b had an attempt at just moving the WriteFile into Flush, but we still need to do something faster than that.

zadjii-msft avatar Jul 20 '23 14:07 zadjii-msft

The problem with "buffering" linefeeds is that they destroy the conhost buffer, since that's only the size of the visible viewport. Every linefeed you execute without flushing is going to erase one line of content from the top of the viewport. By the time you eventually flush the buffer, there's potentially nothing left to send to the conpty client. What should have been pushed into their scrollback would now be lost.

j4james avatar Jul 20 '23 14:07 j4james

Oh sorry - to clarify: in the aforementioned branch, when we circle, I'd render the text buffer out to the conpty buffer but not immediately flush the conpty buffer to the end terminal. I'd wait until the normal frame happens to do that. That would get the conpty WriteFile out of the console I/O thread's hot path (writing text).

That would still preserve the text, because the "frame" still happens before we lose the text. It just gets written to the pipe at a later time.

Now, the real issue is: can we speed writing to the pipe up? Is that slow because pipes are slow? Is it slow because Terminal drains the pipe too slow? Something something xproc calls, etc.

zadjii-msft avatar Jul 20 '23 15:07 zadjii-msft

https://github.com/microsoft/terminal/blob/701062649768c4f6bfb5ea77eab9154014995918/src/renderer/vt/invalidate.cpp#L107-L121

hmm. Do we need to pForcePaint if

  • we're circling, AND
  • the top line is NOT invalid?

Probably not? That might be a place to speed this up a little. Not sure that'd end up actually optimizing that much out. I'd reckon we're probably outputting to the textbuffer a whole viewport worth of lines faster than a frame anyways

zadjii-msft avatar Jul 20 '23 15:07 zadjii-msft

Oh sorry - to clarify: in the aforementioned branch, when we circle, I'd render the text buffer out to the conpty buffer but not immediately flush the conpty buffer to the end terminal.

OK. That makes perfect sense. I'd forgotten there was a conpty buffer.

j4james avatar Jul 20 '23 15:07 j4james