Medindo o impacto da extensão na inicialização
Foco no desempenho da extensão no Visual Studio 2017
Com base nos comentários dos clientes, uma das áreas de foco da versão do Visual Studio 2017 foi o desempenho de inicialização e carregamento da solução. Como equipe da plataforma Visual Studio, temos trabalhado para melhorar o desempenho de inicialização e carga da solução. Em geral, nossas medições sugerem que as extensões instaladas também podem ter um impacto considerável nesses cenários.
Para ajudar os usuários a entender esse impacto, adicionamos um novo recurso no Visual Studio para notificar os usuários sobre extensões lentas. Às vezes, o Visual Studio detecta uma nova extensão que retarda a carga da solução ou a inicialização. Quando uma lentidão é detectada, os usuários verão uma notificação no IDE apontando-os para a nova caixa de diálogo "Gerenciar desempenho do Visual Studio". Essa caixa de diálogo também pode sempre ser acessada pelo menu Ajuda para procurar extensões detectadas anteriormente.
Este documento tem como objetivo ajudar os desenvolvedores de extensão, descrevendo como o impacto da extensão é calculado. Este documento também descreve como o impacto da extensão pode ser analisado localmente. A análise local do impacto da extensão determinará se uma extensão pode ser mostrada como uma extensão que afeta o desempenho.
Observação
Este documento se concentra no impacto das extensões na inicialização e na carga da solução. As extensões também afetam o desempenho do Visual Studio quando fazem com que a interface do usuário pare de responder. Para obter mais informações sobre este tópico, consulte Como: Diagnosticar atrasos da interface do usuário causados por extensões.
Como as extensões podem afetar a inicialização
Uma das maneiras mais comuns de as extensões afetarem o desempenho da inicialização é optar por carregar automaticamente em um dos contextos de interface do usuário de inicialização conhecidos, como NoSolutionExists ou ShellInitialized. Esses contextos de interface do usuário são ativados durante a inicialização. Todos os pacotes que incluem o ProvideAutoLoad
atributo em sua definição com esses contextos serão carregados e inicializados nesse momento.
Quando medimos o impacto de uma extensão, nos concentramos principalmente no tempo gasto pelas extensões que optam por carregar automaticamente nos contextos acima. Os tempos medidos incluiriam, mas não se limitariam a:
- Carregamento de assemblies de extensão para pacotes síncronos
- Tempo gasto no construtor de classe de pacote para pacotes síncronos
- Tempo gasto no método Initialize (ou SetSite) do pacote para pacotes síncronos
- Para pacotes assíncronos, as operações acima são executadas em thread em segundo plano. Como tal, as operações estão excluídas do monitoramento.
- Tempo gasto em qualquer trabalho assíncrono agendado durante a inicialização do pacote para execução no thread principal
- Tempo gasto em manipuladores de eventos, especificamente ativação de contexto inicializado do shell ou alteração de estado de zumbi do shell
- A partir da Atualização 3 do Visual Studio 2017, também começaremos a monitorar o tempo gasto em chamadas ociosas antes que o shell seja inicializado. Operações longas em manipuladores ociosos também causam IDE sem resposta e contribuem para o tempo de inicialização percebido pelo usuário.
Adicionamos muitos recursos a partir do Visual Studio 2015. Esses recursos ajudam a remover a necessidade de carregamento automático de pacotes. Os recursos também adiam a necessidade de carregamento de pacotes para casos mais específicos. Esses casos incluem exemplos em que os usuários teriam mais certeza de usar a extensão ou reduzir um impacto de extensão ao carregar automaticamente.
Você pode encontrar mais detalhes sobre esses recursos nos seguintes documentos:
Contextos de interface do usuário baseados em regras: um mecanismo baseado em regras mais avançado criado em torno de contextos de interface do usuário permite que você crie contextos personalizados com base em tipos de projeto, sabores e atributos. Contextos personalizados podem ser usados para carregar um pacote durante cenários mais específicos. Esses cenários específicos incluem a presença de um projeto com um recurso específico em vez de inicialização. Os contextos personalizados também permitem que a visibilidade do comando seja vinculada a um contexto personalizado com base em componentes do projeto ou outros termos disponíveis. Esse recurso elimina a necessidade de carregar um pacote para registrar um manipulador de consulta de status de comando.
Suporte a pacotes assíncronos: a nova classe base AsyncPackage no Visual Studio 2015 permite que os pacotes do Visual Studio sejam carregados em segundo plano de forma assíncrona se o carregamento do pacote foi solicitado por um atributo de carregamento automático ou uma consulta de serviço assíncrona. Esse carregamento em segundo plano permite que o IDE permaneça responsivo. O IDE é responsivo mesmo quando a extensão é inicializada em segundo plano e cenários críticos, como inicialização e carga de solução, não seriam afetados.
Serviços assíncronos: Com o suporte a pacotes assíncronos, também adicionamos suporte para consultar serviços de forma assíncrona e poder registrar serviços assíncronos. Mais importante, estamos trabalhando na conversão dos principais serviços do Visual Studio para oferecer suporte à consulta assíncrona, de modo que a maior parte do trabalho em uma consulta assíncrona ocorra em threads em segundo plano. SComponentModel (host MEF do Visual Studio) é um dos principais serviços que agora oferece suporte à consulta assíncrona para permitir que as extensões ofereçam suporte ao carregamento assíncrono completamente.
Reduzindo o impacto das extensões carregadas automaticamente
Se um pacote ainda precisar ser carregado automaticamente na inicialização, é importante minimizar o trabalho feito durante a inicialização do pacote. Minimizar o trabalho de inicialização do pacote reduz as chances de a extensão afetar a inicialização.
Alguns exemplos que podem fazer com que a inicialização do pacote seja cara são:
Uso de carga de pacote síncrona em vez de carga de pacote assíncrona
Como os pacotes síncronos são carregados no thread principal por padrão, incentivamos os proprietários de extensões que têm pacotes carregados automaticamente a usar a classe base do pacote assíncrono, conforme mencionado anteriormente. Alterar um pacote carregado automaticamente para oferecer suporte ao carregamento assíncrono também facilitará a resolução dos outros problemas abaixo.
Solicitações de E/S de arquivo/rede síncronas
Idealmente, qualquer arquivo síncrono ou solicitação de E/S de rede deve ser evitada no thread principal. Seu impacto dependerá do estado da máquina e pode bloquear por longos períodos de tempo em alguns casos.
O uso de carregamento de pacote assíncrono e APIs de E/S assíncronas deve garantir que a inicialização do pacote não bloqueie o thread principal nesses casos. Os usuários também podem continuar a interagir com o Visual Studio enquanto as solicitações de E/S acontecem em segundo plano.
Inicialização antecipada de serviços, componentes
Um dos padrões comuns na inicialização de pacotes é inicializar serviços usados por ou fornecidos por esse pacote no pacote constructor
ou initialize
método. Embora isso garanta que os serviços estejam prontos para serem usados, também pode adicionar custos desnecessários ao carregamento de pacotes se esses serviços não forem usados imediatamente. Em vez disso, esses serviços devem ser inicializados sob demanda para minimizar o trabalho feito na inicialização do pacote.
Para serviços globais fornecidos por um pacote, você pode usar AddService
métodos que usam uma função para inicializar preguiçosamente o serviço somente quando ele é solicitado por um componente. Para serviços usados dentro do pacote, você pode usar Lazy T ou AsyncLazy<<T>> para garantir que os serviços sejam inicializados/consultados no primeiro uso.
Medindo o impacto de extensões carregadas automaticamente usando o Registro de atividades
A partir da Atualização 3 do Visual Studio 2017, o log de atividades do Visual Studio agora conterá entradas para o impacto no desempenho dos pacotes durante a inicialização e o carregamento da solução. Para ver essas medidas, você precisa abrir o Visual Studio com a opção /log e abrir o arquivo ActivityLog.xml .
No log de atividades, as entradas estarão na origem "Gerenciar desempenho do Visual Studio" e serão semelhantes ao exemplo a seguir:
Component: 3cd7f5bf-6662-4ff0-ade8-97b5ff12f39c, Inclusive Cost: 2008.9381, Exclusive Cost: 2008.9381, Top Level Inclusive Cost: 2008.9381
Este exemplo mostra que um pacote com GUID "3cd7f5bf-6662-4ff0-ade8-97b5ff12f39c" gastou 2008 ms na inicialização do Visual Studio. Observe que o Visual Studio considera o custo de nível superior como o número principal ao calcular o impacto de um pacote, pois essa seria a economia que os usuários veem quando desabilitam a extensão para esse pacote.
Medindo o impacto de extensões carregadas automaticamente usando PerfView
Embora a análise de código possa ajudar a identificar caminhos de código que podem retardar a inicialização do pacote, você também pode utilizar o rastreamento usando aplicativos como PerfView para entender o impacto de uma carga de pacote na inicialização do Visual Studio.
O PerfView é uma ferramenta de rastreamento para todo o sistema. Esta ferramenta irá ajudá-lo a entender os caminhos quentes em um aplicativo por causa do uso da CPU ou bloqueando chamadas do sistema. Abaixo está um exemplo rápido sobre como analisar uma extensão de exemplo usando PerfView.
Código de exemplo:
Este exemplo é baseado no código de exemplo abaixo, que foi projetado para mostrar algumas causas comuns de atraso:
protected override void Initialize()
{
// Initialize a class from another assembly as an example
MakeVsSlowServiceImpl service = new MakeVsSlowServiceImpl();
// Costly work in main thread involving file IO
string systemPath = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
foreach (string file in Directory.GetFiles(systemPath))
{
DateTime creationDate = File.GetCreationTime(file);
}
// Costly work after shell is initialized. This callback executes on main thread
KnownUIContexts.ShellInitializedContext.WhenActivated(() =>
{
DoMoreWork();
});
// Start async work on background thread
DoAsyncWork().Forget();
}
private async Task DoAsyncWork()
{
// Switch to background thread to do expensive work
await TaskScheduler.Default;
System.Threading.Thread.Sleep(500);
}
private void DoMoreWork()
{
// Costly work
System.Threading.Thread.Sleep(500);
// Blocking call to an asynchronous work.
ThreadHelper.JoinableTaskFactory.Run(async () => { await DoAsyncWork(); });
}
Gravando um rastreamento com PerfView:
Depois de configurar seu ambiente do Visual Studio com sua extensão instalada, você pode gravar um rastreamento de inicialização abrindo PerfView e abrindo a caixa de diálogo Coletar no menu Coletar.
As opções padrão fornecerão pilhas de chamadas para consumo de CPU, mas como estamos interessados em bloquear o tempo também, você também deve habilitar pilhas de tempo de thread. Quando as configurações estiverem prontas, você poderá clicar em Iniciar Coleção e, em seguida, abrir o Visual Studio após o início da gravação.
Antes de parar a coleção, você deseja verificar se o Visual Studio está totalmente inicializado, a janela principal está completamente visível e, se sua extensão tiver quaisquer partes da interface do usuário que aparecem automaticamente, elas também estarão visíveis. Quando o Visual Studio é completamente carregado e sua extensão é inicializada, você pode parar a gravação para analisar o rastreamento.
Analisando um rastreamento com PerfView:
Assim que a gravação for concluída, o PerfView abrirá automaticamente o rastreamento e expandirá as opções.
Para os fins deste exemplo, estamos interessados principalmente na exibição Pilhas de Tempo de Thread, que você pode encontrar em Grupo Avançado. Essa exibição mostrará o tempo total gasto em um thread por um método, incluindo o tempo de CPU e o tempo bloqueado, como E/S de disco ou alças de espera.
Ao abrir a visualização Thread Time Stacks, você deve escolher o processo devenv para iniciar a análise.
O PerfView tem orientações detalhadas sobre como ler pilhas de tempo de thread em seu próprio menu Ajuda para uma análise mais detalhada. Para fins deste exemplo, queremos filtrar ainda mais essa exibição incluindo apenas pilhas com nosso nome de módulo de pacotes e thread de inicialização.
- Defina GroupPats como texto vazio para remover qualquer agrupamento adicionado por padrão.
- Defina IncPats para incluir parte do nome do assembly e do Thread de Inicialização, além do filtro de processo existente. Neste caso, deve ser devenv; Thread de Inicialização; MakeVsSlowExtension.
Agora, a exibição mostrará apenas o custo associado aos assemblies relacionados à extensão. Nesta visualização, qualquer momento listado na coluna Inc (Inclusive cost) do thread de inicialização está relacionado à nossa extensão filtrada e afetará a inicialização.
Para o exemplo acima, algumas pilhas de chamadas interessantes seriam:
E/S usando
System.IO
classe: Embora o custo inclusivo desses quadros possa não ser muito caro no rastreamento, eles são uma causa potencial de um problema, já que a velocidade de E/S do arquivo varia de máquina para máquina.Bloqueio de chamadas aguardando em outro trabalho assíncrono: nesse caso, o tempo inclusivo representaria o tempo em que o thread principal é bloqueado na conclusão do trabalho assíncrono.
Uma das outras exibições no rastreamento que será útil para determinar o impacto será as pilhas de carga de imagem. Você pode aplicar os mesmos filtros aplicados ao modo de exibição Pilhas de Tempo de Thread e descobrir todos os assemblies carregados devido ao código executado pelo pacote carregado automaticamente.
É importante minimizar o número de montagens carregadas dentro de uma rotina de inicialização de pacotes, pois cada montagem adicional envolverá E/S de disco extra, o que pode retardar consideravelmente a inicialização em máquinas mais lentas.
Resumo
A inicialização do Visual Studio tem sido uma das áreas sobre as quais recebemos feedback continuamente. Nosso objetivo, como dito anteriormente, é que todos os usuários tenham uma experiência de inicialização consistente, independentemente dos componentes e extensões que instalaram. Gostaríamos de trabalhar com os proprietários de extensões para ajudá-los a nos ajudar a alcançar esse objetivo. As diretrizes acima devem ser úteis para entender o impacto de extensões na inicialização e evitar a necessidade de carregá-lo automaticamente ou carregá-lo de forma assíncrona para minimizar o impacto na produtividade do usuário.