Barra de rolagem que falhou


Recentemente lançou uma nova versão do Windows Terminal. Tudo ficaria bem, mas o desempenho de sua barra de rolagem deixava muito a desejar. Portanto, é hora de enfiá-lo e tocar pandeiro.

O que os usuários costumam fazer com a nova versão de qualquer aplicativo? É isso mesmo, exatamente o que os testadores não fizeram. Portanto, após um breve uso do terminal para a finalidade pretendida, comecei a fazer coisas terríveis com ele. Tudo bem, tudo bem, eu apenas derramei café no teclado e acidentalmente apertei <Enter> quando o limpei. O que aconteceu no final?



Sim, não parece muito impressionante, mas não se apresse em jogar pedras em mim. Preste atenção ao lado direito. Primeiro tente descobrir o que há de errado com ela. Aqui está uma captura de tela para uma dica:


Obviamente, o título do artigo foi um sério spoiler. :)

Portanto, há um problema com a barra de rolagem. Movendo-se para uma nova linha muitas vezes, depois de cruzar a borda inferior, você normalmente espera que uma barra de rolagem apareça e pode rolar para cima. No entanto, isso não acontece até escrevermos um comando com a saída de alguma coisa. Vamos apenas dizer que o comportamento é estranho. No entanto, isso pode não ter sido tão crítico se a barra de rolagem funcionasse ...

Depois de testar um pouco, descobri que mudar para uma nova linha não aumenta o buffer. Isso apenas produz a saída de comandos. Portanto, o whoami acima aumentará o buffer em apenas uma linha. Por esse motivo, com o tempo, perderemos muita história, principalmente depois de claro.

A primeira coisa que me veio à mente foi usar nosso analisador e ver o que diz:


A conclusão, é claro, é de tamanho impressionante, então aproveitarei o poder de filtrar e cortar tudo, exceto o que contém a ScrollBar :


Não posso dizer que existem muitas mensagens ... Bem, talvez haja algo relacionado ao buffer?


O analisador não falhou e encontrou algo interessante. Eu destaquei este aviso acima. Vamos ver o que está errado lá:

V501 . Existem subexpressões idênticas à esquerda e à direita do operador '-': bufferHeight - bufferHeight TermControl.cpp 592

bool TermControl::_InitializeTerminal()
{
  ....
  auto bottom = _terminal->GetViewport().BottomExclusive();
  auto bufferHeight = bottom;

  ScrollBar().Maximum(bufferHeight - bufferHeight); // <=  
  ScrollBar().Minimum(0);
  ScrollBar().Value(0);
  ScrollBar().ViewportSize(bufferHeight);
  ....
}

Este código é acompanhado por um comentário: "Configure a altura do ScrollViewer e a grade que estamos usando para falsificar nossa altura de rolagem".

É claro que simular a altura do rolo é bom, mas por que estamos colocando 0 no máximo? Voltando à documentação , ficou claro que o código não é muito suspeito. Não me interpretem mal: subtrair uma variável de si mesma é, é claro, suspeito, mas obtemos um zero na saída, o que não nos prejudica. De qualquer forma, tentei especificar o valor padrão (1) no campo Máximo :


A barra de rolagem apareceu, mas também não funciona:



Se for o caso, apertei <Enter> por 30 segundos. Aparentemente, esse não é o problema, então vamos deixá-lo como estava, exceto substituindo bufferHeight - bufferHeight por 0:

bool TermControl::_InitializeTerminal()
{
  ....
  auto bottom = _terminal->GetViewport().BottomExclusive();
  auto bufferHeight = bottom;

  ScrollBar().Maximum(0); // <=   
  ScrollBar().Minimum(0);
  ScrollBar().Value(0);
  ScrollBar().ViewportSize(bufferHeight);
  ....
}

Portanto, não estamos particularmente perto de resolver o problema. Na ausência de uma oferta melhor para entrar em desagrado. No começo, poderíamos colocar um ponto de interrupção na linha alterada, mas duvido que isso nos ajude de alguma forma. Portanto, primeiro precisamos encontrar o fragmento responsável pelo deslocamento da Viewport em relação ao buffer.

Um pouco sobre como a barra de rolagem local (e provavelmente qualquer outra) funciona. Temos um grande buffer que armazena toda a saída. Para interagir com ele, algum tipo de abstração é usada para desenhar na tela, neste caso, viewport .

Usando essas duas primitivas, podemos entender qual é o nosso problema. Ir para uma nova linha não aumenta o buffer e, por isso, simplesmente não temos para onde ir. Portanto, o problema está nele.

Armado com esse conhecimento comum, continuamos nossa debilidade heróica. Após uma pequena caminhada pela função, chamei a atenção para este fragmento:

// This event is explicitly revoked in the destructor: does not need weak_ref
auto onReceiveOutputFn = [this](const hstring str) {
  _terminal->Write(str);
};
_connectionOutputEventToken = _connection.TerminalOutput(onReceiveOutputFn);

Depois de configurar o ScrollBar acima , configuramos várias funções de retorno de chamada e executamos __connection.Start () para nossa janela recém-criada. Após o qual o lambda acima é chamado. Como esta é a primeira vez que escrevemos algo no buffer, sugiro iniciar nossa depuração a partir daí.

Definimos um ponto de interrupção dentro do lambda e olhamos em _terminal :



Agora, temos duas variáveis ​​extremamente importantes para nós - _buffer e _mutableViewport . Coloque pontos de interrupção neles e descubra onde eles mudam. É verdade que, com _viewport , trapaceio um pouco e coloco um ponto de interrupção não na variável em si, mas em seu campo superior (só precisamos dela).

Agora clique em <F5>, e nada acontece ... Bem, vamos pressionar algumas dezenas de vezes <Enter>. Nada aconteceu. Aparentemente, no _buffer , definimos um ponto de interrupção de maneira muito imprudente, e _viewport , como esperado, permaneceu no topo do buffer, o que não aumentou em tamanho.

Nesse caso, faz sentido inserir um comando para fazer com que o vértice seja atualizado._viewport . Depois disso, descobrimos um código muito interessante:

void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
{
  ....
  // Move the viewport down if the cursor moved below the viewport.
  if (cursorPosAfter.Y > _mutableViewport.BottomInclusive())
  {
    const auto newViewTop =
      std::max(0, cursorPosAfter.Y - (_mutableViewport.Height() - 1));
    if (newViewTop != _mutableViewport.Top())
    {
      _mutableViewport = Viewport::FromDimensions(....); // <=
      notifyScroll = true;
    }
  }
  ....
}

Apontei um comentário de onde paramos. Se você olhar o comentário sobre o fragmento, fica claro que estamos mais próximos da solução do que nunca. É neste local que a parte visível é deslocada em relação ao buffer e temos a oportunidade de rolar. Depois de observar um pouco o comportamento, notei um ponto interessante: ao passar para uma nova linha, o valor da variável cursorPosAfter.Y é igual ao valor da janela de visualização , portanto, não a omitimos e nada funciona. Além disso, há um problema semelhante com a variável newViewTop . Portanto, vamos aumentar o valor de cursorPosAfter.Y em um e ver o que aconteceu:

void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
{
  ....
  // Move the viewport down if the cursor moved below the viewport.
  if (cursorPosAfter.Y + 1 > _mutableViewport.BottomInclusive())
  {
    const auto newViewTop =
      std::max(0, cursorPosAfter.Y + 1 - (_mutableViewport.Height() - 1));
    if (newViewTop != _mutableViewport.Top())
    {
      _mutableViewport = Viewport::FromDimensions(....); // <=
      notifyScroll = true;
    }
  }
  ....
}

E o resultado do lançamento:


Maravilhas! Entrei em quantidade e a barra de rolagem funciona. É verdade, até o momento em que apresentamos algo ... Para demonstrar o arquivo, anexarei um gif:


Aparentemente, estamos fazendo alguns saltos extras para uma nova linha. Vamos tentar limitar nossas transições usando a coordenada X. Somente mudaremos a linha quando X for 0:

void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
{
  ....
  if (   proposedCursorPosition.X == 0
      && proposedCursorPosition.Y == _mutableViewport.BottomInclusive())
  {
    proposedCursorPosition.Y++;
  }

  // Update Cursor Position
  ursor.SetPosition(proposedCursorPosition);

  const COORD cursorPosAfter = cursor.GetPosition();

  // Move the viewport down if the cursor moved below the viewport.
  if (cursorPosAfter.Y > _mutableViewport.BottomInclusive())
  {
    const auto newViewTop =
      std::max(0, cursorPosAfter.Y - (_mutableViewport.Height() - 1));
    if (newViewTop != _mutableViewport.Top())
    {
      _mutableViewport = Viewport::FromDimensions(....);
      notifyScroll = true;
    }
  }
  ....
}

O fragmento escrito acima mudará a coordenada Y do cursor. Então atualizamos a posição do cursor. Em teoria, isso deve funcionar ... O que aconteceu?



Bem, claro, melhor. No entanto, há um problema em que alteramos o ponto de saída, mas não alteramos o buffer. Portanto, vemos duas chamadas do mesmo comando. Pode, é claro, parecer que eu sei o que estou fazendo, mas não é assim. :)

Nesse ponto, decidi verificar o conteúdo do buffer e retornei ao ponto em que iniciei a depuração:

// This event is explicitly revoked in the destructor: does not need weak_ref
auto onReceiveOutputFn = [this](const hstring str) {
  _terminal->Write(str);
};
_connectionOutputEventToken = _connection.TerminalOutput(onReceiveOutputFn);

Eu configurei um ponto de interrupção no mesmo local da última vez e comecei a examinar o conteúdo da variável str . Vamos começar com o que vi na minha tela:


O que você acha que estará na string str quando eu pressionar <Enter>?

  1. String "LONG DESCRIPTION" .
  2. O buffer inteiro que vemos agora.
  3. O buffer inteiro, mas sem a primeira linha.

Eu não vou definhar - o buffer inteiro, mas sem a primeira linha. E este é um problema considerável, porque é precisamente por isso que estamos perdendo a história, além disso, pontualmente. É assim que o nosso fragmento de saída de ajuda cuidará de ir para uma nova linha:


Com uma flecha, marquei o local onde estava “LONG DESCRIPTOIN” . Talvez então substitua o buffer com um deslocamento de uma linha? Isso funcionaria se esse retorno de chamada não fosse chamado para todos os espirros.

Eu descobri pelo menos três situações quando é chamado,

  • Quando inserimos qualquer caractere;
  • Quando passamos pela história;
  • Quando executamos o comando.

O problema é que você precisa mover o buffer apenas quando executamos o comando ou digite <Enter>. Em outros casos, fazer isso é uma má ideia. Então, precisamos determinar de alguma forma o que precisa ser mudado.

Conclusão


Quadro 18


Este artigo foi uma tentativa de mostrar com que habilidade o PVS-Studio foi capaz de encontrar códigos defeituosos que levaram ao erro que notei. A mensagem sobre o tópico de subtrair uma variável de si mesma me motivou fortemente, e eu rapidamente comecei a escrever texto. Mas como você pode ver, minha alegria foi prematura e tudo se tornou muito mais complicado.

Então eu decidi parar. Ainda era possível passar algumas noites, mas quanto mais eu fazia isso, mais e mais problemas surgiam. Tudo o que posso fazer é desejar aos desenvolvedores do Terminal do Windows boa sorte na correção desse bug. :)

Espero não decepcionar o leitor por não ter terminado a pesquisa e que tenha sido interessante para mim dar um passeio pelo interior do projeto. Como compensação, sugiro usar o código promocional #WindowsTerminal, graças ao qual você receberá uma versão demo do PVS-Studio não por uma semana, mas imediatamente por um mês. Se você ainda não experimentou o analisador estático do PVS-Studio, esse é um bom motivo para fazer exatamente isso. Basta digitar "#WindowsTerminal" no campo "Mensagem" na página de download .

Além disso, aproveitando esta oportunidade, quero lembrá-lo de que em breve haverá uma versão do analisador C # funcionando no Linux e no macOS. E agora você pode se inscrever para testes preliminares.


Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Maxim Zvyagintsev. A pequena barra de rolagem que não podia .

All Articles