Apresentação

Algumas análises são extremamente demoradas para rodar, ou ainda apresentam muitos passos na sua execução. Desde a limpeza dos dados originais, transformações, múltiplas análises e apresentações destas análises através de figuras ou tabelas. Todo esse conjunto de ações quando tomadas em conjunto é o que chamamos de pipeline ou fluxo de trabalho. Devido a complexidade e tempo que este fluxo pode assumir é comum perdermos controle da sequência que cada tarefa deve ser executada.

Devido a isso, muitas vezes nos pegamos rodando nossos códigos de maneira repetida, e algumas vezes sem nem mesmo saber se precisamos rodá-los. Isso é o que chamamos de loop sisypheano. Ao invés de rolar uma pedra morro acima toda vez, o que fazemos é rodar as mesmas análises repetidamente.

Sisyphus rolando a pedra morro acima, para a eternidade. A principal diferença entre mim e Sisyphus são os músculos

Sisyphus rolando a pedra morro acima, para a eternidade. A principal diferença entre mim e Sisyphus são os músculos

Uma solução para quebrar o loop sisefeano e otimizar nosso fluxo de trabalho é a utilização do pacote {targets}. A partir de agora, esperamos que as únicas pedras rolantes que vamos nos deparar sejam essa do vídeo abaixo.

O pacote {targets}

O pacote {targets} possibilita otimizar a sequência de trabalho (pipeline) por organizar esta sequência e identificar ações no fluxo de trabalho que devem ou não devem ser realizadas.

Exemplo

Para ilustrar o uso do pacote targets vamos utilizar um exemplo contido na própria documentação do pacote.

Neste exemplo vamos analisar a relação entre quantidade de ozônio e temperatura em um conjunto de dados presentes no próprio R base chamado airquality. Para tanto precisamos seguir uma sequência de análise de dados, que, basicamente, consiste em:

  1. Ler e manipular a tabela de dados

  2. Rodar um modelo relacionando ozônio e temperatura

  3. Gerar resultados gráficos (figuras) para o modelo ajustado

A base de dados pode ser lida da seguinte forma

data(airquality)
airquality

Imagine que estes dados estão organizados em um diretório local inicializado a partir de um .Rproject e ele apresenta a seguinte estrutura:

  • Uma pasta data contendo os dados

  • Uma pasta R contendo:

    • script com a leitura e transformação dos dados
    • script com o modelo
    • script com funções para plotar os resultados do modelo ajustado

Esta seria uma pasta organizada, tal como vimos durante as aulas. Porém, para que o pacote targets funciona precisamos transformar esta estrutura de acordo com um pipeline targets, que por sua vez necessita da seguinte estrutura:

Neste diretório precisamos transformar a sequência apresentada anteriormente em uma sequência de funções. Portanto os scripts na pasta R serão transformados em funções que serão colocadas dentro da pasta R, com o nome de functions.R, e que apresentará a seguinte forma:

Veja que a mesma sequência de análise está agora representada como funções, naquilo que chamamos de uma pipeline function. Este formato é necessário pois apenas assim o targets irá funcionar.

Uma vez organizado assim, devemos utilizar uma função do pacote targets para gerar um workflow do tipo target. Isso será feito da seguinte maneira:

targets::use_targets()

Isso criará um documento na raiz do seu projeto denominado _targets.R, como ilustrado na figura abaixo, que representa um diretório que segue um workflow do targets

O documento criado informará a sequência do workflow de análise que o pacote targets deve seguir. Após editar o documento para este exemplo ele ficará da seguinte forma

# _targets.R file
library(targets)
source("R/functions.R")
tar_option_set(packages = c("readr", "dplyr", "ggplot2"))
list(
  tar_target(file, "data.csv", format = "file"),
  tar_target(data, get_data(file)),
  tar_target(model, fit_model(data)),
  tar_target(plot, plot_model(model, data))
)

Neste exemplo o arquivo apresenta os seguintes componentes:

  • As funções necessárias para rodar o workflow

  • os pacotes necessários

  • uma lista que indica a sequência que o workflow deve obedecer

Para rodar o workflow via targets usamos a seguinte função

targets::tar_make()

A sequência do workflow vai iniciar e o tempo decorrido vai aparecer no console

Visualizando o workflow

Uma das vantagens do target é que podemos visualizar a sequência do workflow e se ele está atualizado ou não. Para isso usamos a seguinte função

targets::tar_visnetwork()

Esta função vai produzir um gráfico como mostrado nesta figura

Podemos também acessar o output do workflow

Identificando mudanças no workflow

Uma das maiores potencialidades do targets é identificar de maneira eficiente partes do workflow que precisam ser rodadas novamente após realizarmos mudanças na nossa pipeline. Por exemplo, vamos supor que modificamos apenas um parâmetro que afeta a estética do plot final dos resultados do modelo. Não necessitamos rodar tudo de novo, apenas a figura resultante do modelo. O targets identifica onde esta modificação foi feita e aponta a parte do workflow que precisa ser rodado novamente. Podemos identificar isso através da função tar_viznetwork() que vai gerar a seguinte figura (dado a situação descrita acima)

LS0tDQp0aXRsZTogJ0ZsdXhvIGRlIHRyYWJhbGhvIGNvbSB0YXJnZXRzJw0KYXV0aG9yOiAiR2FicmllbCBOYWthbXVyYSINCmRhdGU6ICJgciBTeXMuRGF0ZSgpYCINCm91dHB1dDogaHRtbF9kb2N1bWVudA0KLS0tDQoNCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLCBmaWcuYWxpZ24gPSAiY2VudGVyIikNCmBgYA0KDQpgYGB7ciBrbGlwcHksIGVjaG89RkFMU0UsIGluY2x1ZGU9VFJVRX0NCmtsaXBweTo6a2xpcHB5KCkNCmBgYA0KDQojIEFwcmVzZW50YcOnw6NvDQoNCkFsZ3VtYXMgYW7DoWxpc2VzIHPDo28gZXh0cmVtYW1lbnRlIGRlbW9yYWRhcyBwYXJhIHJvZGFyLCBvdSBhaW5kYSBhcHJlc2VudGFtIG11aXRvcyBwYXNzb3MgbmEgc3VhIGV4ZWN1w6fDo28uIERlc2RlIGEgbGltcGV6YSBkb3MgZGFkb3Mgb3JpZ2luYWlzLCB0cmFuc2Zvcm1hw6fDtWVzLCBtw7psdGlwbGFzIGFuw6FsaXNlcyBlIGFwcmVzZW50YcOnw7VlcyBkZXN0YXMgYW7DoWxpc2VzIGF0cmF2w6lzIGRlIGZpZ3VyYXMgb3UgdGFiZWxhcy4gVG9kbyBlc3NlIGNvbmp1bnRvIGRlIGHDp8O1ZXMgcXVhbmRvIHRvbWFkYXMgZW0gY29uanVudG8gw6kgbyBxdWUgY2hhbWFtb3MgZGUgKnBpcGVsaW5lKiBvdSAqZmx1eG8gZGUgdHJhYmFsaG8qLiBEZXZpZG8gYSBjb21wbGV4aWRhZGUgZSB0ZW1wbyBxdWUgZXN0ZSBmbHV4byBwb2RlIGFzc3VtaXIgw6kgY29tdW0gcGVyZGVybW9zIGNvbnRyb2xlIGRhIHNlcXXDqm5jaWEgcXVlIGNhZGEgdGFyZWZhIGRldmUgc2VyIGV4ZWN1dGFkYS4gDQoNCkRldmlkbyBhIGlzc28sIG11aXRhcyB2ZXplcyBub3MgcGVnYW1vcyByb2RhbmRvIG5vc3NvcyBjw7NkaWdvcyBkZSBtYW5laXJhIHJlcGV0aWRhLCBlIGFsZ3VtYXMgdmV6ZXMgc2VtIG5lbSBtZXNtbyBzYWJlciBzZSBwcmVjaXNhbW9zIHJvZMOhLWxvcy4gSXNzbyDDqSBvIHF1ZSBjaGFtYW1vcyBkZSBbbG9vcCBzaXN5cGhlYW5vXShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9TaXN5cGh1cykuIEFvIGludsOpcyBkZSByb2xhciB1bWEgcGVkcmEgbW9ycm8gYWNpbWEgdG9kYSB2ZXosIG8gcXVlIGZhemVtb3Mgw6kgcm9kYXIgYXMgbWVzbWFzIGFuw6FsaXNlcyByZXBldGlkYW1lbnRlLg0KDQoNCmBgYHtyIGVjaG89RkFMU0UsZXZhbD1UUlVFLGZpZy5jYXA9IlNpc3lwaHVzIHJvbGFuZG8gYSBwZWRyYSBtb3JybyBhY2ltYSwgcGFyYSBhIGV0ZXJuaWRhZGUuIEEgcHJpbmNpcGFsIGRpZmVyZW7Dp2EgZW50cmUgbWltIGUgU2lzeXBodXMgc8OjbyBvcyBtw7pzY3Vsb3MifQ0Ka25pdHI6OmluY2x1ZGVfZ3JhcGhpY3MoImZpZ3Mvc2lzeXBoeXMuanBlZyIpDQpgYGANCg0KDQpVbWEgc29sdcOnw6NvIHBhcmEgcXVlYnJhciBvIGxvb3Agc2lzZWZlYW5vIGUgb3RpbWl6YXIgbm9zc28gZmx1eG8gZGUgdHJhYmFsaG8gw6kgYSB1dGlsaXphw6fDo28gZG8gcGFjb3RlIGB7dGFyZ2V0c31gLiBBIHBhcnRpciBkZSBhZ29yYSwgZXNwZXJhbW9zIHF1ZSBhcyDDum5pY2FzIHBlZHJhcyByb2xhbnRlcyBxdWUgdmFtb3Mgbm9zIGRlcGFyYXIgc2VqYW0gZXNzYSBkbyB2w61kZW8gYWJhaXhvLg0KDQpgYGB7ciBlY2hvPUZBTFNFLGV2YWw9VFJVRX0NCnZlbWJlZHI6OmVtYmVkX3VybCgiaHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj1wb1h2TUJoalNXayZhYl9jaGFubmVsPTRNTGdpZ3MiKQ0KYGBgDQoNCg0KIyBPIHBhY290ZSBge3RhcmdldHN9YA0KDQpPIHBhY290ZSBge3RhcmdldHN9YCBwb3NzaWJpbGl0YSBvdGltaXphciBhIHNlcXXDqm5jaWEgZGUgdHJhYmFsaG8gKHBpcGVsaW5lKSBwb3Igb3JnYW5pemFyIGVzdGEgc2VxdcOqbmNpYSBlIGlkZW50aWZpY2FyIGHDp8O1ZXMgbm8gZmx1eG8gZGUgdHJhYmFsaG8gcXVlIGRldmVtIG91IG7Do28gZGV2ZW0gc2VyIHJlYWxpemFkYXMuDQoNCiMjIEV4ZW1wbG8NCg0KUGFyYSBpbHVzdHJhciBvIHVzbyBkbyBwYWNvdGUgdGFyZ2V0cyB2YW1vcyB1dGlsaXphciB1bSBleGVtcGxvIGNvbnRpZG8gbmEgcHLDs3ByaWEgW2RvY3VtZW50YcOnw6NvIGRvIHBhY290ZV0oaHR0cHM6Ly9ib29rcy5yb3BlbnNjaS5vcmcvdGFyZ2V0cy8pLg0KDQpOZXN0ZSBleGVtcGxvIHZhbW9zIGFuYWxpc2FyIGEgcmVsYcOnw6NvIGVudHJlIHF1YW50aWRhZGUgZGUgb3rDtG5pbyBlIHRlbXBlcmF0dXJhIGVtIHVtIGNvbmp1bnRvIGRlIGRhZG9zIHByZXNlbnRlcyBubyBwcsOzcHJpbyBSIGJhc2UgY2hhbWFkbyBgYWlycXVhbGl0eWAuIFBhcmEgdGFudG8gcHJlY2lzYW1vcyBzZWd1aXIgdW1hIHNlcXXDqm5jaWEgZGUgYW7DoWxpc2UgZGUgZGFkb3MsIHF1ZSwgYmFzaWNhbWVudGUsIGNvbnNpc3RlIGVtOg0KDQoxLiAgTGVyIGUgbWFuaXB1bGFyIGEgdGFiZWxhIGRlIGRhZG9zDQoNCjIuIFJvZGFyIHVtIG1vZGVsbyByZWxhY2lvbmFuZG8gb3rDtG5pbyBlIHRlbXBlcmF0dXJhDQoNCjMuIEdlcmFyIHJlc3VsdGFkb3MgZ3LDoWZpY29zIChmaWd1cmFzKSBwYXJhIG8gbW9kZWxvIGFqdXN0YWRvDQoNCkEgYmFzZSBkZSBkYWRvcyBwb2RlIHNlciBsaWRhIGRhIHNlZ3VpbnRlIGZvcm1hDQoNCmBgYHtyIGV2YWw9RkFMU0UsZWNobz1UUlVFfQ0KZGF0YShhaXJxdWFsaXR5KQ0KYWlycXVhbGl0eQ0KYGBgDQoNCkltYWdpbmUgcXVlIGVzdGVzIGRhZG9zIGVzdMOjbyBvcmdhbml6YWRvcyBlbSB1bSBkaXJldMOzcmlvIGxvY2FsIGluaWNpYWxpemFkbyBhIHBhcnRpciBkZSB1bSAuUnByb2plY3QgZSBlbGUgYXByZXNlbnRhIGEgc2VndWludGUgZXN0cnV0dXJhOg0KDQotIFVtYSBwYXN0YSBgZGF0YWAgY29udGVuZG8gb3MgZGFkb3MNCg0KLSBVbWEgcGFzdGEgUiBjb250ZW5kbzoNCiAgICArIHNjcmlwdCBjb20gYSBsZWl0dXJhIGUgdHJhbnNmb3JtYcOnw6NvIGRvcyBkYWRvcw0KICAgICsgc2NyaXB0IGNvbSBvIG1vZGVsbw0KICAgICsgc2NyaXB0IGNvbSBmdW7Dp8O1ZXMgcGFyYSBwbG90YXIgb3MgcmVzdWx0YWRvcyBkbyBtb2RlbG8gYWp1c3RhZG8gDQogICAgDQpFc3RhIHNlcmlhIHVtYSBwYXN0YSBvcmdhbml6YWRhLCB0YWwgY29tbyB2aW1vcyBkdXJhbnRlIGFzIGF1bGFzLiBQb3LDqW0sIHBhcmEgcXVlIG8gcGFjb3RlIHRhcmdldHMgZnVuY2lvbmEgcHJlY2lzYW1vcyB0cmFuc2Zvcm1hciBlc3RhIGVzdHJ1dHVyYSBkZSBhY29yZG8gY29tIHVtIHBpcGVsaW5lIHRhcmdldHMsIHF1ZSBwb3Igc3VhIHZleiBuZWNlc3NpdGEgZGEgc2VndWludGUgZXN0cnV0dXJhOg0KDQpgYGB7ciBlY2hvPUZBTFNFLGV2YWw9VFJVRX0NCmtuaXRyOjppbmNsdWRlX2dyYXBoaWNzKCJmaWdzL2Rpcl90YXJnZXRzLnBuZyIpDQpgYGANCg0KTmVzdGUgZGlyZXTDs3JpbyBwcmVjaXNhbW9zIHRyYW5zZm9ybWFyIGEgc2VxdcOqbmNpYSBhcHJlc2VudGFkYSBhbnRlcmlvcm1lbnRlIGVtIHVtYSBzZXF1w6puY2lhIGRlIGZ1bsOnw7Vlcy4gUG9ydGFudG8gb3Mgc2NyaXB0cyBuYSBwYXN0YSBSIHNlcsOjbyB0cmFuc2Zvcm1hZG9zIGVtIGZ1bsOnw7VlcyBxdWUgc2Vyw6NvIGNvbG9jYWRhcyBkZW50cm8gZGEgcGFzdGEgUiwgY29tIG8gbm9tZSBkZSBgZnVuY3Rpb25zLlJgLCBlIHF1ZSBhcHJlc2VudGFyw6EgYSBzZWd1aW50ZSBmb3JtYToNCg0KYGBge3IgZWNobz1GQUxTRSxldmFsPVRSVUV9DQprbml0cjo6aW5jbHVkZV9ncmFwaGljcygiZmlncy9waXBlbGluZS10YXJnZXQucG5nIikNCmBgYA0KDQpWZWphIHF1ZSBhIG1lc21hIHNlcXXDqm5jaWEgZGUgYW7DoWxpc2UgZXN0w6EgYWdvcmEgcmVwcmVzZW50YWRhIGNvbW8gZnVuw6fDtWVzLCBuYXF1aWxvIHF1ZSBjaGFtYW1vcyBkZSB1bWEgKnBpcGVsaW5lIGZ1bmN0aW9uKi4gRXN0ZSBmb3JtYXRvIMOpIG5lY2Vzc8OhcmlvIHBvaXMgYXBlbmFzIGFzc2ltIG8gdGFyZ2V0cyBpcsOhIGZ1bmNpb25hci4NCg0KVW1hIHZleiBvcmdhbml6YWRvIGFzc2ltLCBkZXZlbW9zIHV0aWxpemFyIHVtYSBmdW7Dp8OjbyBkbyBwYWNvdGUgdGFyZ2V0cyBwYXJhIGdlcmFyIHVtIHdvcmtmbG93IGRvIHRpcG8gdGFyZ2V0LiBJc3NvIHNlcsOhIGZlaXRvIGRhIHNlZ3VpbnRlIG1hbmVpcmE6DQoNCmBgYHtyIGVjaG89VFJVRSxldmFsPUZBTFNFfQ0KdGFyZ2V0czo6dXNlX3RhcmdldHMoKQ0KYGBgDQoNCklzc28gY3JpYXLDoSB1bSBkb2N1bWVudG8gbmEgcmFpeiBkbyBzZXUgcHJvamV0byBkZW5vbWluYWRvIGBfdGFyZ2V0cy5SYCwgY29tbyBpbHVzdHJhZG8gbmEgZmlndXJhIGFiYWl4bywgcXVlIHJlcHJlc2VudGEgdW0gZGlyZXTDs3JpbyBxdWUgc2VndWUgdW0gd29ya2Zsb3cgZG8gdGFyZ2V0cw0KDQpgYGB7ciBlY2hvPUZBTFNFLGV2YWw9VFJVRX0NCmtuaXRyOjppbmNsdWRlX2dyYXBoaWNzKCJmaWdzL3RhcmdldF90YXJnZXQtZmlsZS5wbmciKQ0KYGBgDQoNCk8gZG9jdW1lbnRvIGNyaWFkbyBpbmZvcm1hcsOhIGEgc2VxdcOqbmNpYSBkbyB3b3JrZmxvdyBkZSBhbsOhbGlzZSBxdWUgbyBwYWNvdGUgdGFyZ2V0cyBkZXZlIHNlZ3Vpci4gQXDDs3MgZWRpdGFyIG8gZG9jdW1lbnRvIHBhcmEgZXN0ZSBleGVtcGxvIGVsZSBmaWNhcsOhIGRhIHNlZ3VpbnRlIGZvcm1hDQoNCmBgYHtyIGVjaG89VFJVRSxldmFsPUZBTFNFfQ0KIyBfdGFyZ2V0cy5SIGZpbGUNCmxpYnJhcnkodGFyZ2V0cykNCnNvdXJjZSgiUi9mdW5jdGlvbnMuUiIpDQp0YXJfb3B0aW9uX3NldChwYWNrYWdlcyA9IGMoInJlYWRyIiwgImRwbHlyIiwgImdncGxvdDIiKSkNCmxpc3QoDQogIHRhcl90YXJnZXQoZmlsZSwgImRhdGEuY3N2IiwgZm9ybWF0ID0gImZpbGUiKSwNCiAgdGFyX3RhcmdldChkYXRhLCBnZXRfZGF0YShmaWxlKSksDQogIHRhcl90YXJnZXQobW9kZWwsIGZpdF9tb2RlbChkYXRhKSksDQogIHRhcl90YXJnZXQocGxvdCwgcGxvdF9tb2RlbChtb2RlbCwgZGF0YSkpDQopDQpgYGANCg0KTmVzdGUgZXhlbXBsbyBvIGFycXVpdm8gYXByZXNlbnRhIG9zIHNlZ3VpbnRlcyBjb21wb25lbnRlczogDQoNCi0gQXMgZnVuw6fDtWVzIG5lY2Vzc8OhcmlhcyBwYXJhIHJvZGFyIG8gd29ya2Zsb3cNCg0KLSBvcyBwYWNvdGVzIG5lY2Vzc8Ohcmlvcw0KDQotIHVtYSBsaXN0YSBxdWUgaW5kaWNhIGEgc2VxdcOqbmNpYSBxdWUgbyB3b3JrZmxvdyBkZXZlIG9iZWRlY2VyDQoNCg0KUGFyYSByb2RhciBvIHdvcmtmbG93IHZpYSB0YXJnZXRzIHVzYW1vcyBhIHNlZ3VpbnRlIGZ1bsOnw6NvDQoNCmBgYHtyIGVjaG89VFJVRSxldmFsPUZBTFNFfQ0KdGFyZ2V0czo6dGFyX21ha2UoKQ0KYGBgDQoNCkEgc2VxdcOqbmNpYSBkbyB3b3JrZmxvdyB2YWkgaW5pY2lhciBlIG8gdGVtcG8gZGVjb3JyaWRvIHZhaSBhcGFyZWNlciBubyBjb25zb2xlDQoNCmBgYHtyIGVjaG89RkFMU0UsZXZhbD1UUlVFfQ0Ka25pdHI6OmluY2x1ZGVfZ3JhcGhpY3MoImZpZ3MvdGFyZ2V0X3J1bi5wbmciKQ0KYGBgDQoNCiMjIyBWaXN1YWxpemFuZG8gbyB3b3JrZmxvdw0KDQpVbWEgZGFzIHZhbnRhZ2VucyBkbyB0YXJnZXQgw6kgcXVlIHBvZGVtb3MgdmlzdWFsaXphciBhIHNlcXXDqm5jaWEgZG8gd29ya2Zsb3cgZSBzZSBlbGUgZXN0w6EgYXR1YWxpemFkbyBvdSBuw6NvLiBQYXJhIGlzc28gdXNhbW9zIGEgc2VndWludGUgZnVuw6fDo28NCg0KYGBge3IgZWNobz1UUlVFLGV2YWw9RkFMU0V9DQp0YXJnZXRzOjp0YXJfdmlzbmV0d29yaygpDQpgYGANCg0KRXN0YSBmdW7Dp8OjbyB2YWkgcHJvZHV6aXIgdW0gZ3LDoWZpY28gY29tbyBtb3N0cmFkbyBuZXN0YSBmaWd1cmENCg0KYGBge3IgZWNobz1GQUxTRSxldmFsPVRSVUV9DQprbml0cjo6aW5jbHVkZV9ncmFwaGljcygiZmlncy90YXJnZXRfdml6LnBuZyIpDQpgYGANCg0KUG9kZW1vcyB0YW1iw6ltIGFjZXNzYXIgbyBvdXRwdXQgZG8gd29ya2Zsb3cNCg0KYGBge3IgZWNobz1GQUxTRSxldmFsPVRSVUV9DQprbml0cjo6aW5jbHVkZV9ncmFwaGljcygiZmlncy90YXJnZXRfdml6XzIucG5nIikNCmBgYA0KDQojIyMgSWRlbnRpZmljYW5kbyBtdWRhbsOnYXMgbm8gd29ya2Zsb3cNCg0KVW1hIGRhcyBtYWlvcmVzIHBvdGVuY2lhbGlkYWRlcyBkbyB0YXJnZXRzIMOpIGlkZW50aWZpY2FyIGRlIG1hbmVpcmEgZWZpY2llbnRlIHBhcnRlcyBkbyB3b3JrZmxvdyBxdWUgcHJlY2lzYW0gc2VyIHJvZGFkYXMgbm92YW1lbnRlIGFww7NzIHJlYWxpemFybW9zIG11ZGFuw6dhcyBuYSBub3NzYSBwaXBlbGluZS4gUG9yIGV4ZW1wbG8sIHZhbW9zIHN1cG9yIHF1ZSBtb2RpZmljYW1vcyBhcGVuYXMgdW0gcGFyw6JtZXRybyBxdWUgYWZldGEgYSBlc3TDqXRpY2EgZG8gcGxvdCBmaW5hbCBkb3MgcmVzdWx0YWRvcyBkbyBtb2RlbG8uIE7Do28gbmVjZXNzaXRhbW9zIHJvZGFyIHR1ZG8gZGUgbm92bywgYXBlbmFzIGEgZmlndXJhIHJlc3VsdGFudGUgZG8gbW9kZWxvLiBPIHRhcmdldHMgaWRlbnRpZmljYSBvbmRlIGVzdGEgbW9kaWZpY2HDp8OjbyBmb2kgZmVpdGEgZSBhcG9udGEgYSBwYXJ0ZSBkbyB3b3JrZmxvdyBxdWUgcHJlY2lzYSBzZXIgcm9kYWRvIG5vdmFtZW50ZS4gUG9kZW1vcyBpZGVudGlmaWNhciBpc3NvIGF0cmF2w6lzIGRhIGZ1bsOnw6NvIGB0YXJfdml6bmV0d29yaygpYCBxdWUgdmFpIGdlcmFyIGEgc2VndWludGUgZmlndXJhIChkYWRvIGEgc2l0dWHDp8OjbyBkZXNjcml0YSBhY2ltYSkNCg0KYGBge3IgZWNobz1GQUxTRSxldmFsPVRSVUV9DQprbml0cjo6aW5jbHVkZV9ncmFwaGljcygiZmlncy90YXJnZXRfbXVkYW5jYS5wbmciKQ0KYGBgDQoNCg0K