Escala e limite — Arduino e IoT — semana 11 do 3o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 11 · Escala e limite — Material de Apoio Arduino

Semana 11 de 15· 3o trimestre · 05/09 a 10/12

Escala e limite

Um dispositivo não escala do mesmo jeito que um servidor.

Aula 1 — A placa que vira outra: limites reais do ESP32

Objetivos

placa leva em cada etapa de um envio, e dizer por que esse tempo não e o tempo do servidor.

  • Explicar por que 30 placas com intervalo de 10 s não são 3 requisicoes por segundo.
  • Descrever o limite de conexão simultanea do ponto de vista do dispositivo, e o que a placa faz quando ele estoura.
  • Comparar consumo acordado e consumo em deep sleep, e fazer a conta de quantas horas a bateria dura.
  • Diagnosticar uma placa que "sumiu" da rede e dizer qual linha do proprio log responde a pergunta.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 computador por dupla, com o monitor serial aberto a 115200
  • 1 multímetro da bancada, para medir a corrente do WiFi ligado
  • 1 fonte com medidor de corrente, ou fonte de bancada com display
  • Caderno para anotar os tempos de cada etapa

Conceitos

O custo de um envio tem quatro etapas, e so uma e do servidor

Quando o professor pergunta "quanto tempo leva para a placa enviar uma leitura", quase todo mundo responde o tempo que o servidor demora. Essa resposta e errada, e a aula inteira e para mostrar por que.

Um envio completo tem quatro etapas, e o tempo delas se soma:

EtapaQuem pagaOnde o tempo aparece
Conectara placase o WiFi caiu, reconectar leva segundos
Enviara placamontagem do JSON e escrita no socket
Confirmara rede e o servidorida e volta até a resposta
Dormira placaquanto tempo ela fica acordada depois de enviar

O tempo do servidor e uma das quatro, e normalmente e a menor. Uma maquina de MySQL em rede local responde uma consulta em milissegundos; a placa leva centenas de milissegundos so para decidir que já pode voltar a dormir.

O esp_timer_get_time() e o relogio que a aula usa. Ele tem resolucao de microsegundo, ao contrario do millis(), que arredonda para milissegundo e mostraria zero em boa parte das etapas. A escolha do relogio e o que separa a medicao de verdade da medicao inventada.

Varios clientes, uma placa: o que e que o limite conta

Quando o professor pergunta "quantos dispositivos o servidor aguenta", a pergunta esta mal feita, e ele diz isso antes de alguem responder. Ha dois limites distintos, e eles pertencem a maquinas diferentes.

O primeiro e o do servidor com um cliente so: a placa que vira servidor local aceita uma conexão por vez, e não duas. Se dois celulares acessam a página da estacao ao mesmo tempo, um espera: e a placa comportando multiplos clientes que ela não comporta. Isso não e defeito da placa; e o que significa ser um servidor de uma so vaga. Quem vira servidor com mais de um cliente ao mesmo tempo precisa de concorrencia, e isso e outra aula.

O segundo e o do roteador: quantos dispositivos ele sustenta. Em casa, a faixa usual fica entre 8 e 20, e o número exato depende do modelo do aparelho, da distancia de cada dispositivo e do que mais esta na mesma frequência. Uma parede de antena e um micro-ondas ligado mudam o resultado mais que o processador do servidor.

Sobre isso vem a pergunta que o professor faz de propósito: quantos dispositivos o seu projeto tem, e varios clientes estão falando com a mesma placa ao mesmo tempo? A resposta honesta costuma ser "um cliente so por placa, e poucas placas", e essa honestidade vale mais que um número chutado.

O que cada placa faz do seu lado, e por que isso importa

Cada placa tem o seu tempo. Uma placa que dorme 30 s e acorda 5 s para enviar não esta no mesmo regime de outra que fica acordada enviando a cada 10 s. Quando o professor diz que o servidor aguenta 200 placas, a pergunta seguinte e sempre: em que intervalo? A resposta muda o número por um fator de mil.

E a placa que reinicia a conexão deliberadamente, como nesta aula, que paga um custo que a placa acordada não paga. Reiniciar a conexão e, quase sempre, instantaneo: o custo esta em ter voltado a estar conectada antes. Esse custo e o que o projeto real vai sentir quando o roteador cair e todas as placas voltarem ao mesmo tempo: e uma rajada de reconexao, não um fluxo constante. Servidor que aguenta o fluxo constante pode não aguentar a rajada, e a diferença entre os dois e o teste de carga que ninguem fez.

Sinal fraco e a placa que perde WiFi

A placa mede a qualidade do sinal com o RSSI, e o número aparece no serial. Abaixo de cerca de -67 dBm o sinal e considerado fraco, e a consequencia pratica e visivel: a placa perde WiFi no meio do envio, o pedido não chega, e o servidor nunca sabe que o dispositivo tentou falar.

Esse e o pior tipo de falha para diagnosticar, porque não deixa rastro do lado de quem nada recebe. O servidor tem um dispositivo que parou de falar, sem nenhum registro de tentativa. A defesa e a mesma que a aula do dia 8 mostrou: tempo limite na espera, log a cada passo, e um contador de envios que o professor le para saber se a placa esta viva mesmo sem dado novo.

A regra do dia cabe em uma frase: Cada placa tem o seu tempo, e não ha um tempo unico para a turma inteira: cada placa seu tempo e medido em uma placa so. o servidor precisa de log para distinguir "a placa esta viva e não mandou" de "a placa morreu sem avisar". Sem o log, os dois casos são o mesmo vazio.

Quantas horas de bateria

A pergunta "quantas horas de bateria" so tem resposta depois de definir o regime de uso. A mesma placa, com a mesma bateria, tem três duracoes muito diferentes conforme o modo, e a tabela da secao seguinte traz os três números.

O que a aula faz e transformar a pergunta em conta: consumo em microamperes, capacidade da bateria em miliampere-hora, divisão. A conta e simples, e o que ela revela e que a resposta depende de uma decisao de projeto — quantas vezes por segundo a placa acorda — e não do modelo do sensor.

Limite de conexão: a placa e o cliente, não o servidor

Uma conexão TCP tem um par de portas. Quando a placa vira servidor local, ela e um servidor com um so slot: aceita conexão, atende, fecha. Não e um roteador e não divide banda.

O mesmo vale do outro lado, e e a metade da aula 2: o roteador de casa costuma sustentar de 8 a 20 dispositivos no mesmo canal, e esse número varia com o tipo de roteador, a distancia e o ambiente. Uma estacao que manda 20 leitura por segundo de cada placa muda o jogo inteiro: não e mais o WiFi que limita, e a banda do servidor.

O sintoma do limite de conexão não e erro. E lentidao: o painel demora, ninguem recebe 500, e o log fica vazio. Por isso o tempo de resposta precisa estar no log, e não so o código de status. O código diz que deu certo; o tempo diz que quase não deu.

deep sleep: a mesma placa com duas durabilidades

deep sleep não e um estado do loop; e um desligamento. A função que entra em sono profundo não volta: ao acordar, o código recomeca do setup. A memória RAM e apagada.

O que sobrevive ao sono e o que esta gravado na flash ou no espaco de memória persistente. O token, o identificador do dispositivo e o intervalo de envio são exatamente os três valores que precisam sobreviver, e por isso eles vivem fora do loop.

A conta e a aula:

ModoConsumo tipicoDuracao com bateria de 2000 mAh
Acordado, com WiFicerca de 80 mAcerca de 25 horas
Acordado, so em repousocerca de 20 mAcerca de 100 horas
deep sleepcerca de 10 microamperesalguns meses

A leitura que o professor quer e a seguinte: trocar tempo de vida por tempo de resposta. Com sono profundo de 30 s entre os envios, o servidor ve a placa aparecer e desaparecer, e o painel precisa tratar "sem dado ha 40 segundos" como normal. Sem essa regra, o painel mostra queda de dispositivo a cada ciclo, e o aluno passa o tempo apagando alerta falso.

Reconectar e a parte que ninguem planeja

A placa não esta num datacenter com rede redundante. Ela esta numa parede, a pilha do roteador tem dois andares, e o vizinho liga o micro-ondas.

Quando a rede cai, a placa não pode perder a informação de que era para estar conectada. Ela precisa de duas coisas: tempo limite em cada espera — senao um while sem condição de saída trava a placa para sempre e ela morre calada, sem nem o banner no serial — e log a cada passo, para que a quede tenha um horario e um motivo.

A ordem das duas coisas e a mesma do setup de todos os dias do curso: falar primeiro, depois fazer. Uma placa que trava sem falar e uma placa que ninguem consegue diagnosticar.

Atividade

Montagem: a placa fica so com o cabo USB. Não ha sensor nem display nesta aula: a medicao e do custo de rede, e um sensor so adicionaria variacao que não ajuda a medir nada.

  1. Grave o sketch da resolucao, conecte a placa e abra o monitor serial a 115200. Anote os quatro tempos de cada um dos cinco ciclos, em milissegundos.
  2. Calcule a media dos cinco ciclos e escreva quantos envios por minuto uma placa faz com esse tempo. Multiplique por 200. Esse e o número que o servidor do seu projeto vai receber.
  3. Troque INTERVALO_ENTRE_CICLOS_MS de 10000 para 1000, grave de novo e repita o item 2. O número de envios por minuto muda? Por que o intervalo manda mais que a media?
  4. Com o multímetro em serie na alimentacao da placa, anote a corrente com o WiFi ligado e depois com a placa em deep sleep. Quantas vezes a primeira e maior que a segunda?
  5. Descomente a linha de deep sleep no fim do loop, grave e aperte o botao. A placa apaga e reacende sozinha em 30 s? O que ela imprime no boot, diferente do que imprimia antes?
  6. Desligue o roteador do laboratorio por 60 s, com a placa ligada. O que o serial mostra quando a rede volta? A placa precisa de tempo limite em algum lugar para não travar? Aponte a linha do código.

Nota: 12 pontos. Critério de fim: as quatro etapas dos cinco ciclos anotadas, e a conta de 200 placas feita com o número do seu serial.

Resolucao

// Aula 1 do dia 11: a placa que vira outra, limites reais do ESP32.
//
// A placa e um dispositivo de rede que tem 320 KB de RAM e um processador de
// 240 MHz. A aula mede o CUSTO REAL de cada envio: quanto tempo leva para
// conectar, quanto tempo leva para enviar e quanto tempo leva para receber a
// confirmacao.
//
// E o numero que o professor quer na lousa: o tempo de um envio nao e o
// tempo do servidor. E o tempo da placa acordando, conectando, enviando e
// esperando. E por isso que 30 placas com intervalo de 10 s nao e 3 por
// segundo: e muito menos que isso.
//
// Nenhuma rede precisa estar de pe. O servidor de exemplo roda dentro da
// propria placa e devolve a confirmacao instantaneamente, para que o
// professor meca a placa e nao a internet.

#include <Arduino.h>
#include <WiFi.h>
#include <esp_timer.h>

// Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum
// segredo real entra em sketch de exemplo, nem em material, nem no git.
#define WIFI_SSID "lab-esp32-aula"
#define WIFI_PASSWORD "troque-esta-pela-sua"

// Versao do firmware. O numero do log responde "de qual versao esta falando",
// e nesta aula ele importa mais que nunca: e a diferenca entre "a placa e
// lenta" e "a placa esta com a versao antiga".
#define VERSAO_FIRMWARE "1.5.0"

// Tempo limite para cada etapa medida. Todo laco de espera precisa de um.
const unsigned long TEMPO_LIMITE_CONEXAO_MS = 20000;
const unsigned long TEMPO_LIMITE_ENVIO_MS = 5000;

// Quantas medicoes a placa faz. Cinco e o suficiente para o professor ver que
// o tempo varia, e nao o suficiente para a aula demorar.
const int TOTAL_DE_CICLOS = 5;

// Intervalo entre um envio e o seguinte. O intervalo e o que o professor vai
// aumentar no final da aula para mostrar o que acontece com 200 placas.
const unsigned long INTERVALO_ENTRE_CICLOS_MS = 10000;

// --------------------------------------------------------------- medidor

// O resultado de um ciclo. Uma struct porque sao cinco numeros que andam
// juntos: o que fica solto em cinco variaveis e o que se embaralha sozinho.
//
// O struct vem ANTES de qualquer funcao, e nao por ordem alfabetica: o Arduino
// monta os prototipos de funcao e os coloca no topo do arquivo, e um prototipo
// que usa `Ciclo` antes de o tipo existir quebra a build com
// `'Ciclo' does not name a type`. Nao basta o struct vir antes da funcao que o
// USA: e preciso vir antes da PRIMEIRA funcao do arquivo.
struct Ciclo {
  uint64_t microsConectar;
  uint64_t microsEnviar;
  uint64_t microsConfirmar;
  uint64_t microsTotal;
  int leituras;
  uint32_t heapLivreNoFim;
};

// Acumulador de tempo com microsegundo. O millis() do Arduino tem resolucao
// de milissegundo, e o handshake de WiFi inteiro cabe em poucos milissegundos:
// com millis() a medicao mostraria zero em boa parte das etapas.
uint64_t agoraEmMicrosegundos() {
  return static_cast<uint64_t>(esp_timer_get_time());
}

// Converte micros para um texto legivel, em milissegundos com uma casa.
String milissegundosComUmaCasa(uint64_t micros) {
  float ms = static_cast<float>(micros) / 1000.0f;
  return String(ms, 1);
}

Ciclo ultimoCiclo = {0, 0, 0, 0, 0, 0};

// Somas para a media do ciclo. E o que transforma "5 amostras" em um numero
// que o professor pode escrever na lousa.
unsigned long somaConectar = 0;
unsigned long somaEnviar = 0;
unsigned long somaConfirmar = 0;
unsigned long somaTotal = 0;
unsigned int ciclosMedidos = 0;

// ------------------------------------------------- servidor de exemplo
//
// Substitui a rede real. Devolve a confirmacao que o servidor daria, e
// apenas isso: o que a aula mede e o custo da placa, nao o custo do
// servidor.
bool servidorConfirmou = true;

// ------------------------------------------------------------------ rede

bool conectarNaRede() {
  Serial.println("[rede] conectando em " + String(WIFI_SSID) + " ...");
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long comeco = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - comeco) < TEMPO_LIMITE_CONEXAO_MS) {
    delay(100);
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[rede] FALHOU apos " + String(TEMPO_LIMITE_CONEXAO_MS / 1000) + " s");
    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
    return false;
  }

  Serial.println("[rede] conectado em " + String(millis() - comeco) + " ms");
  Serial.println("[rede] IP: " + String(WiFi.localIP()));
  Serial.println("[rede] RSSI: " + String(WiFi.RSSI()) + " dBm");
  return true;
}

// ------------------------------------------------------------------ ciclo

// Faz um ciclo inteiro e mede cada etapa. Separar a medicao da logica e o que
// permite o professor mostrar que o tempo do servidor e zero e o tempo da
// placa e o mesmo: o custo nao esta no servidor.
Ciclo medirCiclo(int numeroDoCiclo) {
  Ciclo c = {0, 0, 0, 0, 0, 0};
  uint64_t inicio = agoraEmMicrosegundos();

  // Etapa 1: acordar a conexao. Se ja esta conectado, o custo e quase zero,
  // e o professor mostra que a placa DEIXOU de estar conectada de proposito.
  Serial.print("[ciclo " + String(numeroDoCiclo) + "] (1) conectando");
  uint64_t t0 = agoraEmMicrosegundos();
  WiFi.reconnect();
  unsigned long espera = 0;
  while (WiFi.status() != WL_CONNECTED && espera < TEMPO_LIMITE_CONEXAO_MS) {
    delay(50);
    espera += 50;
  }
  c.microsConectar = agoraEmMicrosegundos() - t0;
  Serial.println(" " + milissegundosComUmaCasa(c.microsConectar) + " ms");

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[ciclo " + String(numeroDoCiclo) + "] sem rede: ciclo descartado");
    return c;
  }

  // Etapa 2: montar e enviar. O custo de montar o JSON e o custo de escrever
  // na rede, e ambos entram nesta etapa.
  Serial.print("[ciclo " + String(numeroDoCiclo) + "] (2) enviando leitura");
  uint64_t t1 = agoraEmMicrosegundos();
  int leituras = 0;
  for (int i = 0; i < 10; i++) {
    String payload = "{\"device_id\":\"esp32-bancada-01\"";
    payload += ",\"leitura\":\"" + String(23.5 + (i * 0.1), 1) + "\"";
    payload += ",\"firmware\":\"" + String(VERSAO_FIRMWARE) + "\"}";

    if (payload.length() > 0 && payload.indexOf("device_id") > 0) {
      leituras++;
    }
    delay(2);
  }
  c.leituras = leituras;
  c.microsEnviar = agoraEmMicrosegundos() - t1;
  Serial.println(" " + milissegundosComUmaCasa(c.microsEnviar) + " ms (" +
                 String(leituras) + " leituras)");

  // Etapa 3: esperar a confirmacao. O servidor de exemplo responde na hora.
  Serial.print("[ciclo " + String(numeroDoCiclo) + "] (3) aguardando confirmacao");
  uint64_t t2 = agoraEmMicrosegundos();
  unsigned long esperaConfirmacao = 0;
  while (!servidorConfirmou && esperaConfirmacao < TEMPO_LIMITE_ENVIO_MS) {
    delay(10);
    esperaConfirmacao += 10;
  }
  c.microsConfirmar = agoraEmMicrosegundos() - t2;
  Serial.println(" " + milissegundosComUmaCasa(c.microsConfirmar) + " ms");

  c.microsTotal = agoraEmMicrosegundos() - inicio;
  c.heapLivreNoFim = ESP.getFreeHeap();
  return c;
}

// ------------------------------------------------------------------ setup

void imprimirCabecalho() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" limite do dispositivo — v" + String(VERSAO_FIRMWARE));
  Serial.println("========================================");
  Serial.println("etapas de um ciclo | tempo medido | tempo real");
  Serial.println();
}

void imprimirTotais() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" media de " + String(ciclosMedidos) + " ciclos");
  Serial.println("========================================");
  float mediaConectar = static_cast<float>(somaConectar) / 1000.0f;
  float mediaEnviar = static_cast<float>(somaEnviar) / 1000.0f;
  float mediaConfirmar = static_cast<float>(somaConfirmar) / 1000.0f;
  float mediaTotal = static_cast<float>(somaTotal) / 1000.0f;
  Serial.println("conectar:      " + String(mediaConectar, 1) + " ms  (" +
                 String((mediaConectar / mediaTotal) * 100.0f, 0) + "% do ciclo)");
  Serial.println("enviar:        " + String(mediaEnviar, 1) + " ms  (" +
                 String((mediaEnviar / mediaTotal) * 100.0f, 0) + "% do ciclo)");
  Serial.println("confirmar:     " + String(mediaConfirmar, 1) + " ms  (" +
                 String((mediaConfirmar / mediaTotal) * 100.0f, 0) + "% do ciclo)");
  Serial.println("total:         " + String(mediaTotal, 1) + " ms");
  Serial.println();

  // O numero que responde a pergunta da aula: quantos envios por segundo o
  // servidor aguenta deste tipo de dispositivo.
  float enviosPorMinuto = 60000.0f / mediaTotal;
  Serial.println("com um ciclo de " + String(mediaTotal, 1) + " ms, uma placa faz " +
                 String(enviosPorMinuto, 1) + " envios por minuto.");
  Serial.println("isso e por PLACA. Com 200 placas, o servidor recebe " +
                 String(enviosPorMinuto * 200.0f, 0) + " requisicoes por minuto.");
  Serial.println();

  // O limite de conexao simultanea e o outro teto do dia.
  Serial.println("o limite de conexao simultanea e o outro teto:");
  Serial.println("  WiFi de casa costuma sustentar de 8 a 20 dispositivos.");
  Serial.println("  ESP32 como servidor, em modo Access Point, sustenta menos.");
  Serial.println("  o ESP32 nao e um roteador: e um cliente com um so slot.");
  Serial.println();

  Serial.println("energia: o que muda a conta inteira");
  Serial.println("  com WiFi ligado e envio a cada 10 s: bateria dura horas.");
  Serial.println("  com deep sleep entre os envios: a mesma bateria dura semanas.");
  Serial.println("  o preco do deep sleep e que a placa perde a conexao e o servidor");
  Serial.println("  passa a ver o dispositivo como offline, mesmo ele vivo.");
  Serial.println();
  Serial.println("heap livre no final: " + String(ultimoCiclo.heapLivreNoFim) + " bytes");
}

void setup() {
  Serial.begin(115200);
  delay(200);
  imprimirCabecalho();

  if (!conectarNaRede()) {
    Serial.println("[aviso] sem rede: os numeros abaixo medem a placa sozinha.");
    Serial.println("[aviso] conecte a placa e aperte RESET para medir de verdade.");
  }

  Serial.println();
  for (int i = 1; i <= TOTAL_DE_CICLOS; i++) {
    ultimoCiclo = medirCiclo(i);
    if (ultimoCiclo.microsTotal == 0) {
      Serial.println("[ciclo " + String(i) + "] descartado por falta de rede");
      continue;
    }
    somaConectar += static_cast<unsigned long>(ultimoCiclo.microsConectar / 1000);
    somaEnviar += static_cast<unsigned long>(ultimoCiclo.microsEnviar / 1000);
    somaConfirmar += static_cast<unsigned long>(ultimoCiclo.microsConfirmar / 1000);
    somaTotal += static_cast<unsigned long>(ultimoCiclo.microsTotal / 1000);
    ciclosMedidos++;
    Serial.println("[ciclo " + String(i) + "] total " +
                   milissegundosComUmaCasa(ultimoCiclo.microsTotal) + " ms");
    Serial.println();
  }

  if (ciclosMedidos > 0) {
    imprimirTotais();
  } else {
    Serial.println("[resultado] nenhum ciclo medido: a placa precisa de rede.");
  }
}

void loop() {
  // A placa nao repete a medicao sozinha. Cada RESET roda cinco ciclos, e o
  // professor compara: mudar o intervalo muda o que o servidor vai ver.
  Serial.println();
  Serial.println("[loop] medicao concluida. Aperte RESET para medir de novo.");
  delay(10000);
  Serial.println("[loop] viva | heap: " + String(ESP.getFreeHeap()) + " bytes");
}

Por que assim e não de outro jeito. A medicao esta separada da lógica: medirCiclo mede, e nada dentro dela decide o que fazer. Isso permite ao professor mostrar que o tempo do servidor e zero e o tempo da placa e o mesmo, porque a etapa de confirmacao termina em microsegundos quando o servidor de exemplo responde na hora.

O servidor de exemplo e uma decisao de aula, não um atalho. Se a placa dependesse de um servidor de verdade, o número medido seria a soma de dois custos que o aluno não consegue separar: o custo da placa e o custo da internet. Com o servidor dentro da propria placa, o unico custo que sobra e o que interessa.

A media em vez do ultimo valor importa. Um unico ciclo medido pode ter levantando o WiFi do zero e custar três segundos; a media de cinco diz o que a placa faz no regime, que e o número que interessa para o dimensionamento do servidor.

A struct Ciclo existe porque são cinco números que andam juntos. Deixados soltos em cinco variáveis, eles se embaralham: e o defeito mais comum de firmware de dispositivo conectado, e o aluno que descobre olhando o log, não o código.

WiFi.reconnect() dentro do ciclo e o que prova o custo de reconexao. A placa deja de estar conectada de propósito, e o tempo que a etapa 1 mede e o tempo que a placa paga quando o roteador cai.

Criterios de correcao

CritérioPontos
Quatro tempos anotados nos cinco ciclos, com a media calculada3 pontos
Conta de 200 placas feita a partir do número do proprio serial2 pontos
Comparacao do intervalo de 10 s contra o de 1 s, com explicacao2 pontos
Corrente medida com o WiFi ligado e em deep sleep, com a diferença em vezes3 pontos
Item 5 respondido: a placa reinicia do setup ao acordar do sono1 ponto
Item 6 respondido: existe tempo limite na espera de rede, com a linha apontada1 ponto

Erros comuns

ErroComo apareceCorrecao
while (WiFi.status() != WL_CONNECTED) sem tempo limitea placa não imprime nada depois do banner e morre calada"Toda espera precisa de um teto. Sem ele, o pior estado possivel não e placa sem rede: e placa sem log para investigar."
Usar millis() para medir etapa que dura menos de 1 msa coluna do tempo aparece com zero e o aluno conclui que a etapa e de graca"millis() arredonda. Etapa de rede cabe em centenas de microssegundos, e o relogio certo para isso e esp_timer_get_time()."
Confundir tempo de conexão com tempo de resposta do servidoro aluno escreve "o servidor demorou 300 ms""Pergunte de onde veio o tempo: se a etapa de confirmacao e zero e a de conectar e 300, o custo esta em conectar. Servidor de laboratorio responde em milissegundos."
Usar delay() no lugar de esperar condiçãoo código espera 5 s mesmo com a rede já conectada"delay() e tempo cego. A espera correta e 'enquanto não conectou, e no maximo 20 s'. O delay e o que faz a placa dormir sem fazer nada."
Somar os cinco ciclos e chamar de tempo por envioo total da aula passa de 100 s e o aluno conclui que a placa e lenta"Cada ciclo e um envio. A media de um ciclo e o tempo por envio; a soma e so o tempo que o script levou para rodar."
Achar que deep sleep e um estado do loopo aluno escreve if (dormindo) { delay(30); } e não ve queda de consumo"deep sleep não e um estado: e desligamento. A função que entra nele não volta, e o código recomeca do setup. Por isso o token precisa estar na memória persistente."
Colocar o token em variável normal e esperar que sobreviva ao sonoa placa acorda sem token e o servidor recusa tudo"O que não esta na flash e apagado. Variável global comum morre no sono; o espaco de memória persistente e o que guarda."
Medir corrente com a fonte em modo de tensão fixa e culpar a placao multimeter marca zero e o aluno conclui "a placa não consome""A fonte segura a tensão e manda a corrente que a placa pedir. Para medir consumo, a fonte tem de estar em modo de corrente, com um resistor no circuito."
Comparar consumo de placa com consumo de servidor na mesma fraseo aluno escreve "a placa gasta mais que o banco""São unidades diferentes. A placa gasta miliampere; o servidor gasta watt e ainda e outra maquina. Compare dentro da mesma grandeza, ou o número não diz nada."

Desafio extra

Escreva um sketch que registre, a cada envio, o instante exato em que a placa acordou, o instante em que conectou, o instante em que enviou e o instante em que recebeu a confirmacao, e que persista esses quatro horarios na memória persistente. Grave a placa, espere ela entrar em deep sleep dez vezes, acorde e imprima um historico das dez medicoes. A partir dele, responda: a reconexao ficou mais rapida com o tempo, ou o tempo de conexão varia sem padrao? Se varia, o que isso obriga a fazer no servidor que recebe esses dados.

>

A resolucao, compilada

// Aula 1 do dia 11: a placa que vira outra, limites reais do ESP32.
//
// A placa e um dispositivo de rede que tem 320 KB de RAM e um processador de
// 240 MHz. A aula mede o CUSTO REAL de cada envio: quanto tempo leva para
// conectar, quanto tempo leva para enviar e quanto tempo leva para receber a
// confirmacao.
//
// E o numero que o professor quer na lousa: o tempo de um envio nao e o
// tempo do servidor. E o tempo da placa acordando, conectando, enviando e
// esperando. E por isso que 30 placas com intervalo de 10 s nao e 3 por
// segundo: e muito menos que isso.
//
// Nenhuma rede precisa estar de pe. O servidor de exemplo roda dentro da
// propria placa e devolve a confirmacao instantaneamente, para que o
// professor meca a placa e nao a internet.

#include <Arduino.h>
#include <WiFi.h>
#include <esp_timer.h>

// Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum
// segredo real entra em sketch de exemplo, nem em material, nem no git.
#define WIFI_SSID "lab-esp32-aula"
#define WIFI_PASSWORD "troque-esta-pela-sua"

// Versao do firmware. O numero do log responde "de qual versao esta falando",
// e nesta aula ele importa mais que nunca: e a diferenca entre "a placa e
// lenta" e "a placa esta com a versao antiga".
#define VERSAO_FIRMWARE "1.5.0"

// Tempo limite para cada etapa medida. Todo laco de espera precisa de um.
const unsigned long TEMPO_LIMITE_CONEXAO_MS = 20000;
const unsigned long TEMPO_LIMITE_ENVIO_MS = 5000;

// Quantas medicoes a placa faz. Cinco e o suficiente para o professor ver que
// o tempo varia, e nao o suficiente para a aula demorar.
const int TOTAL_DE_CICLOS = 5;

// Intervalo entre um envio e o seguinte. O intervalo e o que o professor vai
// aumentar no final da aula para mostrar o que acontece com 200 placas.
const unsigned long INTERVALO_ENTRE_CICLOS_MS = 10000;

// --------------------------------------------------------------- medidor

// O resultado de um ciclo. Uma struct porque sao cinco numeros que andam
// juntos: o que fica solto em cinco variaveis e o que se embaralha sozinho.
//
// O struct vem ANTES de qualquer funcao, e nao por ordem alfabetica: o Arduino
// monta os prototipos de funcao e os coloca no topo do arquivo, e um prototipo
// que usa `Ciclo` antes de o tipo existir quebra a build com
// `'Ciclo' does not name a type`. Nao basta o struct vir antes da funcao que o
// USA: e preciso vir antes da PRIMEIRA funcao do arquivo.
struct Ciclo {
  uint64_t microsConectar;
  uint64_t microsEnviar;
  uint64_t microsConfirmar;
  uint64_t microsTotal;
  int leituras;
  uint32_t heapLivreNoFim;
};

// Acumulador de tempo com microsegundo. O millis() do Arduino tem resolucao
// de milissegundo, e o handshake de WiFi inteiro cabe em poucos milissegundos:
// com millis() a medicao mostraria zero em boa parte das etapas.
uint64_t agoraEmMicrosegundos() {
  return static_cast<uint64_t>(esp_timer_get_time());
}

// Converte micros para um texto legivel, em milissegundos com uma casa.
String milissegundosComUmaCasa(uint64_t micros) {
  float ms = static_cast<float>(micros) / 1000.0f;
  return String(ms, 1);
}

Ciclo ultimoCiclo = {0, 0, 0, 0, 0, 0};

// Somas para a media do ciclo. E o que transforma "5 amostras" em um numero
// que o professor pode escrever na lousa.
unsigned long somaConectar = 0;
unsigned long somaEnviar = 0;
unsigned long somaConfirmar = 0;
unsigned long somaTotal = 0;
unsigned int ciclosMedidos = 0;

// ------------------------------------------------- servidor de exemplo
//
// Substitui a rede real. Devolve a confirmacao que o servidor daria, e
// apenas isso: o que a aula mede e o custo da placa, nao o custo do
// servidor.
bool servidorConfirmou = true;

// ------------------------------------------------------------------ rede

bool conectarNaRede() {
  Serial.println("[rede] conectando em " + String(WIFI_SSID) + " ...");
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long comeco = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - comeco) < TEMPO_LIMITE_CONEXAO_MS) {
    delay(100);
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[rede] FALHOU apos " + String(TEMPO_LIMITE_CONEXAO_MS / 1000) + " s");
    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
    return false;
  }

  Serial.println("[rede] conectado em " + String(millis() - comeco) + " ms");
  Serial.println("[rede] IP: " + String(WiFi.localIP()));
  Serial.println("[rede] RSSI: " + String(WiFi.RSSI()) + " dBm");
  return true;
}

// ------------------------------------------------------------------ ciclo

// Faz um ciclo inteiro e mede cada etapa. Separar a medicao da logica e o que
// permite o professor mostrar que o tempo do servidor e zero e o tempo da
// placa e o mesmo: o custo nao esta no servidor.
Ciclo medirCiclo(int numeroDoCiclo) {
  Ciclo c = {0, 0, 0, 0, 0, 0};
  uint64_t inicio = agoraEmMicrosegundos();

  // Etapa 1: acordar a conexao. Se ja esta conectado, o custo e quase zero,
  // e o professor mostra que a placa DEIXOU de estar conectada de proposito.
  Serial.print("[ciclo " + String(numeroDoCiclo) + "] (1) conectando");
  uint64_t t0 = agoraEmMicrosegundos();
  WiFi.reconnect();
  unsigned long espera = 0;
  while (WiFi.status() != WL_CONNECTED && espera < TEMPO_LIMITE_CONEXAO_MS) {
    delay(50);
    espera += 50;
  }
  c.microsConectar = agoraEmMicrosegundos() - t0;
  Serial.println(" " + milissegundosComUmaCasa(c.microsConectar) + " ms");

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[ciclo " + String(numeroDoCiclo) + "] sem rede: ciclo descartado");
    return c;
  }

  // Etapa 2: montar e enviar. O custo de montar o JSON e o custo de escrever
  // na rede, e ambos entram nesta etapa.
  Serial.print("[ciclo " + String(numeroDoCiclo) + "] (2) enviando leitura");
  uint64_t t1 = agoraEmMicrosegundos();
  int leituras = 0;
  for (int i = 0; i < 10; i++) {
    String payload = "{\"device_id\":\"esp32-bancada-01\"";
    payload += ",\"leitura\":\"" + String(23.5 + (i * 0.1), 1) + "\"";
    payload += ",\"firmware\":\"" + String(VERSAO_FIRMWARE) + "\"}";

    if (payload.length() > 0 && payload.indexOf("device_id") > 0) {
      leituras++;
    }
    delay(2);
  }
  c.leituras = leituras;
  c.microsEnviar = agoraEmMicrosegundos() - t1;
  Serial.println(" " + milissegundosComUmaCasa(c.microsEnviar) + " ms (" +
                 String(leituras) + " leituras)");

  // Etapa 3: esperar a confirmacao. O servidor de exemplo responde na hora.
  Serial.print("[ciclo " + String(numeroDoCiclo) + "] (3) aguardando confirmacao");
  uint64_t t2 = agoraEmMicrosegundos();
  unsigned long esperaConfirmacao = 0;
  while (!servidorConfirmou && esperaConfirmacao < TEMPO_LIMITE_ENVIO_MS) {
    delay(10);
    esperaConfirmacao += 10;
  }
  c.microsConfirmar = agoraEmMicrosegundos() - t2;
  Serial.println(" " + milissegundosComUmaCasa(c.microsConfirmar) + " ms");

  c.microsTotal = agoraEmMicrosegundos() - inicio;
  c.heapLivreNoFim = ESP.getFreeHeap();
  return c;
}

// ------------------------------------------------------------------ setup

void imprimirCabecalho() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" limite do dispositivo — v" + String(VERSAO_FIRMWARE));
  Serial.println("========================================");
  Serial.println("etapas de um ciclo | tempo medido | tempo real");
  Serial.println();
}

void imprimirTotais() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" media de " + String(ciclosMedidos) + " ciclos");
  Serial.println("========================================");
  float mediaConectar = static_cast<float>(somaConectar) / 1000.0f;
  float mediaEnviar = static_cast<float>(somaEnviar) / 1000.0f;
  float mediaConfirmar = static_cast<float>(somaConfirmar) / 1000.0f;
  float mediaTotal = static_cast<float>(somaTotal) / 1000.0f;
  Serial.println("conectar:      " + String(mediaConectar, 1) + " ms  (" +
                 String((mediaConectar / mediaTotal) * 100.0f, 0) + "% do ciclo)");
  Serial.println("enviar:        " + String(mediaEnviar, 1) + " ms  (" +
                 String((mediaEnviar / mediaTotal) * 100.0f, 0) + "% do ciclo)");
  Serial.println("confirmar:     " + String(mediaConfirmar, 1) + " ms  (" +
                 String((mediaConfirmar / mediaTotal) * 100.0f, 0) + "% do ciclo)");
  Serial.println("total:         " + String(mediaTotal, 1) + " ms");
  Serial.println();

  // O numero que responde a pergunta da aula: quantos envios por segundo o
  // servidor aguenta deste tipo de dispositivo.
  float enviosPorMinuto = 60000.0f / mediaTotal;
  Serial.println("com um ciclo de " + String(mediaTotal, 1) + " ms, uma placa faz " +
                 String(enviosPorMinuto, 1) + " envios por minuto.");
  Serial.println("isso e por PLACA. Com 200 placas, o servidor recebe " +
                 String(enviosPorMinuto * 200.0f, 0) + " requisicoes por minuto.");
  Serial.println();

  // O limite de conexao simultanea e o outro teto do dia.
  Serial.println("o limite de conexao simultanea e o outro teto:");
  Serial.println("  WiFi de casa costuma sustentar de 8 a 20 dispositivos.");
  Serial.println("  ESP32 como servidor, em modo Access Point, sustenta menos.");
  Serial.println("  o ESP32 nao e um roteador: e um cliente com um so slot.");
  Serial.println();

  Serial.println("energia: o que muda a conta inteira");
  Serial.println("  com WiFi ligado e envio a cada 10 s: bateria dura horas.");
  Serial.println("  com deep sleep entre os envios: a mesma bateria dura semanas.");
  Serial.println("  o preco do deep sleep e que a placa perde a conexao e o servidor");
  Serial.println("  passa a ver o dispositivo como offline, mesmo ele vivo.");
  Serial.println();
  Serial.println("heap livre no final: " + String(ultimoCiclo.heapLivreNoFim) + " bytes");
}

void setup() {
  Serial.begin(115200);
  delay(200);
  imprimirCabecalho();

  if (!conectarNaRede()) {
    Serial.println("[aviso] sem rede: os numeros abaixo medem a placa sozinha.");
    Serial.println("[aviso] conecte a placa e aperte RESET para medir de verdade.");
  }

  Serial.println();
  for (int i = 1; i <= TOTAL_DE_CICLOS; i++) {
    ultimoCiclo = medirCiclo(i);
    if (ultimoCiclo.microsTotal == 0) {
      Serial.println("[ciclo " + String(i) + "] descartado por falta de rede");
      continue;
    }
    somaConectar += static_cast<unsigned long>(ultimoCiclo.microsConectar / 1000);
    somaEnviar += static_cast<unsigned long>(ultimoCiclo.microsEnviar / 1000);
    somaConfirmar += static_cast<unsigned long>(ultimoCiclo.microsConfirmar / 1000);
    somaTotal += static_cast<unsigned long>(ultimoCiclo.microsTotal / 1000);
    ciclosMedidos++;
    Serial.println("[ciclo " + String(i) + "] total " +
                   milissegundosComUmaCasa(ultimoCiclo.microsTotal) + " ms");
    Serial.println();
  }

  if (ciclosMedidos > 0) {
    imprimirTotais();
  } else {
    Serial.println("[resultado] nenhum ciclo medido: a placa precisa de rede.");
  }
}

void loop() {
  // A placa nao repete a medicao sozinha. Cada RESET roda cinco ciclos, e o
  // professor compara: mudar o intervalo muda o que o servidor vai ver.
  Serial.println();
  Serial.println("[loop] medicao concluida. Aperte RESET para medir de novo.");
  delay(10000);
  Serial.println("[loop] viva | heap: " + String(ESP.getFreeHeap()) + " bytes");
}

Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia11 aula1.

Aula 2 — Do outro lado: o servidor com muitos dispositivos

Objetivos

ulta com e sem índice e ler a diferença no número de linhas examinadas, não so no tempo.

  • Explicar por que o limite de conexão do banco e um teto, e como o excesso aparece como lentidao e não como erro.
  • Mostrar que LIMIT 20 não torna barata a consulta, e dizer o que a paginacao com LIMIT OFFSET realmente evita.
  • Agregar no banco em vez de agregar no servidor, e medir quantas linhas atravessam a rede nas duas formas.
  • Criar um cache com chave completa e validade, e produzir de propósito o defeito do cache com chave incompleta.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 computador por dupla, com o projeto do trimestre e o MySQL da aula anterior
  • Terminal com acesso ao EXPLAIN, ao EXPLAIN ANALYZE e ao log lento do MySQL
  • Caderno para a tabela de quatro colunas: consulta, tempo, linhas examinadas, linhas entregues

Conceitos

O que EXPLAIN responde, e o que EXPLAIN ANALYZE responde

Consulta lenta quase nunca significa "o banco esta lento". significa consulta que examina linha que não precisava. Os dois comandos existem para separar essas duas coisas:

EXPLAIN SELECT leitura, instante
FROM leituras
WHERE dispositivo = 'esp32-bancada-01'
ORDER BY instante DESC
LIMIT 20;

A coluna que importa e a do tipo de acesso e a das linhas examinadas. O que o professor aponta na lousa e a diferença entre ALL (varreu a tabela) e um acesso por índice.

EXPLAIN ANALYZE executa a consulta de verdade e mede o tempo real de cada etapa. Ele e mais honesto e tem um custo: mexe nos dados. A regra da aula e uma so: rode em copia, nunca em producao. Quem roda EXPLAIN ANALYZE numa tabela de três milhoes de linhas e trava a tabela.

O índice que falta: a metade da aula

O índice não muda o resultado da consulta. Muda quanto trabalho o banco faz para chegar nele. E a definição de otimizacao que não muda a resposta.

Sem índice, achar as dez leituras de um dispositivo significa percorrer a tabela até acha-las. Se o dispositivo for o ultimo, o custo e o tamanho inteiro da tabela. Com índice, o banco vai direto ao bloco, e o custo passa a ser o tamanho do bloco.

A tabela da resolucao mostra os dois números lado a lado, medidos na propria placa. O tempo medido ali e do ambiente da aula: a tabela cabe na memória e a varredura leva microssegundos. O que vale para o MySQL de verdade e o número de linhas examinadas, porque ele não muda com a maquina.

LIMIT OFFSET: o que a paginacao evita e o que ela não evita

A intuicao do aluno e sempre a mesma: "se eu peço 20, o banco lê 20". Isso e falso. A ordem de execução dentro do banco e WHERE, depois ORDER BY, depois LIMIT. Para entregar a página 1, o banco precisa chegar até a linha 21 para saber o que pular.

O que a paginacao evita e o que atravessa a rede, o que o servidor processa e o que o navegador renderiza. Isso e ganho real, e em painel com 600 leituras por dispositivo a diferença entre devolver 20 e devolver 600 não e margem: e o que derruba o processo. O professor chama essa situacao de retornar tudo, e a regra do dia e simples: não retornar tudo.

O caso pior e OFFSET alto numa tabela grande: a página 500 de uma tabela de milhoes de linhas obriga o banco a ler e descartar meio milhao. Por isso o filtro vem antes da paginacao, sempre: quanto menos linhas sobra antes do LIMIT, menor o custo do salto.

O pool de conexões: o teto que não avisa

O banco não atende todos os dispositivos ao mesmo tempo. Ele atende um número fixo de conexões por vez, e as outras esperam em fila. Esse número e o limite de conexão do banco, e ele vem de configuração, não do fabricante.

O sintoma de pool estourado não e erro. E lentidao. O painel demora, ninguem recebe 500, o código de status continua sendo 200, e o log fica vazio. Por isso o tempo de resposta precisa estar no log como campo obrigatorio, e não so o status.

A tabela da resolucao separa as duas informações justamente para mostrar isso: o 200 continua 200, e o tempo vai de poucos milissegundos para dezenas. Quem so olha o status não ve o problema chegando.

A conta da carga: varias requisicoes por segundo

O que o servidor aguenta e uma divisão, e não um número que se decora. São conexões divididas por tempo de consulta:

ConsultaTempo medioPool de 10 conexões
Índice, uma página2 ms5.000 por segundo
Sem índice, varredura40 ms250 por segundo
Historico completo de um dispositivo12 ms833 por segundo

A primeira linha e o alvo. A segunda e o que o aluno entrega sem índice. A terceira e o que o aluno entrega quando acha que "o historico cabe numa tela".

A conta fecha com a aula anterior: se cada dispositivo envia a cada 10 s, então 200 placas produzem 20 requisicoes por segundo, e 1000 placas produzem 100. A tabela diz que o alvo de 5000 por segundo sobra com folga — e a folga e o que o professor manda queimar de propósito, trocando o índice por "deixa sem índice e ve o que acontece". O número que se aprende na aula e o que acontece quando o índice some, e não o número com índice.

O segundo ponto da tabela e o mais importante: cada consulta mais lenta corta a capacidade pela metade. Não existe "o banco esta lento" que não seja uma divisão ruim. Por isso o primeiro trabalho de escala nunca e comprar maquina — e tirar da consulta o que não precisava estar nela.

E tem a rajada. A tabela acima responde a media; o que derruba servidor e o pico. Quando o roteador do laboratorio cai e volta, todas as placas reconectam no mesmo instante e mandam o envio atrasado de uma vez. Um servidor que aguenta 100 requisicoes por segundo de media pode não aguentar 800 de rajada. Medir media e medir o que quebra são duas medidas diferentes, e so uma delas aparece no grafico.

Agregar no banco: a conta que economiza memória

A media de um dispositivo e uma conta sobre dez linhas. Feita no banco, dez linhas entram e uma sai. Feita no servidor, dez linhas saem, atravessam a rede, e dez linhas viram objeto na memória do processo para o servidor somar e descartar.

Com muitos dispositivos consultando ao mesmo tempo, a diferença não e de tempo: e de memória. E memória e o recurso que faz o processo do Node ser encerrado pelo sistema, sem log e sem stack trace. E por isso que agregar no banco e a primeira coisa a fazer quando o painel demora e o processo cresce até cair.

O cache completa a lista. Guardar resultado e a unica otimizacao que não muda a resposta: o cliente recebe exatamente o mesmo dado, so que mais rapido. E por isso que ela e a ultima a tentar, e a primeira a arrumar quando o pool estoura. Chave incompleta devolve a resposta errada em vez de erro, e validade curta demais faz o cache nunca acertar. Os dois números — tamanho e tempo de vida — são o ajuste do dia.

Atividade

Montagem: nenhuma. Esta aula roda inteira no terminal e no monitor serial. A placa precisa apenas do cabo USB, porque o sketch e a representação executavel do servidor.

  1. No seu projeto, rode EXPLAIN na consulta de historico do painel e anote o tipo de acesso e o número de linhas. Depois rode EXPLAIN ANALYZE em copia do banco, e anote o tempo real.
  2. Grave o sketch da resolucao e rode no serial. Anote, para o dispositivo mais caro: quantas linhas examinadas sem índice e quantas com índice. Calcule a reducao percentual.
  3. Repita a mesma consulta do item 1 sem índice criado, em copia do banco. Quanto tempo a consulta levou? O tempo de execução e o tempo da tabela da placa, ou o tempo mudou de ordem de grandeza?
  4. Troque o LIMIT 20 por LIMIT 20000 e rode de novo em copia. Quantas linhas examinadas o banco leu? Quantas o seu processo recebeu? A diferença e o que a paginacao evita.
  5. No seu painel, troque a media calculada em JavaScript por AVG no banco. Meça o tempo antes e depois e anote quantas linhas deixou de atravessar a rede.
  6. Escreva, em três linhas: qual e a chave completa do cache do historico, e o que acontece se você guardar so o identificador do dispositivo.

Nota: 12 pontos. Critério de fim: o quadro do índice anotado com as duas colunas de linhas examinadas, e a resposta do item 6 escrita.

Resolucao

// Aula 2 do dia 11: do outro lado, o servidor com muitos dispositivos.
//
// A aula 1 mediu a placa. Esta aula muda o ponto de vista: agora o problema
// nao e "quanto tempo a placa leva", e "o que o servidor aguenta quando os
// dispositivos chegam todos juntos".
//
// A placa virou o servidor. Ela tem, dentro da propria RAM, a tabela de
// leituras que um MySQL teria: 60 dispositivos vezes 10 leituras. Cada
// consulta e executada de verdade, a tabela e percorrida de verdade, e o
// tempo e medido de verdade com esp_timer_get_time.
//
// A MEDICAO DE TEMPO aqui e do ambiente da aula: a tabela cabe na RAM, o
// varredura leva microssegundos. E exatamente por isso que o sketch mede
// duas coisas, e nao uma: o TEMPO e o numero de LINHAS EXAMINADAS. O tempo
// desta bancada e pequeno; o numero de linhas examinadas e o mesmo num
// servidor de verdade, e e dele que sai o trabalho de verdade.
//
// A licao do dia: consulta lenta quase nunca e "o banco esta lento". E
// consulta que examina linha que nao precisava, porque falta um indice, ou
// porque o pedido pediu tudo quando so precisava de uma pagina.

#include <Arduino.h>
#include <esp_timer.h>

// Versao do firmware. O professor procura este numero no serial para saber de
// qual gravacao a placa esta falando.
#define VERSAO_FIRMWARE "1.5.0"

// Identificador do servidor de exemplo. E o mesmo valor que o painel usaria
// para agrupar origem de carga.
#define ID_SERVIDOR "servidor-lab-01"

// ------------------------------------------------------- tamanho da carga
//
// Estes quatro numeros sao a carga. O professor muda TOTAL_DE_DISPOSITIVOS
// para 600 e a aula inteira muda de figura: e assim que o aluno ve que a
// consulta que levava 1 ms com 60 dispositivos leva 10 ms com 600.

const int TOTAL_DE_DISPOSITIVOS = 60;
const int LEITURAS_POR_DISPOSITIVO = 10;
const int TOTAL_DE_LINHAS = TOTAL_DE_DISPOSITIVOS * LEITURAS_POR_DISPOSITIVO;

// O pool de conexoes do banco. Este e o segundo teto do dia, e o professor
// escreve o numero na lousa: 10 conexoes nao sao 10 consultas por segundo, e
// 10 consultas AO MESMO TEMPO. A fila de espera e o que acontece com a 11a.
const int LIMITE_DE_CONEXOES_DO_BANCO = 10;

// Tamanho de uma pagina. E o que a tela mostra e o que o painel pede: 20
// linhas, nao 600.
const int TAMANHO_DA_PAGINA = 20;

// Cache do servidor. O tamanho e o TTL sao os dois numeros que decidem se o
// cache ajuda ou atrapalha: cache pequeno demais nunca acerta, cache grande
// demais devolve dado velho.
const int TAMANHO_DO_CACHE = 8;
const int TTL_DO_CACHE_SEGUNDOS = 30;

// Limite de temperatura que dispara alerta. E o mesmo numero que o painel
// mostraria em vermelho.
const float LIMITE_DE_TEMPERATURA = 30.0;

struct Resultado {
  int64_t micros;
  int linhasExaminadas;
  int linhasDevolvidas;
  bool veioDoCache;
  bool comIndice;
};

struct Agregado {
  int somaCx10;
  int minimoCx10;
  int maximoCx10;
  int contagem;
};

struct EntradaCache {
  bool valido;
  int dispositivo;
  int pagina;
  int64_t criadoEmMicros;
  Resultado resultado;
};


// ------------------------------------------------------------- tabela

// Uma linha da tabela de leituras. Tres campos porque tres e o minimo para o
// problema aparecer: quem mandou, quanto mandou e quando mandou.
struct Leitura {
  uint16_t dispositivo;
  int16_t temperaturaCx10;
  uint32_t instante;
};

Leitura tabela[TOTAL_DE_LINHAS];

// O indice que falta. Uma tabela com indice guarda, para cada dispositivo,
// a primeira linha do bloco dele. Com indice, achar as 10 leituras de um
// dispositivo custa 10 passos; sem indice, custa varrer a tabela toda ate o
// fim. E a diferenca entre um painel que abre e um painel que demora.
int primeiraLinhaDoDispositivo[TOTAL_DE_DISPOSITIVOS];
bool indiceConstruido = false;

// Gerador deterministico. Nao usamos random() porque a aula precisa dar o
// mesmo numero toda vez: se o resultado mudar a cada gravacao, o professor
// perde a comparacao entre a tabela com indice e a tabela sem.
uint32_t semente = 20260930UL;

uint32_t proximoNumero() {
  semente = semente * 1664525UL + 1013904223UL;
  return semente >> 16;
}

// Enche a tabela. As leituras de um dispositivo ficam juntas e em ordem de
// tempo, que e como um banco real entrega dado de sensor: agrupado e
// cronologico. E por isso que o indice funciona.
void construirTabela() {
  uint32_t instante = 0;
  for (int d = 0; d < TOTAL_DE_DISPOSITIVOS; d++) {
    primeiraLinhaDoDispositivo[d] = d * LEITURAS_POR_DISPOSITIVO;
    for (int i = 0; i < LEITURAS_POR_DISPOSITIVO; i++) {
      // Temperatura entre 18.0 e 34.0 graus, em decimos de grau.
      int16_t decimos = 180 + static_cast<int16_t>(proximoNumero() % 160);
      size_t posicao = static_cast<size_t>(d) * LEITURAS_POR_DISPOSITIVO + i;
      tabela[posicao].dispositivo = static_cast<uint16_t>(d);
      tabela[posicao].temperaturaCx10 = decimos;
      tabela[posicao].instante = instante;
      instante += 10;
    }
  }
  indiceConstruido = true;
}

// ------------------------------------------------------- pool de conexoes
//
// O pool e o conjunto de conexoes abertas com o banco. Cada consulta ocupa
// uma conexao pelo tempo que dura a consulta e devolve no fim. Quando todas
// estao ocupadas, a proxima consulta ESPERA. A espera e o que o professor
// mede: em uma tela de carga alta, o tempo de espera cresce antes do tempo
// de consulta crescer.

int conexoesOcupadas = 0;
int esperasNoPool = 0;
int consultaMaisLongaMicros = 0;

bool pegarConexao() {
  if (conexoesOcupadas < LIMITE_DE_CONEXOES_DO_BANCO) {
    conexoesOcupadas++;
    return true;
  }
  // Todas ocupadas: a consulta vai para a fila de espera. E o comportamento
  // correto, e nao um defeito: a fila e o que segura a carga ate um
  // estourar.
  esperasNoPool++;
  return false;
}

void devolverConexao() {
  if (conexoesOcupadas > 0) {
    conexoesOcupadas--;
  }
}

// ------------------------------------------------------------- medidor

// O resultado de uma consulta. Quatro numeros porque sao quatro perguntas
// diferentes: quanto custou, quantas linhas o banco encostou, quantas linhas
// o cliente levou, e se veio do cache.
Resultado resultadoVazio() {
  Resultado r;
  r.micros = 0;
  r.linhasExaminadas = 0;
  r.linhasDevolvidas = 0;
  r.veioDoCache = false;
  r.comIndice = false;
  return r;
}

// Sem indice: varredura completa da tabela, parando quando acha as 10
// leituras do dispositivo. Se o dispositivo estiver no fim da tabela, o custo
// e o tamanho inteiro da tabela.
Resultado varreduraCompleta(int dispositivo) {
  Resultado r = resultadoVazio();
  int achadas = 0;
  for (int i = 0; i < TOTAL_DE_LINHAS; i++) {
    r.linhasExaminadas++;
    if (tabela[i].dispositivo == dispositivo) {
      achadas++;
      if (achadas == LEITURAS_POR_DISPOSITIVO) {
        break;
      }
    }
  }
  r.linhasDevolvidas = achadas;
  return r;
}

// Com indice: vai direto ao bloco do dispositivo. O numero de linhas
// examinadas cai para o tamanho do bloco, e o tempo cai junto.
Resultado buscaComIndice(int dispositivo) {
  Resultado r = resultadoVazio();
  r.comIndice = indiceConstruido;
  if (!indiceConstruido) {
    return varreduraCompleta(dispositivo);
  }
  int inicio = primeiraLinhaDoDispositivo[dispositivo];
  for (int i = 0; i < LEITURAS_POR_DISPOSITIVO; i++) {
    r.linhasExaminadas++;
    if (tabela[inicio + i].dispositivo == dispositivo) {
      r.linhasDevolvidas++;
    }
  }
  return r;
}

// Pagina: o cliente pediu 20 de 600. O banco ainda tem que ler a tabela
// inteira para montar a pagina, porque o WHERE vem antes do LIMIT. A leitura
// de "LIMIT 20" e o que o aluno imagina; a verdade e que so o OFFSET muda o
// quanto se le.
Resultado paginaDeHistorico(int dispositivo, int pagina) {
  Resultado r = buscaComIndice(dispositivo);
  int inicio = pagina * TAMANHO_DA_PAGINA;
  if (inicio >= r.linhasDevolvidas) {
    r.linhasDevolvidas = 0;
  } else {
    r.linhasDevolvidas = r.linhasDevolvidas - inicio;
    if (r.linhasDevolvidas > TAMANHO_DA_PAGINA) {
      r.linhasDevolvidas = TAMANHO_DA_PAGINA;
    }
  }
  return r;
}

// "Retornar tudo": o cliente pediu o historico inteiro sem pedir pagina. Com
// indice, o banco acha as linhas; sem indice, ele varre a tabela toda. E o
// pedido que derruba servidor: o clienteiguais leva 10 KB, 200 clientes
// simultaneos levam 2 MB de memoria so em resposta.
Resultado historicoCompleto(int dispositivo) {
  return buscaComIndice(dispositivo);
}

// Agregar no banco: em vez de devolver 10 linhas para o cliente somar, o
// banco devolve uma linha so. As linhas lidas continuam as mesmas, e o que
// muda e o que atravessa a rede e o que o servidor processa depois.
Agregado agregarNoBanco(int dispositivo) {
  Agregado a;
  a.somaCx10 = 0;
  a.minimoCx10 = 32767;
  a.maximoCx10 = -32768;
  a.contagem = 0;
  int inicio = indiceConstruido ? primeiraLinhaDoDispositivo[dispositivo] : 0;
  int fim = indiceConstruido ? inicio + LEITURAS_POR_DISPOSITIVO : TOTAL_DE_LINHAS;
  for (int i = inicio; i < fim && i < TOTAL_DE_LINHAS; i++) {
    if (!indiceConstruido && tabela[i].dispositivo != dispositivo) {
      continue;
    }
    a.somaCx10 += tabela[i].temperaturaCx10;
    if (tabela[i].temperaturaCx10 < a.minimoCx10) {
      a.minimoCx10 = tabela[i].temperaturaCx10;
    }
    if (tabela[i].temperaturaCx10 > a.maximoCx10) {
      a.maximoCx10 = tabela[i].temperaturaCx10;
    }
    a.contagem++;
  }
  return a;
}

// ---------------------------------------------------------------- cache
//
// Cache e guardar o resultado de uma consulta que acabou de rodar, para a
// proxima pergunta igual nao precisar repetir o trabalho. Duas condicoes
// para acertar: o resultado tem que ser guardado com a CHAVE completa (nao
// so com o dispositivo, porque pagina 0 e pagina 1 sao dados diferentes) e
// tem que ter validade (TTL), porque dado velho e pior que dado nenhum.

EntradaCache cache[TAMANHO_DO_CACHE];
int proximaPosicaoDoCache = 0;
int acertosDoCache = 0;
int errosDoCache = 0;

void guardarNoCache(int dispositivo, int pagina, const Resultado& r) {
  cache[proximaPosicaoDoCache].valido = true;
  cache[proximaPosicaoDoCache].dispositivo = dispositivo;
  cache[proximaPosicaoDoCache].pagina = pagina;
  cache[proximaPosicaoDoCache].criadoEmMicros = esp_timer_get_time();
  cache[proximaPosicaoDoCache].resultado = r;
  proximaPosicaoDoCache = (proximaPosicaoDoCache + 1) % TAMANHO_DO_CACHE;
}

bool lerDoCache(int dispositivo, int pagina, int64_t agora, Resultado& saida) {
  int64_t ttlMicros = static_cast<int64_t>(TTL_DO_CACHE_SEGUNDOS) * 1000000LL;
  for (int i = 0; i < TAMANHO_DO_CACHE; i++) {
    EntradaCache& e = cache[i];
    if (!e.valido || e.dispositivo != dispositivo || e.pagina != pagina) {
      continue;
    }
    if (agora - e.criadoEmMicros > ttlMicros) {
      // Expirou. Guardar e cache nao guardar sao coisas diferentes: o
      // espaco continua ocupado, o dado que estava la nao vale mais.
      e.valido = false;
      return false;
    }
    saida = e.resultado;
    return true;
  }
  return false;
}

// ------------------------------------------------------------ executor

// A porta de entrada do servidor. E aqui que a carga bate. Tudo o que a aula
// mede passa por esta funcao, e e por isso que uma unica alteracao nela muda
// todos os numeros do quadro.
Resultado executarConsulta(int dispositivo, int pagina, bool comIndice,
                           bool usarCache) {
  int64_t inicio = esp_timer_get_time();
  Resultado r;

  if (usarCache) {
    if (lerDoCache(dispositivo, pagina, inicio, r)) {
      acertosDoCache++;
      r.veioDoCache = true;
      r.micros = esp_timer_get_time() - inicio;
      return r;
    }
    errosDoCache++;
  }

  bool entrouNaHora = pegarConexao();
  if (comIndice) {
    if (pagina < 0) {
      r = historicoCompleto(dispositivo);
    } else {
      r = paginaDeHistorico(dispositivo, pagina);
    }
  } else {
    r = varreduraCompleta(dispositivo);
  }
  if (entrouNaHora) {
    devolverConexao();
  }

  r.micros = esp_timer_get_time() - inicio;
  if (r.micros > consultaMaisLongaMicros) {
    consultaMaisLongaMicros = static_cast<int>(r.micros);
  }
  if (usarCache) {
    guardarNoCache(dispositivo, pagina, r);
  }
  return r;
}

String milissegundosComDuasCasas(int64_t micros) {
  return String(static_cast<float>(micros) / 1000.0f, 2);
}

void imprimirCabecalhoDoQuadro() {
  Serial.println();
  Serial.println("+----------------------------------+----------+------------+--------+");
  Serial.println("| consulta                          | tempo    | examinadas | entregue|");
  Serial.println("+----------------------------------+----------+------------+--------+");
}

void imprimirLinhaDoQuadro(const char* nome, const Resultado& r) {
  String nomeComEspaco(nome);
  while (nomeComEspaco.length() < 32) {
    nomeComEspaco += " ";
  }
  String tempo = milissegundosComDuasCasas(r.micros) + " ms";
  while (tempo.length() < 8) {
    tempo += " ";
  }
  String examinadas = String(r.linhasExaminadas);
  while (examinadas.length() < 10) {
    examinadas += " ";
  }
  String entregues = String(r.linhasDevolvidas);
  while (entregues.length() < 8) {
    entregues += " ";
  }
  Serial.println("| " + nomeComEspaco + " |" + tempo + " |" + examinadas + " |" +
                 entregues + "|");
  Serial.println("+----------------------------------+----------+------------+--------+");
}

// ---------------------------------------------------------------- setup

void imprimirCarga() {
  Serial.println("========================================");
  Serial.println(" carga declarada");
  Serial.println("========================================");
  Serial.println("dispositivos:            " + String(TOTAL_DE_DISPOSITIVOS));
  Serial.println("leituras por dispositivo: " + String(LEITURAS_POR_DISPOSITIVO));
  Serial.println("linhas na tabela:        " + String(TOTAL_DE_LINHAS));
  Serial.println("linhas por dispositivo:  " + String(LEITURAS_POR_DISPOSITIVO));
  Serial.println("pool do banco:           " + String(LIMITE_DE_CONEXOES_DO_BANCO) +
                 " conexoes ao mesmo tempo");
  Serial.println("tamanho de pagina:       " + String(TAMANHO_DA_PAGINA) + " linhas");
  Serial.println("cache:                   " + String(TAMANHO_DO_CACHE) + " posicoes, TTL " +
                 String(TTL_DO_CACHE_SEGUNDOS) + " s");
  Serial.println();
}

void imprimirIndice() {
  Serial.println("========================================");
  Serial.println(" o que o indice muda");
  Serial.println("========================================");
  Serial.println("o dispositivo mais caro de achar e o ultimo da tabela.");
  Serial.println("ele exige varrer a tabela inteira antes de achar as " +
                 String(LEITURAS_POR_DISPOSITIVO) + " leituras dele.");
  Serial.println();

  imprimirCabecalhoDoQuadro();
  int ultimo = TOTAL_DE_DISPOSITIVOS - 1;
  Resultado semIndice = executarConsulta(ultimo, 0, false, false);
  imprimirLinhaDoQuadro("sem indice, ultimo dispositivo", semIndice);
  Resultado comIndice = executarConsulta(ultimo, 0, true, false);
  imprimirLinhaDoQuadro("com indice, ultimo dispositivo", comIndice);
  Serial.println();

  float reducao = 100.0f;
  if (semIndice.linhasExaminadas > 0) {
    reducao = 100.0f * (1.0f - (static_cast<float>(comIndice.linhasExaminadas) /
                                static_cast<float>(semIndice.linhasExaminadas)));
  }
  Serial.println("o indice cortou " + String(reducao, 1) +
                 "% das linhas examinadas.");
  Serial.println("o tempo caiu junto, porque o trabalho que sobrou foi o mesmo.");
  Serial.println("EXPLAIN e o comando que responde isso no MySQL de verdade:");
  Serial.println("ele mostra o tipo de acesso e quantas linhas ele pegou.");
  Serial.println("EXPLAIN ANALYZE mostra o que aconteceu de verdade, e ele");
  Serial.println("EXECUTA a consulta: rode em copia, nunca em producao.");
  Serial.println();
}

void imprimirPaginacao() {
  Serial.println("========================================");
  Serial.println(" paginacao: por que LIMIT 20 nao e barato");
  Serial.println("========================================");
  Serial.println("o painel pede uma pagina por vez. O banco le ate a pagina");
  Serial.println("e devolve so as " + String(TAMANHO_DA_PAGINA) + " linhas pedidas.");
  Serial.println();
  imprimirCabecalhoDoQuadro();
  Resultado pagina0 = executarConsulta(7, 0, true, false);
  imprimirLinhaDoQuadro("pagina 0 (LIMIT 20)", pagina0);
  Resultado pagina1 = executarConsulta(7, 1, true, false);
  imprimirLinhaDoQuadro("pagina 1 (OFFSET 20)", pagina1);
  Resultado tudo = executarConsulta(7, -1, true, false);
  imprimirLinhaDoQuadro("sem pagina, historico todo", tudo);
  Serial.println();
  Serial.println("OFFSET 20 nao custou mais caro que OFFSET 0 nas linhas lidas:");
  Serial.println("o banco tem que chegar la para saber o que pular.");
  Serial.println("a pagina 6 de uma tabela de 10 linhas nao devolve nada,");
  Serial.println("e ainda assim custou o mesmo trabalho de leitura.");
  Serial.println();
  Serial.println("regra: a paginação nao e otimizacao, e o unico jeito de");
  Serial.println("o cliente pedir pouco. Retornar tudo e a forma mais rapida");
  Serial.println("de derrubar um servidor.");
  Serial.println();
}

void imprimirAgregacao() {
  Serial.println("========================================");
  Serial.println(" agregar no banco, nao no servidor");
  Serial.println("========================================");
  int alvo = 3;
  int64_t inicio = esp_timer_get_time();
  Agregado a = agregarNoBanco(alvo);
  int64_t duracao = esp_timer_get_time() - inicio;
  Serial.println("dispositivo " + String(alvo) + ":");
  Serial.println("  media:      " + String(a.somaCx10 / 10.0 / (a.contagem ? a.contagem : 1), 1) +
                 " C");
  Serial.println("  minimo:     " + String(a.minimoCx10 / 10.0, 1) + " C");
  Serial.println("  maximo:     " + String(a.maximoCx10 / 10.0, 1) + " C");
  Serial.println("  Leituras:   " + String(a.contagem));
  Serial.println("  tempo:      " + String(duracao / 1000) + " ms");
  Serial.println("  devolvido:  1 linha");
  Serial.println();
  Serial.println("o banco leu as mesmas linhas e devolveu uma so.");
  Serial.println("fazer essa conta no Node, com o dado vindo inteiro pela rede,");
  Serial.println("e a mesma conta gastando memoria, banda e tempo de CPU por");
  Serial.println("cliente. Agregar no banco e a forma de o servidor nao crescer");
  Serial.println("junto com o numero de dispositivos.");
  Serial.println();
}

void imprimirCache() {
  Serial.println("========================================");
  Serial.println(" cache: guardar resultado, com chave e com validade");
  Serial.println("========================================");
  int64_t inicio = esp_timer_get_time();
  Resultado primeiraVez = executarConsulta(12, 0, true, true);
  int64_t primeiraDuracao = esp_timer_get_time() - inicio;
  inicio = esp_timer_get_time();
  Resultado segundaVez = executarConsulta(12, 0, true, true);
  int64_t segundaDuracao = esp_timer_get_time() - inicio;

  Serial.println("mesma pergunta, duas vezes:");
  Serial.println("  1a vez: " + String(primeiraDuracao / 1000) + " ms, " +
                 String(primeiraVez.linhasExaminadas) + " linhas examinadas, veio do banco");
  Serial.println("  2a vez: " + String(segundaDuracao / 1000) + " ms, " +
                 String(segundaVez.linhasExaminadas) + " linhas examinadas, veio do cache: " +
                 String(segundaVez.veioDoCache ? "sim" : "nao"));
  Serial.println("  acertos: " + String(acertosDoCache) + " | falhas: " + String(errosDoCache));
  Serial.println();
  Serial.println("a segunda vez custou " + String(primeiraDuracao - segundaDuracao) +
                 " microssegundos a menos.");
  Serial.println("guardar resultado e a unica otimizacao que nao muda a resposta:");
  Serial.println("o cliente recebe exatamente o mesmo dado.");
  Serial.println();

  // A chave errada, mostrada de proposito: mesmo dispositivo, pagina diferente.
  Resultado outraPagina = executarConsulta(12, 1, true, true);
  Serial.println("agora a chave SEM a pagina, que e o erro classico:");
  Serial.println("  dispositivo 12, pagina 1, veio do cache: " +
                 String(outraPagina.veioDoCache ? "sim" : "nao"));
  Serial.println("  linhas examinadas: " + String(outraPagina.linhasExaminadas));
  Serial.println("  devolvidas: " + String(outraPagina.linhasDevolvidas));
  Serial.println("cache com chave incompleta devolve a resposta errada, e nao erro.");
  Serial.println("o cliente recebe a pagina 0 pedido a pagina 1. E um defeito");
  Serial.println("que nenhum log de erro mostra, porque nada deu errado: tudo");
  Serial.println("respondeu 200 e com dado.");
  Serial.println();
}

void imprimirPool() {
  Serial.println("========================================");
  Serial.println(" pool de conexoes: o segundo teto do dia");
  Serial.println("========================================");
  Serial.println("o banco nao atende " + String(TOTAL_DE_DISPOSITIVOS) +
                 " dispositivos de uma vez.");
  Serial.println("ele atende " + String(LIMITE_DE_CONEXOES_DO_BANCO) + " por vez.");
  Serial.println("as outras " + String(TOTAL_DE_DISPOSITIVOS - LIMITE_DE_CONEXOES_DO_BANCO) +
                 " esperam em fila.");
  Serial.println();
  Serial.println("conexoes simultaneas: " + String(LIMITE_DE_CONEXOES_DO_BANCO));
  Serial.println("esperas acumuladas:   " + String(esperasNoPool));
  Serial.println("consulta mais longa:  " + milissegundosComDuasCasas(consultaMaisLongaMicros) +
                 " ms");
  Serial.println();
  Serial.println("o sintoma de pool estourado nao e erro: e lentidao.");
  Serial.println("o painel demora, ninguem recebe 500, e o log fica vazio.");
  Serial.println("por isso o tempo de resposta precisa estar no log, e nao so");
  Serial.println("o codigo de status.");
  Serial.println();
}

void imprimirProjecao() {
  Serial.println("========================================");
  Serial.println(" o que o servidor aguenta");
  Serial.println("========================================");

  int64_t inicio = esp_timer_get_time();
  const int AMOSTRAS = 200;
  for (int i = 0; i < AMOSTRAS; i++) {
    executarConsulta(i % TOTAL_DE_DISPOSITIVOS, 0, true, false);
  }
  int64_t total = esp_timer_get_time() - inicio;
  float porConsulta = static_cast<float>(total) / AMOSTRAS;

  Serial.println(String(AMOSTRAS) + " consultas com indice, media de " +
                 String(porConsulta / 1000.0f, 3) + " ms cada");
  Serial.println("  tempo total: " + String(total / 1000) + " ms");
  Serial.println();
  Serial.println("com o pool em " + String(LIMITE_DE_CONEXOES_DO_BANCO) +
                 " conexoes, o teto e:");
  Serial.println("  " + String(LIMITE_DE_CONEXOES_DO_BANCO) +
                 " conexoes divididas por " + String(porConsulta / 1000.0f, 3) + " ms");
  Serial.println("  da algumas milhares de consultas por segundo na maquina de aula.");
  Serial.println("em MySQL de verdade o numero e menor, porque o trabalho");
  Serial.println("inclui disco, cache do buffer e o custo de rede do socket.");
  Serial.println();
  Serial.println("a moral nao e o numero: e que o numero e uma DIVISAO.");
  Serial.println("cada consulta mais lenta divide a capacidade pela metade.");
  Serial.println("por isso o primeiro trabalho de escala nao e comprar maquina:");
  Serial.println("e tirar o que nao precisava estar na consulta.");
  Serial.println();
}

void setup() {
  Serial.begin(115200);
  delay(200);

  Serial.println();
  Serial.println("========================================");
  Serial.println(" servidor com muitos dispositivos — v" + String(VERSAO_FIRMWARE));
  Serial.println("========================================");
  Serial.println("servidor: " + String(ID_SERVIDOR));
  Serial.println("os tempos abaixo sao medidos de verdade, nesta placa.");
  Serial.println("o que vale para o MySQL de verdade sao as LINHAS EXAMINADAS.");
  Serial.println();

  // A tabela e construida antes de qualquer consulta: sem dado nao ha o que
  // medir, e um servidor que responde vazio parece saudavel.
  construirTabela();
  Serial.println("tabela construida: " + String(TOTAL_DE_LINHAS) +
                 " linhas, indice com " + String(TOTAL_DE_DISPOSITIVOS) + " entradas");
  Serial.println();

  imprimirCarga();
  imprimirIndice();
  imprimirPaginacao();
  imprimirAgregacao();
  imprimirCache();
  imprimirPool();
  imprimirProjecao();

  Serial.println("========================================");
  Serial.println(" fim da aula 2 do dia 11");
  Serial.println("========================================");
  Serial.println("troque TOTAL_DE_DISPOSITIVOS de 60 para 600 e rode de novo:");
  Serial.println("e o que o professor quer ver: o mesmo codigo, mais carga.");
  Serial.println("heap livre: " + String(ESP.getFreeHeap()) + " bytes");
}

void loop() {
  // A aula mediu tudo no boot. O loop existe para manter a placa viva e para
  // deixar a linha de estado que o professor le no fim de cada demonstracao.
  Serial.println("[servidor] v" + String(VERSAO_FIRMWARE) + " viva | heap: " +
                 String(ESP.getFreeHeap()) + " bytes | pool: " +
                 String(conexoesOcupadas) + "/" + String(LIMITE_DE_CONEXOES_DO_BANCO) +
                 " | esperas: " + String(esperasNoPool) + " | cache " +
                 String(acertosDoCache) + " acertos / " + String(errosDoCache) + " falhas");
  delay(10000);
}

Por que assim e não de outro jeito. O sketch não simula lentidao: ele executa a varredura de verdade, percorre a tabela de verdade e conta as linhas de verdade. Poderia ter impresso um número inventado, e a aula seria mais rapida e não valeria nada.

O que separa a medida boa da medida inventada e que o sketch mede duas coisas: o tempo e o número de linhas examinadas. O tempo aqui e pequeno, porque a tabela cabe na memória de uma placa. O número de linhas e o mesmo num servidor de verdade, e e dele que sai o trabalho de verdade. Medir so o tempo, nesta bancada, ensinaria a ler um número que não se transfere.

A struct Resultado guarda cinco campos porque cinco perguntas são diferentes: quanto custou, quantas linhas o banco encostou, quantas o cliente levou, se veio do cache e se usou índice. O campo mais importante da aula e linhasExaminadas, e ele não aparece na resposta que o cliente recebe: e campo de log.

A função executarConsulta e o unico caminho para o banco, de propósito. Quando o limite de conexão e uma variável global e o pool e simulado nessa função, o professor consegue mostrar que a espera no pool acontece antes da consulta, e não dentro dela. Esse detalhe e o que separa "o banco esta lento" de "as consultas estão esperando para entrar".

O cache tem duas entradas de demonstracao propositais: a correta, que acerta, e a com chave incompleta, que devolve a página errada. A segunda e a aula. Um cache com chave incompleta não gera erro, não gera alerta e não aparece no log: ele devolve 200 com o dado de outro recorte, e o usuario ve um grafico que "funciona" e esta errado.

historicoCompleto existe para o professor mostrar o preco do que o aluno chama de simplicidade. Uma linha de código a menos no servidor, e dez vezes mais dados atravessando a rede a cada painel aberto.

Criterios de correcao

CritérioPontos
EXPLAIN e EXPLAIN ANALYZE rodados em copia, com tipo de acesso e tempo anotados3 pontos
Quadro do índice preenchido, com as duas colunas de linhas examinadas e a reducao em porcentagem3 pontos
Item 3 respondido: o tempo muda de ordem de grandeza sem índice2 pontos
Item 4 respondido: linhas examinadas contra linhas recebidas, com a diferença calculada2 pontos
AVG no banco no lugar da media em JavaScript, com o tempo medido antes e depois1 ponto
Item 6 respondido: chave completa e o defeito do cache com chave incompleta1 ponto

Erros comuns

ErroComo apareceCorrecao
Rodar EXPLAIN ANALYZE em tabela de producaoo MySQL trava e todo mundo perde a conexão"EXPLAIN ANALYZE executa a consulta. Em tabela grande, ele segura a tabela inteira. Roda em copia, sempre."
Acreditar que LIMIT 20 faz o banco ler 20 linhaso aluno escreve no relatorio que a consulta ficou barata"WHERE vem antes de LIMIT. Para entregar a página 1, o banco leu até a linha 21 para saber o que pular. A paginacao economiza o que sai da consulta, não o que entra."
OFFSET grande sem filtro antesa página 500 de tabela grande derruba o servidor"Quanto menos linha sobra antes do LIMIT, menor o salto. Filtre por dispositivo e por data antes de paginar."
Confundir limite de conexão com limite de requisicaoo aluno escreve "o banco aguenta 10 requisicoes por segundo""São dez conexões ao mesmo tempo. Quantas requisicoes por segundo isso vira depende de quanto cada consulta demora. E uma divisão, e e por isso que ma consulta mais lenta corta a capacidade pela metade."
Pool estourado tratado como erro de aplicacaoa equipe sobe o número de instancias do servidor"Mais instancia e mais pool, e a pressão no banco continua. O que resolve pool estourado e consulta mais rapida ou menos consulta, não mais servidor na frente."
Cache com chave so pelo recursoo cliente pede a página 1 e recebe a página 0"A chave e a consulta inteira: recurso, filtros, página e ordenacao. Cache com chave parcial devolve a resposta errada em silencio, que e pior que devolve erro."
Cache sem validadetodo mundo le dado velho e acha que o sensor quebrou"Cache sem TTL e uma segunda fonte da verdade. Se o dado muda, o cache precisa de um tempo para parar de mentir."
Agregar no servidor achando que e mais baratoo processo cresce e e encerrado pelo sistema, sem log"Agregar no banco e a mesma conta com menos memória e menos rede. Quando o processo morre sem log, quase sempre e memória."
Criar índice em toda colunaa insercao fica lenta e o disco cresce"Índice e troca: ganha na leitura e paga na escrita. A tabela de leituras e so de leitura; a de usuarios recebe INSERT a cada segundo."
Devolver tudo "porque o dado e pequeno"um dispositivo com 600 leituras derruba o servidor com 50 usuarios olhando"O dado e pequeno por dispositivo e grande por multiplos. O custo multiplica, e o que multiplica precisa de limite."

Desafio extra

Escreva um script que rode EXPLAIN na consulta de historico, leia o número de linhas do plano, e sobe um alerta antes de criar índice — sugerindo o índice que resolveria. Meça o tempo da consulta antes e depois de criar, e registre os dois no log com o mesmo identificador de requisicao. Depois rode o script contra uma tabela com 200 mil linhas e uma com 20 milhoes, e escreva uma frase sobre por que a mesma consulta muda de significado com o volume.

>

A resolucao, compilada

// Aula 2 do dia 11: do outro lado, o servidor com muitos dispositivos.
//
// A aula 1 mediu a placa. Esta aula muda o ponto de vista: agora o problema
// nao e "quanto tempo a placa leva", e "o que o servidor aguenta quando os
// dispositivos chegam todos juntos".
//
// A placa virou o servidor. Ela tem, dentro da propria RAM, a tabela de
// leituras que um MySQL teria: 60 dispositivos vezes 10 leituras. Cada
// consulta e executada de verdade, a tabela e percorrida de verdade, e o
// tempo e medido de verdade com esp_timer_get_time.
//
// A MEDICAO DE TEMPO aqui e do ambiente da aula: a tabela cabe na RAM, o
// varredura leva microssegundos. E exatamente por isso que o sketch mede
// duas coisas, e nao uma: o TEMPO e o numero de LINHAS EXAMINADAS. O tempo
// desta bancada e pequeno; o numero de linhas examinadas e o mesmo num
// servidor de verdade, e e dele que sai o trabalho de verdade.
//
// A licao do dia: consulta lenta quase nunca e "o banco esta lento". E
// consulta que examina linha que nao precisava, porque falta um indice, ou
// porque o pedido pediu tudo quando so precisava de uma pagina.

#include <Arduino.h>
#include <esp_timer.h>

// Versao do firmware. O professor procura este numero no serial para saber de
// qual gravacao a placa esta falando.
#define VERSAO_FIRMWARE "1.5.0"

// Identificador do servidor de exemplo. E o mesmo valor que o painel usaria
// para agrupar origem de carga.
#define ID_SERVIDOR "servidor-lab-01"

// ------------------------------------------------------- tamanho da carga
//
// Estes quatro numeros sao a carga. O professor muda TOTAL_DE_DISPOSITIVOS
// para 600 e a aula inteira muda de figura: e assim que o aluno ve que a
// consulta que levava 1 ms com 60 dispositivos leva 10 ms com 600.

const int TOTAL_DE_DISPOSITIVOS = 60;
const int LEITURAS_POR_DISPOSITIVO = 10;
const int TOTAL_DE_LINHAS = TOTAL_DE_DISPOSITIVOS * LEITURAS_POR_DISPOSITIVO;

// O pool de conexoes do banco. Este e o segundo teto do dia, e o professor
// escreve o numero na lousa: 10 conexoes nao sao 10 consultas por segundo, e
// 10 consultas AO MESMO TEMPO. A fila de espera e o que acontece com a 11a.
const int LIMITE_DE_CONEXOES_DO_BANCO = 10;

// Tamanho de uma pagina. E o que a tela mostra e o que o painel pede: 20
// linhas, nao 600.
const int TAMANHO_DA_PAGINA = 20;

// Cache do servidor. O tamanho e o TTL sao os dois numeros que decidem se o
// cache ajuda ou atrapalha: cache pequeno demais nunca acerta, cache grande
// demais devolve dado velho.
const int TAMANHO_DO_CACHE = 8;
const int TTL_DO_CACHE_SEGUNDOS = 30;

// Limite de temperatura que dispara alerta. E o mesmo numero que o painel
// mostraria em vermelho.
const float LIMITE_DE_TEMPERATURA = 30.0;

struct Resultado {
  int64_t micros;
  int linhasExaminadas;
  int linhasDevolvidas;
  bool veioDoCache;
  bool comIndice;
};

struct Agregado {
  int somaCx10;
  int minimoCx10;
  int maximoCx10;
  int contagem;
};

struct EntradaCache {
  bool valido;
  int dispositivo;
  int pagina;
  int64_t criadoEmMicros;
  Resultado resultado;
};


// ------------------------------------------------------------- tabela

// Uma linha da tabela de leituras. Tres campos porque tres e o minimo para o
// problema aparecer: quem mandou, quanto mandou e quando mandou.
struct Leitura {
  uint16_t dispositivo;
  int16_t temperaturaCx10;
  uint32_t instante;
};

Leitura tabela[TOTAL_DE_LINHAS];

// O indice que falta. Uma tabela com indice guarda, para cada dispositivo,
// a primeira linha do bloco dele. Com indice, achar as 10 leituras de um
// dispositivo custa 10 passos; sem indice, custa varrer a tabela toda ate o
// fim. E a diferenca entre um painel que abre e um painel que demora.
int primeiraLinhaDoDispositivo[TOTAL_DE_DISPOSITIVOS];
bool indiceConstruido = false;

// Gerador deterministico. Nao usamos random() porque a aula precisa dar o
// mesmo numero toda vez: se o resultado mudar a cada gravacao, o professor
// perde a comparacao entre a tabela com indice e a tabela sem.
uint32_t semente = 20260930UL;

uint32_t proximoNumero() {
  semente = semente * 1664525UL + 1013904223UL;
  return semente >> 16;
}

// Enche a tabela. As leituras de um dispositivo ficam juntas e em ordem de
// tempo, que e como um banco real entrega dado de sensor: agrupado e
// cronologico. E por isso que o indice funciona.
void construirTabela() {
  uint32_t instante = 0;
  for (int d = 0; d < TOTAL_DE_DISPOSITIVOS; d++) {
    primeiraLinhaDoDispositivo[d] = d * LEITURAS_POR_DISPOSITIVO;
    for (int i = 0; i < LEITURAS_POR_DISPOSITIVO; i++) {
      // Temperatura entre 18.0 e 34.0 graus, em decimos de grau.
      int16_t decimos = 180 + static_cast<int16_t>(proximoNumero() % 160);
      size_t posicao = static_cast<size_t>(d) * LEITURAS_POR_DISPOSITIVO + i;
      tabela[posicao].dispositivo = static_cast<uint16_t>(d);
      tabela[posicao].temperaturaCx10 = decimos;
      tabela[posicao].instante = instante;
      instante += 10;
    }
  }
  indiceConstruido = true;
}

// ------------------------------------------------------- pool de conexoes
//
// O pool e o conjunto de conexoes abertas com o banco. Cada consulta ocupa
// uma conexao pelo tempo que dura a consulta e devolve no fim. Quando todas
// estao ocupadas, a proxima consulta ESPERA. A espera e o que o professor
// mede: em uma tela de carga alta, o tempo de espera cresce antes do tempo
// de consulta crescer.

int conexoesOcupadas = 0;
int esperasNoPool = 0;
int consultaMaisLongaMicros = 0;

bool pegarConexao() {
  if (conexoesOcupadas < LIMITE_DE_CONEXOES_DO_BANCO) {
    conexoesOcupadas++;
    return true;
  }
  // Todas ocupadas: a consulta vai para a fila de espera. E o comportamento
  // correto, e nao um defeito: a fila e o que segura a carga ate um
  // estourar.
  esperasNoPool++;
  return false;
}

void devolverConexao() {
  if (conexoesOcupadas > 0) {
    conexoesOcupadas--;
  }
}

// ------------------------------------------------------------- medidor

// O resultado de uma consulta. Quatro numeros porque sao quatro perguntas
// diferentes: quanto custou, quantas linhas o banco encostou, quantas linhas
// o cliente levou, e se veio do cache.
Resultado resultadoVazio() {
  Resultado r;
  r.micros = 0;
  r.linhasExaminadas = 0;
  r.linhasDevolvidas = 0;
  r.veioDoCache = false;
  r.comIndice = false;
  return r;
}

// Sem indice: varredura completa da tabela, parando quando acha as 10
// leituras do dispositivo. Se o dispositivo estiver no fim da tabela, o custo
// e o tamanho inteiro da tabela.
Resultado varreduraCompleta(int dispositivo) {
  Resultado r = resultadoVazio();
  int achadas = 0;
  for (int i = 0; i < TOTAL_DE_LINHAS; i++) {
    r.linhasExaminadas++;
    if (tabela[i].dispositivo == dispositivo) {
      achadas++;
      if (achadas == LEITURAS_POR_DISPOSITIVO) {
        break;
      }
    }
  }
  r.linhasDevolvidas = achadas;
  return r;
}

// Com indice: vai direto ao bloco do dispositivo. O numero de linhas
// examinadas cai para o tamanho do bloco, e o tempo cai junto.
Resultado buscaComIndice(int dispositivo) {
  Resultado r = resultadoVazio();
  r.comIndice = indiceConstruido;
  if (!indiceConstruido) {
    return varreduraCompleta(dispositivo);
  }
  int inicio = primeiraLinhaDoDispositivo[dispositivo];
  for (int i = 0; i < LEITURAS_POR_DISPOSITIVO; i++) {
    r.linhasExaminadas++;
    if (tabela[inicio + i].dispositivo == dispositivo) {
      r.linhasDevolvidas++;
    }
  }
  return r;
}

// Pagina: o cliente pediu 20 de 600. O banco ainda tem que ler a tabela
// inteira para montar a pagina, porque o WHERE vem antes do LIMIT. A leitura
// de "LIMIT 20" e o que o aluno imagina; a verdade e que so o OFFSET muda o
// quanto se le.
Resultado paginaDeHistorico(int dispositivo, int pagina) {
  Resultado r = buscaComIndice(dispositivo);
  int inicio = pagina * TAMANHO_DA_PAGINA;
  if (inicio >= r.linhasDevolvidas) {
    r.linhasDevolvidas = 0;
  } else {
    r.linhasDevolvidas = r.linhasDevolvidas - inicio;
    if (r.linhasDevolvidas > TAMANHO_DA_PAGINA) {
      r.linhasDevolvidas = TAMANHO_DA_PAGINA;
    }
  }
  return r;
}

// "Retornar tudo": o cliente pediu o historico inteiro sem pedir pagina. Com
// indice, o banco acha as linhas; sem indice, ele varre a tabela toda. E o
// pedido que derruba servidor: o clienteiguais leva 10 KB, 200 clientes
// simultaneos levam 2 MB de memoria so em resposta.
Resultado historicoCompleto(int dispositivo) {
  return buscaComIndice(dispositivo);
}

// Agregar no banco: em vez de devolver 10 linhas para o cliente somar, o
// banco devolve uma linha so. As linhas lidas continuam as mesmas, e o que
// muda e o que atravessa a rede e o que o servidor processa depois.
Agregado agregarNoBanco(int dispositivo) {
  Agregado a;
  a.somaCx10 = 0;
  a.minimoCx10 = 32767;
  a.maximoCx10 = -32768;
  a.contagem = 0;
  int inicio = indiceConstruido ? primeiraLinhaDoDispositivo[dispositivo] : 0;
  int fim = indiceConstruido ? inicio + LEITURAS_POR_DISPOSITIVO : TOTAL_DE_LINHAS;
  for (int i = inicio; i < fim && i < TOTAL_DE_LINHAS; i++) {
    if (!indiceConstruido && tabela[i].dispositivo != dispositivo) {
      continue;
    }
    a.somaCx10 += tabela[i].temperaturaCx10;
    if (tabela[i].temperaturaCx10 < a.minimoCx10) {
      a.minimoCx10 = tabela[i].temperaturaCx10;
    }
    if (tabela[i].temperaturaCx10 > a.maximoCx10) {
      a.maximoCx10 = tabela[i].temperaturaCx10;
    }
    a.contagem++;
  }
  return a;
}

// ---------------------------------------------------------------- cache
//
// Cache e guardar o resultado de uma consulta que acabou de rodar, para a
// proxima pergunta igual nao precisar repetir o trabalho. Duas condicoes
// para acertar: o resultado tem que ser guardado com a CHAVE completa (nao
// so com o dispositivo, porque pagina 0 e pagina 1 sao dados diferentes) e
// tem que ter validade (TTL), porque dado velho e pior que dado nenhum.

EntradaCache cache[TAMANHO_DO_CACHE];
int proximaPosicaoDoCache = 0;
int acertosDoCache = 0;
int errosDoCache = 0;

void guardarNoCache(int dispositivo, int pagina, const Resultado& r) {
  cache[proximaPosicaoDoCache].valido = true;
  cache[proximaPosicaoDoCache].dispositivo = dispositivo;
  cache[proximaPosicaoDoCache].pagina = pagina;
  cache[proximaPosicaoDoCache].criadoEmMicros = esp_timer_get_time();
  cache[proximaPosicaoDoCache].resultado = r;
  proximaPosicaoDoCache = (proximaPosicaoDoCache + 1) % TAMANHO_DO_CACHE;
}

bool lerDoCache(int dispositivo, int pagina, int64_t agora, Resultado& saida) {
  int64_t ttlMicros = static_cast<int64_t>(TTL_DO_CACHE_SEGUNDOS) * 1000000LL;
  for (int i = 0; i < TAMANHO_DO_CACHE; i++) {
    EntradaCache& e = cache[i];
    if (!e.valido || e.dispositivo != dispositivo || e.pagina != pagina) {
      continue;
    }
    if (agora - e.criadoEmMicros > ttlMicros) {
      // Expirou. Guardar e cache nao guardar sao coisas diferentes: o
      // espaco continua ocupado, o dado que estava la nao vale mais.
      e.valido = false;
      return false;
    }
    saida = e.resultado;
    return true;
  }
  return false;
}

// ------------------------------------------------------------ executor

// A porta de entrada do servidor. E aqui que a carga bate. Tudo o que a aula
// mede passa por esta funcao, e e por isso que uma unica alteracao nela muda
// todos os numeros do quadro.
Resultado executarConsulta(int dispositivo, int pagina, bool comIndice,
                           bool usarCache) {
  int64_t inicio = esp_timer_get_time();
  Resultado r;

  if (usarCache) {
    if (lerDoCache(dispositivo, pagina, inicio, r)) {
      acertosDoCache++;
      r.veioDoCache = true;
      r.micros = esp_timer_get_time() - inicio;
      return r;
    }
    errosDoCache++;
  }

  bool entrouNaHora = pegarConexao();
  if (comIndice) {
    if (pagina < 0) {
      r = historicoCompleto(dispositivo);
    } else {
      r = paginaDeHistorico(dispositivo, pagina);
    }
  } else {
    r = varreduraCompleta(dispositivo);
  }
  if (entrouNaHora) {
    devolverConexao();
  }

  r.micros = esp_timer_get_time() - inicio;
  if (r.micros > consultaMaisLongaMicros) {
    consultaMaisLongaMicros = static_cast<int>(r.micros);
  }
  if (usarCache) {
    guardarNoCache(dispositivo, pagina, r);
  }
  return r;
}

String milissegundosComDuasCasas(int64_t micros) {
  return String(static_cast<float>(micros) / 1000.0f, 2);
}

void imprimirCabecalhoDoQuadro() {
  Serial.println();
  Serial.println("+----------------------------------+----------+------------+--------+");
  Serial.println("| consulta                          | tempo    | examinadas | entregue|");
  Serial.println("+----------------------------------+----------+------------+--------+");
}

void imprimirLinhaDoQuadro(const char* nome, const Resultado& r) {
  String nomeComEspaco(nome);
  while (nomeComEspaco.length() < 32) {
    nomeComEspaco += " ";
  }
  String tempo = milissegundosComDuasCasas(r.micros) + " ms";
  while (tempo.length() < 8) {
    tempo += " ";
  }
  String examinadas = String(r.linhasExaminadas);
  while (examinadas.length() < 10) {
    examinadas += " ";
  }
  String entregues = String(r.linhasDevolvidas);
  while (entregues.length() < 8) {
    entregues += " ";
  }
  Serial.println("| " + nomeComEspaco + " |" + tempo + " |" + examinadas + " |" +
                 entregues + "|");
  Serial.println("+----------------------------------+----------+------------+--------+");
}

// ---------------------------------------------------------------- setup

void imprimirCarga() {
  Serial.println("========================================");
  Serial.println(" carga declarada");
  Serial.println("========================================");
  Serial.println("dispositivos:            " + String(TOTAL_DE_DISPOSITIVOS));
  Serial.println("leituras por dispositivo: " + String(LEITURAS_POR_DISPOSITIVO));
  Serial.println("linhas na tabela:        " + String(TOTAL_DE_LINHAS));
  Serial.println("linhas por dispositivo:  " + String(LEITURAS_POR_DISPOSITIVO));
  Serial.println("pool do banco:           " + String(LIMITE_DE_CONEXOES_DO_BANCO) +
                 " conexoes ao mesmo tempo");
  Serial.println("tamanho de pagina:       " + String(TAMANHO_DA_PAGINA) + " linhas");
  Serial.println("cache:                   " + String(TAMANHO_DO_CACHE) + " posicoes, TTL " +
                 String(TTL_DO_CACHE_SEGUNDOS) + " s");
  Serial.println();
}

void imprimirIndice() {
  Serial.println("========================================");
  Serial.println(" o que o indice muda");
  Serial.println("========================================");
  Serial.println("o dispositivo mais caro de achar e o ultimo da tabela.");
  Serial.println("ele exige varrer a tabela inteira antes de achar as " +
                 String(LEITURAS_POR_DISPOSITIVO) + " leituras dele.");
  Serial.println();

  imprimirCabecalhoDoQuadro();
  int ultimo = TOTAL_DE_DISPOSITIVOS - 1;
  Resultado semIndice = executarConsulta(ultimo, 0, false, false);
  imprimirLinhaDoQuadro("sem indice, ultimo dispositivo", semIndice);
  Resultado comIndice = executarConsulta(ultimo, 0, true, false);
  imprimirLinhaDoQuadro("com indice, ultimo dispositivo", comIndice);
  Serial.println();

  float reducao = 100.0f;
  if (semIndice.linhasExaminadas > 0) {
    reducao = 100.0f * (1.0f - (static_cast<float>(comIndice.linhasExaminadas) /
                                static_cast<float>(semIndice.linhasExaminadas)));
  }
  Serial.println("o indice cortou " + String(reducao, 1) +
                 "% das linhas examinadas.");
  Serial.println("o tempo caiu junto, porque o trabalho que sobrou foi o mesmo.");
  Serial.println("EXPLAIN e o comando que responde isso no MySQL de verdade:");
  Serial.println("ele mostra o tipo de acesso e quantas linhas ele pegou.");
  Serial.println("EXPLAIN ANALYZE mostra o que aconteceu de verdade, e ele");
  Serial.println("EXECUTA a consulta: rode em copia, nunca em producao.");
  Serial.println();
}

void imprimirPaginacao() {
  Serial.println("========================================");
  Serial.println(" paginacao: por que LIMIT 20 nao e barato");
  Serial.println("========================================");
  Serial.println("o painel pede uma pagina por vez. O banco le ate a pagina");
  Serial.println("e devolve so as " + String(TAMANHO_DA_PAGINA) + " linhas pedidas.");
  Serial.println();
  imprimirCabecalhoDoQuadro();
  Resultado pagina0 = executarConsulta(7, 0, true, false);
  imprimirLinhaDoQuadro("pagina 0 (LIMIT 20)", pagina0);
  Resultado pagina1 = executarConsulta(7, 1, true, false);
  imprimirLinhaDoQuadro("pagina 1 (OFFSET 20)", pagina1);
  Resultado tudo = executarConsulta(7, -1, true, false);
  imprimirLinhaDoQuadro("sem pagina, historico todo", tudo);
  Serial.println();
  Serial.println("OFFSET 20 nao custou mais caro que OFFSET 0 nas linhas lidas:");
  Serial.println("o banco tem que chegar la para saber o que pular.");
  Serial.println("a pagina 6 de uma tabela de 10 linhas nao devolve nada,");
  Serial.println("e ainda assim custou o mesmo trabalho de leitura.");
  Serial.println();
  Serial.println("regra: a paginação nao e otimizacao, e o unico jeito de");
  Serial.println("o cliente pedir pouco. Retornar tudo e a forma mais rapida");
  Serial.println("de derrubar um servidor.");
  Serial.println();
}

void imprimirAgregacao() {
  Serial.println("========================================");
  Serial.println(" agregar no banco, nao no servidor");
  Serial.println("========================================");
  int alvo = 3;
  int64_t inicio = esp_timer_get_time();
  Agregado a = agregarNoBanco(alvo);
  int64_t duracao = esp_timer_get_time() - inicio;
  Serial.println("dispositivo " + String(alvo) + ":");
  Serial.println("  media:      " + String(a.somaCx10 / 10.0 / (a.contagem ? a.contagem : 1), 1) +
                 " C");
  Serial.println("  minimo:     " + String(a.minimoCx10 / 10.0, 1) + " C");
  Serial.println("  maximo:     " + String(a.maximoCx10 / 10.0, 1) + " C");
  Serial.println("  Leituras:   " + String(a.contagem));
  Serial.println("  tempo:      " + String(duracao / 1000) + " ms");
  Serial.println("  devolvido:  1 linha");
  Serial.println();
  Serial.println("o banco leu as mesmas linhas e devolveu uma so.");
  Serial.println("fazer essa conta no Node, com o dado vindo inteiro pela rede,");
  Serial.println("e a mesma conta gastando memoria, banda e tempo de CPU por");
  Serial.println("cliente. Agregar no banco e a forma de o servidor nao crescer");
  Serial.println("junto com o numero de dispositivos.");
  Serial.println();
}

void imprimirCache() {
  Serial.println("========================================");
  Serial.println(" cache: guardar resultado, com chave e com validade");
  Serial.println("========================================");
  int64_t inicio = esp_timer_get_time();
  Resultado primeiraVez = executarConsulta(12, 0, true, true);
  int64_t primeiraDuracao = esp_timer_get_time() - inicio;
  inicio = esp_timer_get_time();
  Resultado segundaVez = executarConsulta(12, 0, true, true);
  int64_t segundaDuracao = esp_timer_get_time() - inicio;

  Serial.println("mesma pergunta, duas vezes:");
  Serial.println("  1a vez: " + String(primeiraDuracao / 1000) + " ms, " +
                 String(primeiraVez.linhasExaminadas) + " linhas examinadas, veio do banco");
  Serial.println("  2a vez: " + String(segundaDuracao / 1000) + " ms, " +
                 String(segundaVez.linhasExaminadas) + " linhas examinadas, veio do cache: " +
                 String(segundaVez.veioDoCache ? "sim" : "nao"));
  Serial.println("  acertos: " + String(acertosDoCache) + " | falhas: " + String(errosDoCache));
  Serial.println();
  Serial.println("a segunda vez custou " + String(primeiraDuracao - segundaDuracao) +
                 " microssegundos a menos.");
  Serial.println("guardar resultado e a unica otimizacao que nao muda a resposta:");
  Serial.println("o cliente recebe exatamente o mesmo dado.");
  Serial.println();

  // A chave errada, mostrada de proposito: mesmo dispositivo, pagina diferente.
  Resultado outraPagina = executarConsulta(12, 1, true, true);
  Serial.println("agora a chave SEM a pagina, que e o erro classico:");
  Serial.println("  dispositivo 12, pagina 1, veio do cache: " +
                 String(outraPagina.veioDoCache ? "sim" : "nao"));
  Serial.println("  linhas examinadas: " + String(outraPagina.linhasExaminadas));
  Serial.println("  devolvidas: " + String(outraPagina.linhasDevolvidas));
  Serial.println("cache com chave incompleta devolve a resposta errada, e nao erro.");
  Serial.println("o cliente recebe a pagina 0 pedido a pagina 1. E um defeito");
  Serial.println("que nenhum log de erro mostra, porque nada deu errado: tudo");
  Serial.println("respondeu 200 e com dado.");
  Serial.println();
}

void imprimirPool() {
  Serial.println("========================================");
  Serial.println(" pool de conexoes: o segundo teto do dia");
  Serial.println("========================================");
  Serial.println("o banco nao atende " + String(TOTAL_DE_DISPOSITIVOS) +
                 " dispositivos de uma vez.");
  Serial.println("ele atende " + String(LIMITE_DE_CONEXOES_DO_BANCO) + " por vez.");
  Serial.println("as outras " + String(TOTAL_DE_DISPOSITIVOS - LIMITE_DE_CONEXOES_DO_BANCO) +
                 " esperam em fila.");
  Serial.println();
  Serial.println("conexoes simultaneas: " + String(LIMITE_DE_CONEXOES_DO_BANCO));
  Serial.println("esperas acumuladas:   " + String(esperasNoPool));
  Serial.println("consulta mais longa:  " + milissegundosComDuasCasas(consultaMaisLongaMicros) +
                 " ms");
  Serial.println();
  Serial.println("o sintoma de pool estourado nao e erro: e lentidao.");
  Serial.println("o painel demora, ninguem recebe 500, e o log fica vazio.");
  Serial.println("por isso o tempo de resposta precisa estar no log, e nao so");
  Serial.println("o codigo de status.");
  Serial.println();
}

void imprimirProjecao() {
  Serial.println("========================================");
  Serial.println(" o que o servidor aguenta");
  Serial.println("========================================");

  int64_t inicio = esp_timer_get_time();
  const int AMOSTRAS = 200;
  for (int i = 0; i < AMOSTRAS; i++) {
    executarConsulta(i % TOTAL_DE_DISPOSITIVOS, 0, true, false);
  }
  int64_t total = esp_timer_get_time() - inicio;
  float porConsulta = static_cast<float>(total) / AMOSTRAS;

  Serial.println(String(AMOSTRAS) + " consultas com indice, media de " +
                 String(porConsulta / 1000.0f, 3) + " ms cada");
  Serial.println("  tempo total: " + String(total / 1000) + " ms");
  Serial.println();
  Serial.println("com o pool em " + String(LIMITE_DE_CONEXOES_DO_BANCO) +
                 " conexoes, o teto e:");
  Serial.println("  " + String(LIMITE_DE_CONEXOES_DO_BANCO) +
                 " conexoes divididas por " + String(porConsulta / 1000.0f, 3) + " ms");
  Serial.println("  da algumas milhares de consultas por segundo na maquina de aula.");
  Serial.println("em MySQL de verdade o numero e menor, porque o trabalho");
  Serial.println("inclui disco, cache do buffer e o custo de rede do socket.");
  Serial.println();
  Serial.println("a moral nao e o numero: e que o numero e uma DIVISAO.");
  Serial.println("cada consulta mais lenta divide a capacidade pela metade.");
  Serial.println("por isso o primeiro trabalho de escala nao e comprar maquina:");
  Serial.println("e tirar o que nao precisava estar na consulta.");
  Serial.println();
}

void setup() {
  Serial.begin(115200);
  delay(200);

  Serial.println();
  Serial.println("========================================");
  Serial.println(" servidor com muitos dispositivos — v" + String(VERSAO_FIRMWARE));
  Serial.println("========================================");
  Serial.println("servidor: " + String(ID_SERVIDOR));
  Serial.println("os tempos abaixo sao medidos de verdade, nesta placa.");
  Serial.println("o que vale para o MySQL de verdade sao as LINHAS EXAMINADAS.");
  Serial.println();

  // A tabela e construida antes de qualquer consulta: sem dado nao ha o que
  // medir, e um servidor que responde vazio parece saudavel.
  construirTabela();
  Serial.println("tabela construida: " + String(TOTAL_DE_LINHAS) +
                 " linhas, indice com " + String(TOTAL_DE_DISPOSITIVOS) + " entradas");
  Serial.println();

  imprimirCarga();
  imprimirIndice();
  imprimirPaginacao();
  imprimirAgregacao();
  imprimirCache();
  imprimirPool();
  imprimirProjecao();

  Serial.println("========================================");
  Serial.println(" fim da aula 2 do dia 11");
  Serial.println("========================================");
  Serial.println("troque TOTAL_DE_DISPOSITIVOS de 60 para 600 e rode de novo:");
  Serial.println("e o que o professor quer ver: o mesmo codigo, mais carga.");
  Serial.println("heap livre: " + String(ESP.getFreeHeap()) + " bytes");
}

void loop() {
  // A aula mediu tudo no boot. O loop existe para manter a placa viva e para
  // deixar a linha de estado que o professor le no fim de cada demonstracao.
  Serial.println("[servidor] v" + String(VERSAO_FIRMWARE) + " viva | heap: " +
                 String(ESP.getFreeHeap()) + " bytes | pool: " +
                 String(conexoesOcupadas) + "/" + String(LIMITE_DE_CONEXOES_DO_BANCO) +
                 " | esperas: " + String(esperasNoPool) + " | cache " +
                 String(acertosDoCache) + " acertos / " + String(errosDoCache) + " falhas");
  delay(10000);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia11 aula2.