Skip to content

Very slow rendering of colored text #4129

@joaobzrr

Description

@joaobzrr

WriteConsole() performs reasonably well when writing plain uncolored text to the console (~2ms at best, ~20ms at worst with a 240x64 buffer), but is incredibly slow when writing text with color escape sequences. Below is some code that demonstrates this.

I write an entire screen worth of colored text as fast as possible shifting every character to the one next to it in ASCII as to avoid any possible caching that might happen. I measure the time it takes for each WriteConsole() call and print the results at the end. No third-party libraries are needed to compile.

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

#define NUM_OF_MEASUREMENTS 100


static LONGLONG perf_counter_freq;


LONGLONG
get_timestamp(void)
{
    LARGE_INTEGER timestamp;
    QueryPerformanceCounter(&timestamp);
    LONGLONG result = timestamp.QuadPart;
    return result;
}


float
get_seconds_elapsed(LONGLONG start, LONGLONG end)
{
    float result = (float)(end - start) / (float)perf_counter_freq;
    return result;
}


void
generate_text(char start_char, char *buffer, size_t buffer_size)
{
    for (size_t i = 0; i < buffer_size; i++)
    {
        buffer[i] = 'A' + (((start_char - 'A') + i) % 26);
    }
}


char *
copy_string(char *dest, char *source)
{
    while (*source)
    {
        *dest++ = *source++;
    }

    return dest;
}


int main(int argc, char **argv)
{
    HANDLE con_out = CreateFile("CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
    if (con_out == INVALID_HANDLE_VALUE) goto cleanup;

    DWORD console_mode;
    GetConsoleMode(con_out, &console_mode);

    console_mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_WRAP_AT_EOL_OUTPUT;
    SetConsoleMode(con_out, console_mode);

    CONSOLE_SCREEN_BUFFER_INFO buffer_info;
    GetConsoleScreenBufferInfo(con_out, &buffer_info);

    LARGE_INTEGER perf_counter_freq_result;
    QueryPerformanceFrequency(&perf_counter_freq_result);
    perf_counter_freq = perf_counter_freq_result.QuadPart;

    int console_width  = (buffer_info.srWindow.Right  - buffer_info.srWindow.Left) + 1;
    int console_height = (buffer_info.srWindow.Bottom - buffer_info.srWindow.Top);
    size_t buffer_size = console_width * console_height;

    char *buffer1 = (char*)malloc(buffer_size);
    if (!buffer1) goto cleanup;

    char *buffer2 = (char*)malloc(buffer_size);
    if (!buffer2) goto cleanup;

    generate_text('A', buffer1, buffer_size);
    generate_text('B', buffer2, buffer_size);

    size_t screen_buffer_size = 1048576;
    char *screen_buffer = (char*)malloc(screen_buffer_size);
    if (!screen_buffer) goto cleanup;

    int measurement_index = 0;
    int num_of_measurements = NUM_OF_MEASUREMENTS;
    float *measurements = (float*)malloc(num_of_measurements*sizeof(float));
    if (!measurements) goto cleanup;

    int c = 0;
    int b = 1;
    for (int count = 0; count < num_of_measurements; count++)
    {
        char *src = b ? buffer1 : buffer2;
        char *at = screen_buffer;

        at = copy_string(at, "\x1b[H");
        for (int j = 0; j < console_height; j++)
        {
            size_t row_offset = j * console_width;
            char *row = &src[row_offset];

            for (int i = 0; i < console_width; i++)
            {
                at = copy_string(at, "\x1b[38;2;");
                switch(c)
                {
                    case 1:  { at = copy_string(at, "28;215;119m"); } break;
                    case 2:  { at = copy_string(at, "0;159;255m");  } break;
                    default: { at = copy_string(at, "226;51;73m");  } break;
                }
                c = (c + 1) % 3;

                *at++ = row[i];
                at = copy_string(at, "\x1b[0m");
            }
            at = copy_string(at, "\n");
        }

        LONGLONG start = get_timestamp();

        DWORD written = 0;
        size_t chars_to_write = at - screen_buffer;
        WriteConsole(con_out, screen_buffer, (DWORD)chars_to_write, &written, 0);

        LONGLONG end = get_timestamp();

        float seconds_elapsed = get_seconds_elapsed(start, end);
        measurements[measurement_index++] = seconds_elapsed;

        b = !b;
    }

    DWORD written = 0;
    WriteConsole(con_out, "\x1b[2J\x1b[H", 7, &written, 0);

    float fastest = measurements[0];
    float slowest = measurements[0];
    float total = 0;
    for (int i = 0; i < num_of_measurements; i++)
    {
        float measurement = measurements[i];

        fastest = measurement < fastest ? measurement : fastest;
        slowest = measurement > slowest ? measurement : slowest;
        total += measurement;
    }
    float average = total / num_of_measurements;

    printf("Fastest: %.4f\n", fastest);
    printf("Slowest: %.4f\n", slowest);
    printf("Average: %.4f\n", average);

cleanup:
    if (con_out != INVALID_HANDLE_VALUE) CloseHandle(con_out);
    if (buffer1)       free(buffer1);
    if (buffer2)       free(buffer2);
    if (screen_buffer) free(screen_buffer);
    if (measurements)  free(measurements);
}

Is there anything I can do to make this run fast enough for animation (at least 30 FPS)? Techniques like only writing what changed are not an option, although I doubt that doing that would be faster than just writing the whole buffer to the console at once. I know about WriteConsoleOutput() and double-buffering with SetConsoleActiveScreenBuffer() but I don't know if they could be made to work with VT escape sequences.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area-PerformancePerformance-related issueIn-PRThis issue has a related PRIssue-BugIt either shouldn't be doing this or needs an investigation.Needs-Tag-FixDoesn't match tag requirementsPriority-1A description (P1)Product-ConhostFor issues in the Console codebase

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions