Implementando malloc() e free() — dividindo blocos grandes

No artigo anterior da série, vimos como a ordem em que escolhemos que blocos de memória reutilizar pode levar a maior ou menor consumo de memória, e mudamos nossas funções para evitar esse desperdício. Mas precisamos resolver outro problema, até mais grave: às vezes, um bloco de memória muito grande pode ocupar o espaço que vários blocos menores poderiam usar. Considere o caso abaixo, em que alocamos um grande pedaço de memória, desalocamos e depois alocamos dois blocos bem menores:

void *ptr1 = abmalloc(128);
void *ptr2 = abmalloc(8);
abfree(ptr1);
void *ptr3 = abmalloc(8);
void *ptr4 = abmalloc(8);Code language: JavaScript (javascript)

Aqui, temos um bloco de memória de 128 bytes livre, e, ao alocarmos um bloco de apenas 8 bytes, todos os 128 bytes ficam indisponíveis. Quando alocamos outro bloco de 8 bytes, o heap precisa crescer novamente. Isso não é um uso eficiente de memória.

Existem pelo menos duas soluções populares para esse caso. Uma, mais eficiente, é usar bins: listas que agrupam blocos por tamanho. Essa é uma abordagem mais sofisticada, porém mais complexa. Outra opção, mais simples, é encontrar um bloco grande e dividi-lo em blocos menores. Vamos seguir por essa abordagem.

Mas lembre-se: mais simples não significa exatamente simples 😉

Refatoração Inicial

Antes de começarmos, faremos uma pequena refatoração. Atualmente, a função header_new() faz duas coisas: aloca mais memória para um novo bloco e já inicializa seu cabeçalho, definindo os metadados e os ponteiros para o bloco anterior. A parte de inicializar o cabeçalho pode ser útil, então vamos extraí-la. Criamos duas novas funções para melhorar a legibilidade:

  • A função header_plug(), que “conecta” o bloco inicializado ao anterior e ao próximo.
  • A função header_init(), que define os valores iniciais dos metadados do bloco (tamanho e disponibilidade).

Aqui está como elas ficam:

void header_init(Header *header, size_t size, bool available) {
    header->size = size;
    header->available = available;
}

void header_plug(Header *header, Header *previous, Header *next) {
    header->previous = previous;
    if (previous != NULL) {
        previous->next = header;
    }
    header->next = next;
    if (next != NULL) {
        next->previous = header;
    }
}Code language: PHP (php)

Agora, basta alterar header_new() para usar essas novas funções:

Header *header_new(Header *previous, size_t size, bool available) {
    Header *header = sbrk(sizeof(Header) + size);
    header_init(header, size, available);
    header_plug(header, previous, NULL);
    return header;
}Code language: PHP (php)

(Além disso, podemos remover a linha last->previous->next = last; da função abmalloc(), já que header_plug() agora cuida disso.)

Dividindo Blocos

Com essas ferramentas em mãos, vamos criar a função header_split(). Dado um cabeçalho e um tamanho mínimo requerido, esta função divide o bloco de memória em dois, se o bloco original for grande o suficiente para conter

  • o tamanho requerido,
  • um novo cabeçalho do novo bloco e
  • um pouco de memória extra.

Primeiro, verificamos se o bloco é grande o bastante:

Header *header_split(Header *header, size_t size) {
    size_t original_size = header->size;
    if (original_size >= size + sizeof(Header)) {Code language: PHP (php)

Se essa condição for satisfeita, dividimos o bloco. Primeiro, reduzimos o tamanho do bloco atual, subtraindo o tamanho de um cabeçalho e o espaço solicitado por abmalloc:

    header->size = original_size - size - sizeof(Header);Code language: PHP (php)

Isso deixa um espaço de memória após o bloco atual, que usaremos para criar o novo bloco. Calculamos o ponteiro para esse novo bloco:

    Header *new_header = header + sizeof(Header) + header->size;Code language: PHP (php)

Agora que temos o ponteiro para o novo bloco, inicializamos o cabeçalho com header_init():

    header_init(new_header, size, true);Code language: JavaScript (javascript)

E conectamos o novo bloco ao anterior e ao próximo usando header_plug():

    header_plug(new_header, header, header->next);Code language: PHP (php)

Se o bloco original for o último, o novo bloco agora será o último, então atualizamos o ponteiro last:

    if (header == last) {
        last = new_header;
    }

Por fim, retornamos o novo bloco:

    return new_header;Code language: JavaScript (javascript)

Se o bloco original não for grande o suficiente, simplesmente retornamos o bloco original:

  } else {
    return header;
  }
}Code language: JavaScript (javascript)

Atualizando abmalloc()

Agora, basta voltar à função abmalloc() e, no lugar onde encontramos um bloco utilizável, invocamos header_split() para tentar dividi-lo:

if (header->available && (header->size >= size)) {
    header = header_split(header, size);
    header->available = false;
    return header + 1;
}Code language: PHP (php)

Se o bloco puder ser dividido, o novo bloco será retornado. Caso contrário, o bloco original será mantido e retornado, como antes.

Nota sobre a Divisão de Blocos

Observe que criamos o novo bloco ao final do bloco original. Poderíamos criá-lo no início, mas criar o novo bloco utilizado ao final, o novo bloco livre fica mais próximo dos blocos mais antigos. Com isso, ele será encontrado primeiro na próxima vez que abmalloc() for invocado.

Dividir blocos de memória grandes é um avanço, mas há um problema contrário: blocos de memória pequenos podem causar fragmentação, fazendo com que requisições maiores façam o heap crescer. Veremos como resolver isso no próximo post.

Implementando malloc() e free() — memória antiga tem preferência

No post anterior desta série sobre a implementação das funções malloc() e free(), mostramos como é possível reutilizar blocos de memória e reduzir o heap ao liberar os blocos mais novos. No entanto, a função atual reutiliza o bloco de memória livre mais recente, levando a um consumo de memória maior. Vamos analisar como isso ocorre.

Redução do heap com reaproveitamento de blocos recentes

Considere o cenário a seguir. Primeiro, alocamos quatro blocos de memória:

void *ptr1 = abmalloc(8);
void *ptr2 = abmalloc(8);
void *ptr3 = abmalloc(8);
void *ptr4 = abmalloc(8);

A estrutura em memória pode ser visualizada assim:

Lista ligada com quatro nós. Os nós mais novos apontam para os mais antigos. Todos os nós estão em uso, por isso todos estão com o rótulo Ocupado.

Agora, liberamos o primeiro e o terceiro bloco…

abfree(ptr1);
abfree(ptr3);

…resultando na seguinte estrutura:

Lista ligada com quatro nós. Os nós mais novos apontam para os mais antigos. O segundo e quarto nós da lista (ou seja, o segundo mais novo e o mais antigo de todos) foram liberados com free(), então estão rotulados com a palavra Livre.

Em seguida, alocamos outro bloco do mesmo tamanho:

void *ptr5 = abmalloc(8); 

Como função abmalloc() começa a busca pelo bloco livre mais recente, ela reutiliza o bloco que está no topo. Se agora liberarmos o último bloco:

Lista ligada com quatro nós. Os nós mais novos apontam para os mais antigos. Agora apenas o último nó está liberado e rotulado com a palavra Livre

Se, agora, liberamos o último bloco…

abfree(ptr4);

…poderemos reduzir o tamanho do heap em apenas um bloco de 8 bytes, já que o penúltimo bloco não está mais livre:

Lista ligada com três nós. Os nós mais novos apontam para os mais antigos. O último nó está liberado e rotulado com a palavra Livre

Reaproveitamento de blocos antigos

Agora, imagine o mesmo cenário, mas com uma modificação: nossa função busca blocos livres começando pelo mais antigo.

A estrutura inicial será a mesma…

Lista ligada com quatro nós. Os nós mais antigos apontam para os mais novos. Todos os nós estão em uso e rotulados com a palavra Ocupado.

…e novamente liberamos o primeiro e o terceiro bloco de memória:

Lista ligada com quatro nós. Os nós mais antigos apontam para os mais novos. O primeiro e terceiro nós (isto é, o mais antigo e o terceiro mais antigo/segundo mais novo) foram desalocados e estão rotulados com a palavra Livre

Dessa vez, o primeiro bloco será reutilizado:

Lista ligada com quatro nós. Os nós mais antigos apontam para os mais novos. O terceiro nós (isto é, o mais antigo e o terceiro mais antigo/segundo mais novo) foi desalocado. Por isso, está rotulado com a palavra Livre

Agora, ao liberarmos o último bloco, teremos dois blocos livres no topo, permitindo reduzir o heap em dois blocos de 8 bytes:

Lista ligada com dois nós.  Todos estão ocupados porque os nós livres estavam no topo do heap e, portanto, foram destruídos.

Esse exemplo ilustra como, ao dar preferência a blocos mais novos, acabamos acumulando blocos livres antigos, desperdiçando memória e levando a um crescimento desnecessário do heap. A solução é modificar a estratégia de busca, priorizando a reutilização de blocos mais antigos.

Implementando preferência por blocos antigos

Para resolver esse problema, vamos implementar uma nova abordagem no código. Primeiramente, vamos adicionar ao cabeçalho dos blocos um ponteiro para o próximo bloco, permitindo percorrermos a memória de maneira eficiente. Também adicionaremos um ponteiro para o primeiro bloco, o que facilitará a busca:

typedef struct Header {
struct Header *previous, *next;
size_t size;
bool available;
} Header;

Header *first = NULL;
Header *last = NULL;

Agora, faremos uma pequena refatoração. Como criamos blocos de memória com headers em duas situações diferentes, extrairemos essa lógica para uma função auxiliar que aloca e inicializa o cabeçalho (inclusive setando o campo next com NULL):

Header *header_new(Header *previous, size_t size, bool available) {
Header *header = sbrk(sizeof(Header) + size);
header->previous = previous;
header->next = NULL;
header->size = size;
header->available = false;
return header;
}

Com essa nova função, podemos simplificar a lógica dentro de abmalloc():

void *abmalloc(size_t size) {
if (size == 0) {
return NULL;
}
Header *header = last;
while (header != NULL) {
if (header->available && (header->size >= size)) {
header->available = false;
return header + 1;
}
header = header->previous;
}
last = header_new(last, size, false);
return last + 1;
}

Agora temos acesso ao primeiro e último blocos e, dado um bloco, podemos descobrir o anterior e o posterior. Sabemos, também, que quando o ponteiro para o primeiro bloco for nulo, nenhum bloco terá sido alocado ainda. Então, neste caso, alocaremos o bloco imediatamente, e inicializaremos tanto first quanto last:

void *abmalloc(size_t size) {
if (size == 0) {
return NULL;
}
if (first == NULL) {
first = last = header_new(NULL, size, false);
return first + 1;
}

Se first não é mais NULL, já existem blocos alocados, então começaremos a busca por um bloco reutilizável. Continuaremos utilizando a variável header como iterador, mas, em vez de começar pelo bloco mais recente, a pesquisa será iniciada a partir do mais antigo:

  Header *header = first;

A cada iteração, avançaremos para o próximo bloco na sequência, em vez de retroceder para o bloco anterior:

  while (header != NULL) {
if (header->available && (header->size >= size)) {
header->available = false;
return header + 1;
}
header = header->next;
}

A lógica permanece a mesma: se encontramos um bloco disponível e de tamanho suficiente, ele é retornado. Caso contrário, se nenhum bloco reutilizável for encontrado após percorrermos a lista, um novo bloco será alocado:

  last = header_new(last, size, false);

Agora, precisamos ajustar o bloco que era o último (e que, após a alocação, passa a ser o penúltimo). Ele apontava para NULL, mas agora deve apontar para o novo bloco. Para isso, setamos o campo field do block anterior com o novo bloco:

  last->previous->next = last;
return last + 1;
}

Ajustes na Função abfree()

A função abfree() mantém basicamente a mesma estrutura, mas agora devemos tratar alguns casos de borda. Quando liberamos blocos no topo do heap, um novo bloco passa a ser o último, conforme já fazemos neste trecho:

    last = header->previous;
brk(header)

Aqui, o ponteiro header referencia o último bloco não nulo e disponível na pilha. Temos dois cenários possíveis:

  1. no primeiro, o bloco atual possui um bloco anterior, que se tornará o novo último bloco. Nesse caso, devemos ajustar o ponteiro next desse bloco para NULL.
  2. No segundo cenário, o bloco atual não possui um bloco anterior (ou seja, ele é o primeiro e mais antigo bloco). Quando ele é liberado, a pilha fica vazia. Nesse caso, em vez de tentar atualizar um campo de um bloco inexistente, basta definirmos first como NULL, indicando que não há mais blocos alocados:
  last = header->previous;
if (last != NULL) {
last->next = NULL;
} else {
first = NULL;
}
brk(header);

Conclusão

Nossas funções abmalloc() e abfree() agora estão assim:

typedef struct Header {
  struct Header *previous, *next;
  size_t size;
  bool available;
} Header;

Header *first = NULL;
Header *last = NULL;

Header *header_new(Header *previous, size_t size, bool available) {
  Header *header = sbrk(sizeof(Header) + size);
  header->previous = previous;
  header->next = NULL;
  header->size = size;
  header->available = false;
  return header;
}

void *abmalloc(size_t size) {
  if (size == 0) {
    return NULL;
  }
  if (first == NULL) {
    first = last = header_new(NULL, size, false);
    return first + 1;
  }
  Header *header = first;
  while (header != NULL) {
    if (header->available && (header->size >= size)) {
      header->available = false;
      return header + 1;
    }
    header = header->next;
  }
  last = header_new(last, size, false);
  last->previous->next = last;
  return last + 1;
}

void abfree(void *ptr) {
  if (ptr == NULL) {
   return;
  }
  Header *header = (Header*) ptr - 1;
  if (header == last) {
    while ((header->previous != NULL) && header->previous->available) {
      header = header->previous;
    }
    last = header->previous;
    if (last != NULL) {
      last->next = NULL;
    } else {
      first = NULL;
    }
    brk(header);
  } else {
   header->available = true;
  }
 }Code language: PHP (php)

Esta mudança nos permite economizar consideravelmente mais memória. Existem, contudo, problemas a resolver ainda. Por exemplo, considere o seguinte cenário: solicitamos a alocação de um bloco de memória de 8 bytes, e abmalloc() reaproveita um boco de, digamos, 1024 bytes. Há claramente um desperdício.

Veremos como resolver isso no próximo post.

Implementando malloc() e free() — reduzindo ainda mais o heap

Este post faz parte de uma série sobre como implementar as funções malloc() e free(). No artigo anterior, aprendemos como reutilizar blocos de memória. Foi um grande avanço, mas há muito mais a melhorar.

Um exemplo é a redução do tamanho do heap. Quando liberamos o último bloco de memória, movemos o topo do heap para fim do bloco anterior. Contudo, este bloco anterior pode também estar livre, assim como outros. Considere o caso abaixo:

void *ptr1 = abmalloc(8);
void *ptr2 = abmalloc(8);
abfree(ptr1);
abfree(ptr2);

Neste caso, ao liberarmos o bloco apontado por ptr2, fazemos com que ptr1 seja o último bloco. Contudo, ptr1 também está livre, então poderíamos reduzir ainda mais o tamanho do heap.

Para isso, vamos iterar sobre os ponteiros do final da lista até não haver mais blocos livres. Se o cabeçalho do ponteiro recebido apontar para o último bloco e o bloco anterior estiver livre, movemos o ponteiro de cabeçalho para ele. Repetimos esse processo até chegar a um bloco disponível cujo anterior esteja em uso (ou seja NULL, caso seja o primeiro bloco). Em seguida, executamos o procedimento de redução do heap:

  if (header == last) {
while ((header->previous != NULL) && header->previous->available) {
header = header->previous;
}
last = header->previous;
brk(header);
} else {

Agora, porém, precisamos corrigir um bug em abfree(). De acordo com a especificação, a função free() deve aceitar o ponteiro nulo e não fazer nada. Se abfree() receber NULL, teremos uma falha de segmentação! Felizmente, isto é fácil de resolver adicionando uma verificação no início da função:

void abfree(void *ptr) {
   if (ptr == NULL) {
     return;
   }
   Header *header = (Header*) ptr - 1

E assim fica nossa função abfree() até o momento:

void abfree(void *ptr) {
   if (ptr == NULL) {
     return;
   }
   Header *header = (Header*) ptr - 1;
   if (header == last) {
     while ((header->previous != NULL) && header->previous->available) {
       header = header->previous;
     }
     last = header->previous;
     brk(header);
   } else {
     header->available = true;
   }
 }

Reduzir o tamanho do heap é uma otimização simples, mas ainda há desafios a serem enfrentados. Por exemplo, a maneira com que escolhemos qual bloco reutilizar pode levar a heaps maiores que o necessário. Veremos por que isto acontece, e como resolver isto, no próximo post.

Implementando malloc() e free() — reutilizando blocos de memória

Este post faz parte de uma série sobre como implementar as funções malloc() e free(). No artigo anterior, alteramos nossas funções para liberar alguns blocos de memória. Contudo, isto apenas ocorria se os blocos liberados fossem desalocados do mais novo ao mais antigo.

Isto não faria muita diferença. Raramente a memória alocada dinamicamente se comporta com uma pilha, onde o bloco mais novo é sempre desalocado primeiro. A grande vantagem da alocação de memória dinâmica, afinal, é que ela não funciona como uma pilha.

Para entender as limitações de nossa implementação, considere o código abaixo

void *ptr1 = abmalloc(8);
void *ptr2 = abmalloc(8);
abfree(ptr1);
void *ptr3 = abmalloc(8);

Na primeira linha, alocamos oito bytes, e os liberamos na terceira linha. Na última linha, alocamos oito bytes de novo. Contudo, não podemos reaproveitar a memória liberada, pois novos blocos de memória só são alocados no topo do heap. Para economizar memória de verdade, precisamos de uma solução mais sofisticada.

Uma opção é reutilizar blocos livres. Quando desalocarmos um bloco de memória, vamos marcá-lo como disponível para reuso. Quando uma solicitação de memória de tamanho menor ou igual a algum bloco disponível ocorrer, podemos apenas retornar um ponteiro para o bloco disponível e marcá-lo como em uso.

Para isto, acrescentamos ao cabeçalho dos blocos um campo booleano, chamado available, que indicará se o bloco está livre. Como um bloco só pode ser reusado se a memória solicitada por abmalloc() for menor ou igual à disponível no bloco, precisamos também de um campo no cabeçalho indicando o tamanho do bloco, que chamaremos de size.

typedef struct Header {
  struct Header *previous;
  size_t size;
  bool available;
} Header;

Quando o bloco é alocado, o valor do campo available deve ser false (já que o bloco não está disponível). Também registramos o tamanho do bloco no campo size:

void *abmalloc(size_t size) {
  Header *header = sbrk(sizeof(Header) + size);
  header->previous = last;
  header->size = size;
  header->available = false;
  last = header;
  return last + 1;
}

Temos as informações no cabeçalho mas ainda não estamos reusando memória desalocada. Para reaproveitar os blocos disponíveis, precisamos encontrá-los! O algoritmo para isto é bem simples: logo no começo abmalloc() irá iterar sobre os blocos, a partir do último até chegar ao primeiro. Como o ponteiro previous do primeiro bloco é sempre NULL, paramos quando o ponteiro for NULL:

void *abmalloc(size_t size) {
Header *header = last;
while (header != NULL) {
header = header->previous;
}

Em cada iteração, verificamos se o bloco está disponível e tem tamanho aceitável. Se no meio desse processo encontrarmos um bloco disponível maior ou igual ao que precisamos, estamos com sorte! Marcamos o bloco como indisponível, e o retornamos.

void *abmalloc(size_t size) {
Header *header = last;
while (header != NULL) {
if (header->available && (header->size >= size)) {
header->available = false;
return header + 1;
}
header = header->previous;
}

E se não encontrarmos um bloco que satisfaça essas condições? Neste caso a função abmalloc() aumenta o heap, como já fazia antes:

void *abmalloc(size_t size) {
  Header *header = last;
  while (header != NULL) {
    if (header->available && (header->size >= size)) {
      header->available = false;
      return header + 1;
    }
    header = header->previous;
  }
  header = sbrk(sizeof(Header) + size);
  header->previous = last;
  header->size = size;
  header->available = false;
  last = header;
  return last + 1;
}

Na hora de desalocar, temos duas situações possíveis. Se o bloco desalocado por abfree() for o último, nada muda: movemos o topo do heap para o começo do bloco, alteramos o ponteiro last. Mas se o bloco não estiver no topo do heap? Simplesmente marcamos ele como disponível, como pode ser visto na cláusula else da função abaixo:

void abfree(void *ptr) {
   Header *header = (Header*) ptr - 1;
   if (header == last) {
     last = header->previous;
     brk(header);
   } else {
     header->available = true;
   }
 }

Reutilizar blocos de memória é um enorme avanço. Contudo, podemos ser ainda mais eficientes no uso de memória. Por exemplo, só reduzimos o tamanho do heap se desalocamos o último bloco. Se há mais blocos inutilizados logo antes dele, poderíamos liberá-los também. Veremos como fazer isto no próximo post.

Implementando malloc() e free() — adicionando metadados aos blocos de memória

Este post faz parte de uma série sobre como implementar as funções malloc() e free(). Anteriormente, fizemos uma implementação são simplória que praticamente não libera memória nenhuma: um ponteiro aponta para o último bloco alocado, o que permite que abfree() o desaloque, mas somente ele.

Uma opção melhor é fazer o último bloco apontar para o penúltimo, o penúltimo para o antepenúltimo e assim por diante, formando uma lista ligada. Para isso, criamos uma struct que servirá como cabeçalho dos blocos, contendo um ponteiro para o bloco anterior:

typedef struct Header {
  struct Header *previous;
} Header;

Além disso, o ponteiro para o último bloco, que era do tipo void*, agora é do tipo Header*:

Header *last = NULL;

Para usar esses cabeçalhos, abmalloc() reserva memória suficiente para armazenar tanto o cabeçalho quanto o tamanho solicitado:

void *abmalloc(size_t size) {
Header *header = sbrk(sizeof(Header) + size);

Deste modo, utilizamos o início do bloco para armazenar informações necessárias, como um ponteiro para o último bloco alocado antes do novo:

  header->previous = last;

Em seguida, atualizamos last para apontar para o novo bloco:

   last = header;

Por fim, retornamos um ponteiro para a memória que o usuário pode utilizar. Como header aponta para os metadados, não podemos simplesmente retorná-lo. Caso contrário, toda informação do cabeçalho seria sobrescrita quando o usuário usasse o ponteiro! Ao invés disso, retornamos um ponteiro para logo depois do cabeçalho. Este ponteiro é fácil de calcular: é o endereço de memória do cabeçalho mais o tamanho do cabeçalho:

  return header + 1;
}

Note como incrementamos o ponteiro header em 1. Como o tipo do ponteiro é Header*, o incremento será, na verdade, o número de bytes da struct Header, não um byte apenas. Por isso, o tipo do ponteiro incrementado é muito relevante na aritmética de ponteiros.

Agora que nossos blocos de memória possuem metadados no início, é preciso levar isso em conta na hora de desalocar. free() recebe um ponteiro não para o começo do bloco, mas para a memória disponibilizada ao usuário. Logo, precisamos encontrar o começo do bloco a partir do ponteiro que o usuário passou. Nada que uma pequena aritmética de ponteiros não resolva:

void abfree(void *ptr) {
  Header *header = (Header*) ptr - 1;

Se header aponta para o último bloco alocado, o bloco anterior passará a ser o último. Neste caso, podemos retornar memória do heap para o sistema operacional através de brk():

  if (header == last) {
last = header->previous;
brk(header);
}

Eis a nova versão de nossas funções malloc() e free():

typedef struct Header {
   struct Header *previous;
 } Header;

 Header *last = NULL;

 void *abmalloc(size_t size) {
   Header *header = sbrk(sizeof(Header) + size);
   header->previous = last;
   last = header;
   return header + 1;
 }

 void abfree(void *ptr) {
   Header *header = (Header*) ptr - 1;
   if (header == last) {
     last = header->previous;
     brk(header);
   }
 }

abmalloc() e abfree() podem ser ligeiramente mais econômicos em memória agora, mas não muito. Raramente a memória alocada dinamicamente se comporta com uma pilha, onde o bloco mais velho é sempre desalocado primeiro. No próximo post, veremos como utilizar a memória de blocos mais antigos que não estão mais em uso.

Pequenos tipos de tíquetes

Tickets no Jira tendem a acumular campos redundantes e opcionais, ficando complexos e confusos. Gosto do Jira, mas compreendo a frustração que isso causa. Por isso, inspirado pelas , sugiro aplicar a abordagem de criar tíquetes menores.

Apenas três estados

Uma maneira de limitar o tamanho dos tickets é simplificar o workflow restringindo o número de estados. Por exemplo, podemos definir que cada tipo de tíquete teria, no máximo, três estados:

  • A fazer
  • Em progresso
  • Concluído

Para representar outros estágios, podemos criar de novos tipos de tíquetes, como subtarefas.

Um tipo de tíquete medianamente complexo

Vejamos um exemplo. Considere o tíquete abaixo:

Chave: XYZ-1234. Status: Em teste. Título: Demônios nasais. Descrição: Chamar free() em ponteiro previamente desalocado resulta em demônios saindo do nariz. Análise técnica: A causa-raiz é um comportamento indefinido. Resultado de teste: O patch não funciona. Agora fantasmas saem pelas orelhas. Data de lançamento: 2023-12-22.

Ele seguiria por este workflow:

Aberto ⇨ A fazer ⇨ Em análise ⇨ Fazendo ⇨ Em teste ⇨ A lançar ⇨ Feito

Como poderíamos reduzir o número de fases?

Podemos começar removendo estágio “Em análise. No seu lugar, criamos um novo tipo de tíquete, chamado “Análise Técnica”. Assim, a tarefa original ficará em execução (“Fazendo”) enquanto a análise técnica estiver em andamento.

Menos campos em um tíquete

Uma vantagem disto seria transferência de campos para subtarefas. Campos que se misturariam no tíquete original apareceriam apenas nas tarefas em que são relevantes.

Considere o campo “Data de lançamento”, que só faz sentido na fase “A Lançar”. Se desenvolvedores, testadores etc. não são responsáveis pelo lançamento, este campo é confuso e polui a tarefa original. Com um novo tipo de tarefa chamado “Release“, esse campo estaria no lugar mais apropriado, mantendo o tíquete original sucinto.

Repetindo estágios sem regredir

Outra vantagem é que o ticket original pode passar pelo mesmo “estágio” várias vezes. É comum um tíquete ter uma fase de desenvolvimento, seguida por testes de qualidade, por exemplo. Só que, se surgir um problema na avaliação, não é recomendável retroceder à fase de desenvolvimento. Como lidar com isso?

Ao trabalhar com subtarefas, podemos marcar a validação como concluída e criar um novo tíquete de implementação. No nosso tíquete, por exemplo, podemos remover a fase “Em teste” e criar uma subtarefa do tipo “Testagem”, assim como uma outra chamada “Desenvolvimento”. Cada vez que o teste falhar, fechamos a testagem e abrimos uma nova tarefa de desenvovimento.

Resultado

Seguindo esta estratégia, nosso tíquete ficaria assim:

Chave: XYZ-1234. Status: Em teste. Título: Demônios nasais. Descrição: Chamar free() em ponteiro previamente desalocado resulta em demônios saindo do nariz. Links: XYZ-1235 Análise técnica; XYZ-2345 Remover trecho em latim; XYZ-2345 Usar função medium(); XYZ-3456 Testar função medium(); XYZ-4444 Plano de release

E o workflow ficaria bem mais simples:

Naturalmente, esta estratégia é flexível. No nosso caso, por exemplo, não removemos a fase “A fazer” ainda. Restringir a cinco (incluindo backlog e validação) é outra possibilidade, mas não muito mais que isso.

Conclusões

Em programação, é comum encontrar os chamados “God objects“, objetos enormes que são responsáveis por várias funções diferentes. Quebrá-los é uma maneira segura de obter qualidade de código. Por isso, suspeito que o mesmo princípio pode se aplicar a tíquetes no Jira.

Não sou o gerente de projeto, mas como programador, acredito que limitar o tamanho e os passos dos tickets pode ser uma ideia eficaz. Fico curioso para saber se alguém já experimentou isso e como foi.

Implementando malloc() e free() — primeiros passos

Seguindo a maravilhosa jornada que é ler  Crafting Interpreters, cheguei no ponto onde implementamos um interpretador em C! Como sempre, Bob Nystrom impiedosamente nos propõe desafios interessantíssimos que nos mantém ocupados por longos períodos. Por exemplo, neste capítulo ele nos sugere implementar nosso próprio alocador de memória, sem a menor necessidade! Inevitavelmente, caí na armadilha.

O desafio nos permite alocar uma grande região de memória e gerenciá-la, mas decidi implementar a função malloc() do zero. Como uso Ubuntu, foi necessário primeiramente entender melhor o layout da memória de um processo no Linux.

Considere o diagrama abaixo, que representa o layout da memória de um processo.

Layout da memória alocada para um processo no Linux: primeiro, temos um bloco com o código executável, depois blocos para as variáveis globais e estáticas (inicializadas e não inicializadas). Segue-se um largo bloco de memória não utilizado no início do programa, depois os argumentos e variaveis de ambiente, e um block reservado ao kernel. O bloco não utilizado será, com o tempo, utilizado para variáveis locais (stack) e blocos dinamicamente alocados (heap).

Na memória alocada para o processo, há várias seções. Quando o programa inicia sua execução, a parte acinzentada não está em uso ainda. Ao longo de sua execução, o programa declara variáveis locais, fazendo a stack (pilha) crescer para trás.

Já a memória dinamicamente alocada é obtida do heap, que cresce na direção oposta. A maneira bem popular de expandir o heap é aumentando o tamanho do segmento de data (i.e. a seção que contém variáveis globais e estáticas) com a chamada de sistema sbrk().

O diagrama acima ilustra como esta chamada de sistema funcional funciona. sbrk() recebe como parâmetro um número inteiro que irá ser somado ao ponteiro que indica o fim do segmento de data. Depois disso, sbrk() retorna o valor do ponteiro antes do incremento.

De certo modo, o comportamento de sbrk() já é suficiente para alocar memória. Nossa funçaõ malloc() pode simplesmente invocar sbrk() e retornar para o usuário o ponteiro para o início do bloco de memória alocada:

void *abmalloc(size_t size) {
return sbrk(size);
}

Em princípio, free() não precisa fazer nada: como nesta implementação sempre usamos a memória do topo do heap, não há nada que possamos fazer para reutilizar blocos de memória mais antigos. Nesse sentido, free() pode perfeitamente ser um no-op:

void abfree(void *ptr) {
}

Uma operação útil pode ser feita, porém, se o bloco a ser liberado tiver sido o último a ser alocado. Isto significa que ele está no topo da pilha, então basta mover o ponteiro do topo da pilha para o início do bloco desalocado. Para isto, primeiramente devemos saber onde começa o último bloco de memória alocado, o que é fácil com uma variável global:

void *last_block = NULL;

void *abmalloc(size_t size) {
last_block = sbrk(size);
return last_block;
}

Depois, na função free(), verificamos se o ponteiro passado aponta para o último bloco. Neste caso, movemos o topo da pilha para trás com a chamada de sistema brk(). Esta syscall recebe como parâmetro um ponteiro e, se este ponteiro é um valor “razoável” (não é nulo, não aponta para dentro da stack, não aponta para antes do heap), usa o valor do ponteiro como novo topo do heap. O resultado seria algo assim.

void abfree(void *ptr) {
  if (ptr == last_block) {
      brk(last_block);
  }
}

Esta desalocação, porém, é inútil na prática. Considere o exemplo abaixo:

void *ptr1 = abmalloc(8);
void *ptr2 = abmalloc(8);
abfree(ptr2);
abfree(ptr1);

Com a versão atual de abfree(), conseguimos liberar a memória apontada por ptr1, mas não a apontada por ptr2. Para poder liberar ptr2, seria preciso saber que, uma vez que ptr1 foi desalocada, o próximo último bloco é ptr2. Poderíamos criar uma variável second_last_block? Não adiantaria: teríamos o mesmo problema com o antepenúltimo bloco, e assim por diante.

Precisamos de uma estrutura de dados mais poderosa aqui, e é isto que veremos no nosso próximo post.

Substituindo métodos de set-up por utilitários de teste

(Este é um post que publiquei no antigo blog da SEA Tecnologia. Como ainda é relevante, resolvi republicá-lo.)

Uma das muitas aprendizagens que adquiri na antiga SEA Tecnologia é a criação de utilitários de teste.

Utilitários de teste são uma maneira de reaproveitar código em testes unitários. Usualmente, isso é feito utilizando os métodos setUp ou @Before dos casos de teste, mas isso tem algumas desvantagens. Por exemplo, em um caso de teste, podemos ter a seguinte inicialização:

private Address address;
private AddressDAO addressDAO;

@Before
public void setUp() {
    address = new Address();
    address.setStreet("Rua fulano");
    address.setNumber("123/A");
    addressDAO = new AddressDAO();
}Code language: PHP (php)

Essa inicialização funciona bem no teste abaixo…

@Test
public void testGetAllAddresses(){
    addressDAO.addAddress(address);

    List<Address> addresses = addressDAO.getAllAddresses();

    assertEquals(1, addresses.size());
    assertEquals("Rua fulano", addresses.get(0).getStreet());
    assertEquals("123/A", addresses.get(0).getNumber());
} Code language: PHP (php)

Agora, se tivermos o teste a seguir, o objeto criado é desperdiçado:

@Test
public void testGetNoAddress() {
    List<Address> addresses = addressDAO.getAllAddresses();

    assertEquals(0, addresses.size());
}Code language: PHP (php)

Se o código for como o seguinte, teremos redundância de código. também temos de decidir SE o outro objeto deve ser criado no @Before também ou no método.

@Test
public void testGetAllAddressesMoreThanOne() {
    addressDAO.addAddress(address);
    Address address2 = new Address();
    address2.setStreet("Outra rua");
    address2.setNumber("111");
    addressDAO.addAddress(address2);
    List<Address> addresses = addressDAO.getAllAddresses(); 
    assertEquals(1, addresses.size());
    assertEquals("Rua fulano", addresses.get(0).getStreet());
    assertEquals("123/A", addresses.get(0).getNumber()); 
}Code language: PHP (php)

Esses inconvenientes são menores quando comparados à tarefa de criar uma rede de dependências. Por exemplo, para testar uma classe Person que agrega um Address em um outro caso de teste, teremos de ter um @Before semelhante a esse:

private Person person;
private Address address;
private PersonDAO personDAO;

@Before     
public void setUp() {
    address = new Address();
    address.setStreet("Rua fulano");
    address.setNumber("123/A");
    person = new Person();
    person.setName("João");
    person.setAddress(address);
    personDAO = new PersonDAO();
} Code language: PHP (php)

O código para a criação de endereços foi duplicado, e é difícil criar as dependências. Nesses exemplos, vemos casos simples, mas é fácil visualizar como a situação irá se complicar.

Nós solucionamos esse problema criando uma classe para criar esses objetos. Essa classe seria algo como isso:

public class TestUtil {
    public static Address utilCreateAddress(String street, String number) {
        Address address = new Address();
        address.setStreet("Rua fulano");
        address.setNumber("123/A");
        return address;     
    }

    public static Person utilCreatePerson(String name, Address address) {
        Person person = new Person();
        person.setName(name);
        person.setAddress(address);
        return person;
    }
}Code language: JavaScript (javascript)

Nossos casos de teste estendiam a TestUtil, facilitando a criação de objetos:

public class TestAddress2 extends TestUtil {
    private AddressDAO addressDAO = new AddressDAO();

    @Test
    public void testGetAllAddresses() {
        Address address = utilCreateAddress("Rua fulano", "123/A");
        addressDAO.addAddress(address);

        List<Address> addresses = addressDAO.getAllAddresses();

        assertEquals(1, addresses.size());
        assertEquals("Rua fulano", addresses.get(0).getStreet());
        assertEquals("123/A", addresses.get(0).getNumber());
    }

    @Test
    public void testGetNoAddress() {
        List<Address> addresses = addressDAO.getAllAddresses();

        assertEquals(0, addresses.size());
    }

    @Test
    public void testGetAllAddressesMoreThanOne() {
        Address address = utilCreateAddress("Rua fulano", "123/A");
        Address address2 = utilCreateAddress("Outra rua", "111");
        addressDAO.addAddress(address);
        addressDAO.addAddress(address2);

        List<Address> addresses = addressDAO.getAllAddresses();

        assertEquals(2, addresses.size());
        assertEquals("Rua fulano", addresses.get(0).getStreet());
        assertEquals("123/A", addresses.get(0).getNumber());
    } 
} Code language: PHP (php)

Como também precisávamos frequentemente de um objeto qualquer, ou que apenas um ou outro parâmetro fosse definido, criávamos variantes dos métodos:

public static Address utilCreateAddress() {
    return utilCreateAddress("Qualquer", "Qualquer");
}

public static Person utilCreatePerson() {
    return utilCreatePerson("José", utilCreateAddress());
} Code language: PHP (php)

Aprendemos isto em um projeto um tanto complexo, com grandes redes de dependências de objetos. O uso desses utilitários de teste viabilizou a prática de TDD no sistema. Era emocionante descobrir que, para criar aquele documento que dependia de sete outros documentos e uns cinco ou seis usuários, bastava chamar um método.

Naturalmente, há mais sobre nossos utilitários de teste do que foi escrito aqui, e pode haver mais ainda que sequer fizemos. (Por exemplo, pode ser interessante produzir utilitários de teste para classes específicas, ao invés de um gigantesco utilitário) Entretanto, como a ideia é bem simples, esperamos que esse pontapé inicial lhe motive a pensar sobre o tema. Até mais!

Sem Comentários. E agora?

Tradicionalmente, considera-se uma boa prática comentar o código. Há algum tempo, tem-se revisto este conceito. Na Liferay, por exemplo, seguimos uma política de não comentar código. Pessoalmente, sou um entusiasta desta filosofia. Mas não quero apresentar ou defender esta estratégia: há muito material bom sobre isto. Quero discutir uma questão em aberto.

Quem comenta quer transmitir alguma informação importante. Que informação é essa? E, mais importante ainda, onde podemos registrá-la? Vejamos algumas alternativas.

O que essas linhas fazem?

Os nomes de funções são excelentes para explicar o que o código faz. Se um bloco de código precisa de um comentário, considere extraí-lo para uma função ou classe. O nome da entidade já esclarecerá seu propósito.

Observe, por exemplo, as linhas abaixo, retiradas desta classe de testes:

Assert.assertNotNull(recurrence);
Assert.assertNull(recurrence.getUntilJCalendar());
Assert.assertEquals(0, recurrence.getCount());Code language: CSS (css)

Essas linhas verificam se a RRule de um evento tem certas propriedades: ela deve existir, ter um “untilCalendar nulo e uma contagem de zero.

Os conceitos são complexos; eu mesmo me confundiria ao reler estes asserts. Um comentário poderia explicá-los. Mas este commit já esclareceu tudo ao mover essas linhas para um método e invocá-lo:

assertRepeatsForever(recurrence);

Aquelas asserções verificavam se o evento se repete eternamente! Nenhum comentário foi necessário — felizmente, pois estes asserts estavam em vários testes.

O que está acontecendo?

Se o comentário iria explicar algo relevante em tempo de execução, considere transformá-lo em uma mensagem de log! Note o exempo abaixo.

if (Validator.isBlank(serviceAccountKey)) {
	// If no credentials are set for GCS Store, the library will
	// use Application Default Credentials.
	_googleCredentials =
		ServiceAccountCredentials.getApplicationDefault();
}
else {
	_googleCredentials = ServiceAccountCredentials.fromStream(
		new ByteArrayInputStream(serviceAccountKey.getBytes()));
}Code language: JavaScript (javascript)

Este comentário pode ser relevante para quem lê o código. Contudo, seria crucial para alguém investigando um problema de autenticação. Por isso, na prática, escolhi logar uma mensagem:

if (Validator.isBlank(serviceAccountKey)) {
	if (_log.isInfoEnabled()) {
		_log.info(
			"No credentials set for GCS Store. Library will use " +
				"Application Default Credentials.");
	}

	_googleCredentials =
		ServiceAccountCredentials.getApplicationDefault();
}
else {
	_googleCredentials = ServiceAccountCredentials.fromStream(
		new ByteArrayInputStream(serviceAccountKey.getBytes()));
}Code language: JavaScript (javascript)

Por que este código está aqui?

Comentários para explicar por que algumas linhas estão ali também são comuns. Um local melhor para compartilhar essas informações são as mensagens de commits.

Estes dias, por exemplo, me pediram para ajudar com um código em que trabalhei anos atrás. Lendo uma JSP — lembre-se, anos atrás — eu encontrei essas linhas:

<liferay-portlet:renderURL portletName="<%= KaleoDesignerPortletKeys.KALEO_DESIGNER %>" var="viewURL">
	<portlet:param name="mvcPath" value="/designer/view_kaleo_definition_version.jsp" />
	<portlet:param name="redirect" value="<%= currentURL %>" />
	<portlet:param name="name" value="<%= kaleoDefinitionVersion.getName() %>" />
	<portlet:param name="draftVersion" value="<%= kaleoDefinitionVersion.getVersion() %>" />
</liferay-portlet:renderURL>Code language: HTML, XML (xml)

Esta tag está gerando uma URL para ser utilizada em outro lugar. Mas meus olhos treinados acharam estranho aquele parâmetro portletName. Este valor costumar ser definido automaticamente.

Um git blame esclareceu tudo, quando encontrei este commit. A mensagem é clara:

LPS-74977 / LPS-73731 By making the render URL explicitly use the Kaleo Designer name, it will be valid when rendered in another portlet.

Entendi! Este código provavelmente vai ser invocado por algum outro portlet. Neste caso, o valor seria automaticamente setado pela outra aplicação, e por alguma razão queremos evitar isso.

(Por esta razão, aliás, prefiro commits pequenos: eles facilitam descobrir a razão de trechos de código bem específicos. É como se todas as linhas de código tivessem um comentário! Não é uma posição unânime, porém: há quem prefira commits maiores.)

A razão da linha foi esclarecida. Mas por que ela pode ser invocada de outra aplicação? Isto não é usual…

Por que esta mudança foi feita?

Um código bem escrito explica como algo foi implementado. A mensagem de commit esclarece o porquê, mas em um contexto local. Como explicar a motivação mais ampla por trás de um código sem recorrer a comentários?

Os tíquetes do issue tracker são excelentes para isto. Normalmente escritos para guiar o desenvolvimento, esses documentos ajudam demais na interpretação do código. Se adicionarmos a chave do tíquete à mensagem de commit, podemos rastrear as razões.

Voltando ao exemplo acima. Descobrimos que uma linha permite usar o mesmo código em vários portlets. Mas isso raramente é necessário. Por que precisamos reutilizar o código neste caso? Por sorte, a mensagem menciona dois tíquetes. Fui verificar o mais antigo; cheguei a LPSA-64324:

[Information Architecture] EE – As a portal admin, I would like to access all workflow portlets from the control panel section under the same tab.

O título já ajuda, e o texto esclarece de vez. Por razões de usabilidade, três aplicações diferentes passaram a aparecer em abas de um mesmo portlet. Faz todo sentido!

Os comentários que a gente gosta

É importante destacar que tentamos evitar comentários desorganizados, que se entrelaçam no código e tentam explicar trechos difíceis de entender. Há vários comentários, frequentemente com formatos padronizados, que não atrapalham a leitura. Um exemplo óbvio são os cabeçalhos de copyright.

Outra maneira de usar comentários efetivamente é a programação letrada. Neste estilo de programação, os comentários são a estrela do show: o código-fonte contem mais prosa do que código executável. Isto é útil quando explicar o algoritmo é mais importante do que lê-lo, como em pesquisas acadêmicas e análise de dados. Não por acaso, é o paradigma de ferramentas populares como Jupyter Notebook e Quarto.

Mais relevante ainda, ferramentas como Javadoc, JSDoc, Doxygen etc. leem comentários em um formato específico para gerar documentação. Estes comentários não afetam a legibilidade. Pelo contrário: javadocs são ótimos para explicar como usar estas entidades. Combinados com ferramentas como meu querido Doctest, temos até garantias de acurácia e atualidade!

Um mundo de possibilidades

Esses são apenas alguns exemplos de alternativas aos comentários. Há muitas outras opções, como wikis, blogs. Já encontrei a explicação para um código que escrevi no Stack Overflow! Podemos pensar em ainda mais soluções para atender a diferentes necessidades. O ponto principal é que, tendo estas ferramentas à nossa disposição, adicionar comentários diretamente ao código torna-se desnecessário.

Naturalmente, evitar comentários é apenas uma das formas para se escrever código legível. Comentários não são proibidos; de fato, há estratégias que podem torná-los eficazes. No entanto, na minha experiência, comentar indisciplinadamente leva a piores resultados, e essas técnicas ajudam a documentar informações importantes que não cabem diretamente no código.

Você é um adepto da estratégia “sem comentários”? Se sim, que outros meios você usa para transmitir infirmações? Se não, como você faz para ter comentários efetivos? Que tipo de comentário você não vê sendo substituído por essas abordagens? Adoraria escutar suas opiniões.

Dez anos de Liferay

Dias atrás, recebemos aqui em casa um pacote inesperado. O que encontramos dentro dele foi ainda mais surpreendente! O que estaria acontecendo?

Uma caixa de iPad com um cartão sobre ela. O cartão tem o número "10" escrito em cor dourada.

Bem, acontece que há alguns meses atrás, eu completei incríveis dez anos trabalhando na Liferay! Isso não é apenas um long período, mas também uma jornada que me proporcionou muito crescimento. Morei em duas cidades, viajei para algumas outras ao redor do mundo, aprendi a trabalhar remotamente, lidei com inúmeras tecnologias e testemunhei o crescimento da filial da LATAM, que saiu de um punhado de pessoas para centenas.

Um cartão com um cabeçalho escrito: "Happy Liferay Anniversary."

Abaixo, escrito à mão:

"Adam,
É uma honra escrever este cartão para vocÊ comemorar seus 10 anos de Liferay.
Um trabalho feito com comprometimento e dedicação sempre gera bons frutos.
Tenho muitok orgulho de ter feito parte da sua história. Que você continue sendo inspiração para todos nós.
Feliz 10 anos de LIferay!! Que venham muito mais..."

Hoje em dia, é raro permanecer tanto tempo no mesmo lugar, especialmente em uma carreira na área de tecnologia. No entanto, a Liferay é realmente um local agradável para trabalhar, onde sempre há coisas novas para aprender e desafios, sejam da tecnologia, do trabalho em equipe ou do cuidado com o cliente. Sem dúvida, cresci muito e, ao que parece, ainda tenho espaço aqui para evoluir ainda mais!

Portanto, agradeço a todos pelo presente, mas, o que é mais importante, agradeço pelo ótimo momento, pelo crescimento e pelos desafios. E preparem-se, pois pretendo ser uma “incomodação” encantadora entre todos vocês por muitos e frutíferos anos que estão por vir! 😄🎉