<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="pt-BR"><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://codesilva.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://codesilva.com/" rel="alternate" type="text/html" hreflang="pt-BR" /><updated>2026-09-29T17:58:54+00:00</updated><id>https://codesilva.com/feed.xml</id><title type="html">CodeSilva</title><subtitle>Bem-vindo ao meu blog pessoal, CodeSilva! Como um aspirante a Engenheiro de Software, estou aqui para compartilhar minhas experiências, percepções e ocasionais desventuras de programação.</subtitle><entry xml:lang="pt-BR"><title type="html">Deixando o node:sqlite mais rápido, passo a passo</title><link href="https://codesilva.com/programacao/2026/08/21/deixando-o-node-sqlite-mais-rapido-passo-a-passo.html" rel="alternate" type="text/html" title="Deixando o node:sqlite mais rápido, passo a passo" /><published>2026-08-21T00:00:00+00:00</published><updated>2026-08-21T00:00:00+00:00</updated><id>https://codesilva.com/programacao/2026/08/21/deixando-o-node-sqlite-mais-rapido-passo-a-passo</id><content type="html" xml:base="https://codesilva.com/programacao/2026/08/21/deixando-o-node-sqlite-mais-rapido-passo-a-passo.html"><![CDATA[<p>Ler uma linha do SQLite no Node ficou uns <strong>17% mais rápido</strong>. Um loop que puxava
1,48 milhão de linhas por segundo agora puxa 1,73 milhão, mesma máquina, mesma
query. Nos vinte benchmarks de leitura que o Node tem, a maioria ficou mais
rápida e nenhum ficou mais lento.</p>

<p>O patch que fez isso <strong>tira 35 linhas</strong> do arquivo. Ele não reescreve nada e não
adiciona nada: o caminho rápido já estava lá, usado por exatamente uma das três
funções que queriam ele. E
<a href="https://github.com/nodejs/node/pull/65276">entrou no core do Node.js</a> semana
passada. Daí a parte que vale um post não é qual foi a mudança - é como uma coisa
tão barata ficou à vista de todos num arquivo em que eu moro há meses.</p>

<p>Ler o código não foi o que achou. Medir foi. O Nate Berkopec põe essa disciplina
inteira numa frase no <em>The Complete Guide to Rails Performance</em>, e é a única
parte daquele curso que eu chamaria de obrigatória:</p>

<blockquote>
  <p><strong>Repita comigo: eu não vou otimizar nada na minha aplicação até minhas
métricas mandarem.</strong></p>
</blockquote>

<p>O Brendan Gregg dá nome aos jeitos de errar isso no <em>Systems Performance</em>. O
<strong>Street Light Anti-Method</strong> é investigar com a ferramenta que você já conhece,
que é procurar a chave onde tem luz em vez de onde ela caiu. O <strong>Random Change
Anti-Method</strong> é mudar coisa até algum número se mexer. Os dois produzem
atividade. Nenhum produz conhecimento.</p>

<p>Então segue a alternativa, na ordem em que eu rodei de verdade - e a virada
acontece no passo 4, onde apareceu que um décimo de cada leitura de uma linha ia
pra montar os <em>nomes</em> das colunas. Quatro strings que nunca mudam, reconstruídas do
zero toda vez.</p>

<h2 id="1-deixe-a-baseline-honesta">1. Deixe a baseline honesta</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git rev-list --left-right --count main...upstream/main
0	91
</code></pre></div></div>

<p>Noventa e um commits atrás. Faz o merge, recompila, e daí vem a linha que importa
mais do que parece:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ make -j10
$ cp out/Release/node /tmp/node-main
</code></pre></div></div>

<p>O <code class="language-plaintext highlighter-rouge">make</code> deixa um symlink <code class="language-plaintext highlighter-rouge">./node</code> apontando pra <code class="language-plaintext highlighter-rouge">out/Release/node</code>. Aponta um
benchmark pro <code class="language-plaintext highlighter-rouge">./node</code> e ele silenciosamente segue o que você compilou por
último, então você acaba comparando com todo cuidado um binário contra ele mesmo
e chamando o resultado de ganho. <strong>Copie o binário de verdade pra fora antes de
mexer em qualquer coisa.</strong></p>

<h2 id="2-descubra-o-noise-floor">2. Descubra o <code class="language-plaintext highlighter-rouge">noise floor</code></h2>

<p>Agora rode o benchmark da baseline <strong>contra ela mesma</strong> - binário idêntico dos
dois lados, então toda diferença que aparecer é ruído de medição. Essa variação é
o seu limite de detecção, e qualquer “ganho” menor que ela depois é
infalsificável.</p>

<p>Mesmo motivo pelo qual você sobe na balança duas vezes antes de acreditar que
perdeu 200 gramas.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./out/Release/node benchmark/compare.js \
    --old /tmp/node-main --new /tmp/node-main \
    --runs 10 --no-progress \
    --set n=20000 --set tableSeedSize=10000 \
    --filter sqlite-prepare-select-get.js \
    --filter sqlite-prepare-select-all.js sqlite &gt; noise-floor.csv
</code></pre></div></div>

<p>O Node já tem o <code class="language-plaintext highlighter-rouge">benchmark/sqlite/</code>, e vale usar: é a régua pela qual um
maintainer vai julgar o seu PR, e tira qualquer discussão sobre se o seu harness
era justo.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>improvement  p-value    old rate    new rate   config
  -1.52%     0.1832      2.31M      2.28M  select-all 'SELECT text_column, integer_column FROM foo LIMIT 1'
  -0.31%     0.8124      1.52M      1.52M  select-get 'SELECT * FROM foo LIMIT 1'
+   1.30%     0.3721      1.49M      1.51M  select-get 'SELECT text,int,real,blob FROM foo LIMIT 1'
+   1.96%     0.2159      3.30M      3.37M  select-get 'SELECT 1'

configs: 20  significant: 0
geomean speedup (all configs): 0.38%
</code></pre></div></div>

<p>Três coisas dessa saída, se ela for nova pra você, porque o resto do post se
apoia nelas. O <strong>p-value</strong> é a chance de aparecer uma diferença desse tamanho se
a mudança não fizesse nada - então um valor alto, que nem o 0.81 ali, quer dizer
“isso pode facilmente ser nada”. Um resultado é chamado de <strong>significativo</strong>
quando essa probabilidade fica pequena o bastante pra você apostar contra, e mais
pra frente no post você vai ver as linhas significativas marcadas com estrelas. E
<strong>geomean</strong> é a linha de resumo: uma média entre as vinte configurações, feita de
um jeito que um número espetacular não carrega o total sozinho.</p>

<p>Aqui, zero resultados são significativos, que é o que binários idênticos deviam
produzir. A variação inteira, de -1,52% a +1,96%, é a máquina conversando com ela
mesma.</p>

<p><strong>Então: 2%.</strong> Abaixo disso, não estou autorizado a chamar de ganho pelo resto
deste post.</p>

<blockquote>
  <p>“O primeiro princípio é que você não deve se enganar - e você é a pessoa mais
fácil de enganar.” O Feynman estava falando de física, mas o <code class="language-plaintext highlighter-rouge">noise floor</code> é
essa frase virada em comando de shell.</p>
</blockquote>

<h2 id="3-benchmark-e-profiling-respondem-perguntas-diferentes">3. Benchmark e profiling respondem perguntas diferentes</h2>

<p>O Berkopec conta a melhor versão dessa história. Ele tinha feito benchmark de uma
mudança, achado o <code class="language-plaintext highlighter-rouge">shuffle</code> 12x mais rápido que <code class="language-plaintext highlighter-rouge">sort_by { rand }</code>, e levou o
número pro Ryan Davis, autor do <code class="language-plaintext highlighter-rouge">minitest</code>. A resposta:</p>

<blockquote>
  <p>“você fez benchmark, mas fez profiling?”</p>
</blockquote>

<p>Um benchmark te dá um número por configuração. Ele te diz sem pestanejar que sua
mudança deixou tudo 3% mais rápido, e está completamente errado, porque 3% está
dentro do ruído da máquina onde você rodou. E ele não tem nada a dizer sobre
<em>por que</em> algo está lento: rode a suíte de sqlite inteira do Node num <code class="language-plaintext highlighter-rouge">main</code>
limpo e você recebe vinte taxas e zero suspeitos.</p>

<p>Um profiler te dá atribuição. O que ele não te dá é se consertar aquilo vale
alguma coisa, porque profile não tem grupo de controle.</p>

<p><strong>O profiler aponta o suspeito, o benchmark condena.</strong> Nessa ordem.</p>

<h2 id="4-faça-o-profile-daí-leia-a-árvore-de-chamadas">4. Faça o profile, daí leia a árvore de chamadas</h2>

<p>O <code class="language-plaintext highlighter-rouge">sample</code> já vem no macOS. Duas coisas pegam todo mundo na primeira vez: ele se
<strong>anexa</strong> a um processo que já está rodando, nunca sobe um, e ele casa nome
parcial, então <code class="language-plaintext highlighter-rouge">sample node</code> com um language server aberto pode acabar
perfilando outra coisa completamente. Use o PID. E rode <code class="language-plaintext highlighter-rouge">dsymutil
out/Release/node</code> antes, ou você recebe endereços crus em vez de símbolos.</p>

<p>Também não faça profile do harness de benchmark - ele mistura seed, warmup e
medição num processo só. Escreva uma carga que chega num estado estável e fica
lá:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">stmt</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">prepare</span><span class="p">(</span><span class="s2">`SELECT </span><span class="p">${</span><span class="nx">cols</span><span class="p">}</span><span class="s2"> FROM foo LIMIT </span><span class="p">${</span><span class="nx">limit</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">run</span> <span class="o">=</span> <span class="nx">op</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">all</span><span class="dl">'</span> <span class="p">?</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">stmt</span><span class="p">.</span><span class="nf">all</span><span class="p">()</span> <span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">stmt</span><span class="p">.</span><span class="nf">get</span><span class="p">();</span>

<span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">20000</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="nf">run</span><span class="p">();</span>          <span class="c1">// aquecimento</span>
<span class="nx">process</span><span class="p">.</span><span class="nx">stderr</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="s2">`READY pid=</span><span class="p">${</span><span class="nx">process</span><span class="p">.</span><span class="nx">pid</span><span class="p">}</span><span class="s2">\n`</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">deadline</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">+</span> <span class="nx">seconds</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">;</span>   <span class="c1">// estado estável</span>
<span class="k">while </span><span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">&lt;</span> <span class="nx">deadline</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">1000</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="nx">sink</span> <span class="o">=</span> <span class="nf">run</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Duas armadilhas na leitura do que volta. Os maiores símbolos da página são
<code class="language-plaintext highlighter-rouge">__psynch_cvwait</code>, <code class="language-plaintext highlighter-rouge">kevent</code> e <code class="language-plaintext highlighter-rouge">semaphore_wait_trap</code> - threads ociosas do
threadpool do libuv paradas no kernel. Não querem dizer nada. E um mesmo custo se
espalha por vários nomes de símbolo, então agrupe por categoria antes de comparar
magnitudes.</p>

<p>Aqui está o <code class="language-plaintext highlighter-rouge">get()</code> numa linha de quatro colunas, como fração das amostras não
ociosas:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  1865   20.4%  pthread mutex
  1732   18.9%  sqlite VDBE + btree (real query work)
  1382   15.1%  V8 object construction (dictionary-mode rows)
  1067   11.7%  malloc/free
   739    8.1%  column-name interning (V8 strings)
   542    5.9%  V8 buffers (BLOB -&gt; Uint8Array)
   411    4.5%  sqlite C API entry points
   159    1.7%  node:sqlite binding
</code></pre></div></div>

<p>A linha que eu quero é a quinta: <strong>column-name interning, 8.1%.</strong></p>

<p>Eu já tinha um palpite sobre essa. O <code class="language-plaintext highlighter-rouge">iterate()</code> vinha entregando nomes de coluna
cacheados há um tempo, e eu já tinha me perguntado mais de uma vez se o <code class="language-plaintext highlighter-rouge">get()</code> e
o <code class="language-plaintext highlighter-rouge">all()</code> não podiam beber da mesma fonte. O que eu não tinha era motivo pra
mexer. Palpite não é número, e a regra do Berkopec é que palpite não autoriza
patch. Eu tenho uma pasta cheia de palpites sobre aquele arquivo e a maioria não
vale nada. <strong>8.1% foi o que tirou esse da pasta.</strong></p>

<blockquote>
  <p>A linha de cima é um número maior e não é dela que esse post trata. É o mutex
por conexão do SQLite, some com uma flag de compilação, e tirar ele significa
escrever na mão o lock que falta. Outra investigação, outro PR, e eu
ainda não sei se aquele entra - e é por isso que ele não divide post com um
patch que já entrou.</p>
</blockquote>

<p>Agora, 8,1% das amostras em símbolos que nem <code class="language-plaintext highlighter-rouge">StringTable::LookupKey</code> não diz de
<em>quem</em> são aquelas strings, e a V8 interna string por uma dúzia de motivos. Esse
é o trabalho da árvore de chamadas:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>141 node::sqlite::StatementExecutionHelper::Get(...) + 396
  | 121 v8::String::NewFromUtf8(...)
  | : 88 v8::internal::Factory::InternalizeUtf8String(...)
  | : | 54 v8::internal::FactoryBase&lt;...&gt;::InternalizeString(...)
  | : | + 36 v8::internal::StringTable::LookupKey&lt;...&gt;(...)
  | : | 23 v8::internal::FactoryBase&lt;...&gt;::InternalizeString(...)
  | : | + 23 v8::internal::StringHasher::HashSequentialString&lt;...&gt;(...)
  | : 30 v8::internal::Factory::InternalizeUtf8String(...)
  | : | 30 v8::internal::Utf8DecoderBase&lt;...&gt;::Utf8DecoderBase(...)
 78 node::sqlite::StatementExecutionHelper::Get(...) + 372
  | 29 columnName  (in node)
  | : 13 _pthread_mutex_lock_init_slow  (in libsystem_pthread.dylib)
  | 21 columnName  (in node)
  | 18 columnName  (in node)
</code></pre></div></div>

<p>Dois offsets de instrução vizinhos dentro da mesma função, e são as duas metades
de uma operação só. O <code class="language-plaintext highlighter-rouge">+372</code> chama <code class="language-plaintext highlighter-rouge">sqlite3_column_name()</code>, cuja implementação no
<code class="language-plaintext highlighter-rouge">sqlite3.c</code> é o <code class="language-plaintext highlighter-rouge">columnName</code>. O <code class="language-plaintext highlighter-rouge">+396</code> chama <code class="language-plaintext highlighter-rouge">String::NewFromUtf8</code> com
<code class="language-plaintext highlighter-rouge">kInternalized</code>, e a V8 faz o serviço inteiro: decodifica o UTF-8, calcula o hash,
procura na string table.</p>

<p>Tudo isso <strong>por coluna, por chamada, para um <code class="language-plaintext highlighter-rouge">prepared statement</code> cujos nomes de
coluna não podem mudar.</strong></p>

<p>Está aqui no fonte, e não tem nada errado com ele - é o jeito óbvio de escrever:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">col_name</span> <span class="o">=</span> <span class="n">sqlite3_column_name</span><span class="p">(</span><span class="n">stmt</span><span class="p">,</span> <span class="n">column</span><span class="p">);</span>
  <span class="c1">// ...</span>
  <span class="k">return</span> <span class="n">String</span><span class="o">::</span><span class="n">NewFromUtf8</span><span class="p">(</span>
             <span class="n">env</span><span class="o">-&gt;</span><span class="n">isolate</span><span class="p">(),</span> <span class="n">col_name</span><span class="p">,</span> <span class="n">NewStringType</span><span class="o">::</span><span class="n">kInternalized</span><span class="p">)</span>
      <span class="p">.</span><span class="n">As</span><span class="o">&lt;</span><span class="n">Name</span><span class="o">&gt;</span><span class="p">();</span>
</code></pre></div></div>

<p>Ninguém lendo essa função pensa “gargalo”, porque isolada ela não é. O profiler é
o que põe ela lado a lado com o <code class="language-plaintext highlighter-rouge">sqlite3VdbeExec</code> e mostra o preço: montar as chaves
custa um pouco menos da metade do que custa rodar a query inteira.</p>

<h2 id="5-faça-profile-de-mais-de-uma-forma-de-carga">5. Faça profile de mais de uma forma de carga</h2>

<p>Mesmo binário, mesmo código, perfilando <code class="language-plaintext highlighter-rouge">all()</code> com <code class="language-plaintext highlighter-rouge">LIMIT 100</code> em vez de uma
linha. O <code class="language-plaintext highlighter-rouge">column-name interning</code> <strong>desapareceu</strong> - não diminuiu, desapareceu.
Todo símbolo daquela categoria caiu abaixo do corte de 5 amostras do <code class="language-plaintext highlighter-rouge">sample</code>:</p>

<table>
  <thead>
    <tr>
      <th>símbolo</th>
      <th><code class="language-plaintext highlighter-rouge">get()</code> LIMIT 1</th>
      <th><code class="language-plaintext highlighter-rouge">all()</code> LIMIT 100</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">StringTable::LookupKey</code></td>
      <td>150</td>
      <td>-</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Utf8DecoderBase</code></td>
      <td>126</td>
      <td>-</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">StringHasher::HashSequentialString</code></td>
      <td>102</td>
      <td>-</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">columnName</code></td>
      <td>88</td>
      <td>-</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">String::NewFromUtf8</code></td>
      <td>65</td>
      <td>-</td>
    </tr>
  </tbody>
</table>

<p>Nada mudou no código. O custo é idêntico <em>por chamada</em> - o <code class="language-plaintext highlighter-rouge">all()</code> monta as
chaves uma vez e reusa pras 100 linhas, então o que resta desaparece debaixo de
todo o resto. Enquanto isso o mutex quase não se mexeu, 20,4% pra 19,0%, porque
esse é pago por <em>valor</em>.</p>

<p>É custo de frete. Pede um livro na internet e o frete é metade do que você paga.
Pede cem e ele é erro de arredondamento na nota - frete idêntico, nas duas vezes.
Se você só olhar a nota dos cem livros, vai concluir que frete é grátis.</p>

<p>Ou seja: um custo pago <em>uma vez por chamada</em> desaparece quando você perfila
muitas linhas, e um custo pago <em>uma vez por linha</em> é invisível quando você
perfila uma. A carga mais pesada é a natural pra perfilar, e é a que eu teria
escolhido se fosse escolher uma. <strong>Faça uma das suas cargas ser pequena.</strong></p>

<h2 id="6-leia-o-fonte-do-que-você-está-mudando">6. Leia o fonte do que você está mudando</h2>

<p>Então, de volta ao palpite. <strong>O cache já estava no arquivo</strong>, no <code class="language-plaintext highlighter-rouge">StatementSync</code>,
chaveado pelo contador de re-prepare do SQLite pra invalidar corretamente quando
uma mudança de schema força um re-prepare silencioso:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">v8</span><span class="o">::</span><span class="n">Global</span><span class="o">&lt;</span><span class="n">v8</span><span class="o">::</span><span class="n">Name</span><span class="o">&gt;&gt;</span> <span class="n">cached_column_names_</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">cached_column_names_reprepare_count_</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
</code></pre></div></div>

<p>Esse contador é a parte que importa. É o motivo de mandar mais dois callers pro
cache ser seguro em vez de esperto - a invalidação já estava escrita e já estava
rodando em produção sob o <code class="language-plaintext highlighter-rouge">iterate()</code>.</p>

<p>Daí a mudança não escreve cache nenhum. Ela apaga dois loops:</p>

<div class="code-compare">
  <div class="code-compare-side">

    <p>Antes, no <code class="language-plaintext highlighter-rouge">get()</code> e no <code class="language-plaintext highlighter-rouge">all()</code></p>

    <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">row_keys</span><span class="p">.</span><span class="n">reserve</span><span class="p">(</span><span class="n">num_cols</span><span class="p">);</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">num_cols</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">Local</span><span class="o">&lt;</span><span class="n">Name</span><span class="o">&gt;</span> <span class="n">key</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">ColumnNameToName</span><span class="p">(</span><span class="n">env</span><span class="p">,</span> <span class="n">stmt</span><span class="p">,</span> <span class="n">i</span><span class="p">)</span>
           <span class="p">.</span><span class="n">ToLocal</span><span class="p">(</span><span class="o">&amp;</span><span class="n">key</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">MaybeLocal</span><span class="o">&lt;</span><span class="n">Value</span><span class="o">&gt;</span><span class="p">();</span>
  <span class="p">}</span>
  <span class="n">row_keys</span><span class="p">.</span><span class="n">emplace_back</span><span class="p">(</span><span class="n">key</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>    </div>

  </div>
  <div class="code-compare-side">

    <p>Depois, o mesmo que o <code class="language-plaintext highlighter-rouge">iterate()</code> faz</p>

    <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">statement</span><span class="o">-&gt;</span><span class="n">GetCachedColumnNames</span><span class="p">(</span>
        <span class="o">&amp;</span><span class="n">row_keys</span><span class="p">))</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">MaybeLocal</span><span class="o">&lt;</span><span class="n">Value</span><span class="o">&gt;</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div>    </div>

  </div>
</div>

<p>Nove linhas viram quatro, em dois lugares. O patch inteiro é 31 inserções contra
66 remoções: ele tira linhas do arquivo e deixa as leituras mais rápidas.</p>

<p>E nenhum profiler ia me entregar isso. Ele apontou a função; ler o arquivo é o
que transformou “isso é caro” em “isso é caro <strong>e evitável, com código que já
está aqui e em que já se confia</strong>”.</p>

<h2 id="7-use-o-benchmark-pra-condenar">7. Use o benchmark pra condenar</h2>

<p>Recompila, e mede contra o binário da baseline do passo 1:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>improvement  p-value    old rate    new rate   config
  -0.97%     0.3266      26.7k      26.4k  select-all 'SELECT * FROM foo LIMIT 100'
+   0.92%     0.1861      14.8k      15.0k  select-all 'SELECT text_8kb_column FROM foo_large LIMIT 100'
+   2.57% *   0.0245      70.0k      71.8k  select-all 'SELECT text_column FROM foo LIMIT 100'
+   8.62% *** 0.0005      3.22M      3.50M  select-get 'SELECT 1'
+  12.88% *** 0.0000      1.40M      1.58M  select-all 'SELECT * FROM foo LIMIT 1'
+  15.51% *** 0.0000      1.48M      1.71M  select-get 'SELECT text,int,real,blob FROM foo LIMIT 1'
+  17.03% *** 0.0000      1.48M      1.73M  select-get 'SELECT * FROM foo LIMIT 1'
+  17.71% *** 0.0000      1.38M      1.63M  select-all 'SELECT text,int,real,blob FROM foo LIMIT 1'

configs: 20  significant: 14
geomean speedup (all configs): 8.25%
</code></pre></div></div>

<p>Ordenada, a lista parte em duas: tudo com três estrelas é <code class="language-plaintext highlighter-rouge">LIMIT 1</code>, tudo perto
de zero é <code class="language-plaintext highlighter-rouge">LIMIT 100</code>. E o <code class="language-plaintext highlighter-rouge">select-all</code> aparece nos dois grupos, então a divisão
não é <code class="language-plaintext highlighter-rouge">get()</code> contra <code class="language-plaintext highlighter-rouge">all()</code> - é <strong>quantas linhas voltam por chamada</strong>.</p>

<p>Esse é o profile do passo 5, confirmado por medição. Que é o motivo de verdade
pra rodar os dois: o benchmark não disse só “mais rápido”, ele disse mais rápido
<em>na forma exata que o mecanismo prevê</em>. Quando os dois concordam, você entende a
sua própria mudança.</p>

<p>A linha de <code class="language-plaintext highlighter-rouge">-0,97%</code> não é regressão, aliás. Sem estrela, p-value 0,33, <code class="language-plaintext highlighter-rouge">noise
floor</code> de ±2%. É zero, e o passo 2 é o que me deixa afirmar isso sem discussão.</p>

<blockquote>
  <p>Uma ideia morreu aqui, e era a que eu mais gostava. A maior categoria no
profile de 100 linhas é construção de objeto na V8, com 22,5%, e o
<code class="language-plaintext highlighter-rouge">DictionaryTemplate</code> existe pra resolver isso - descreve a forma da linha uma
vez, compartilha um mapa entre as instâncias. As leituras ficaram 23-29% mais
rápidas e a construção ficou 11-20% <em>mais lenta</em>, porque as linhas do
<code class="language-plaintext highlighter-rouge">node:sqlite</code> têm protótipo nulo, o <code class="language-plaintext highlighter-rouge">DictionaryTemplate</code> te entrega
<code class="language-plaintext highlighter-rouge">Object.prototype</code>, e o <code class="language-plaintext highlighter-rouge">SetPrototypeV2</code> por linha custa mais do que o mapa
compartilhado economiza. Descartada. Hipótese morrendo numa medição é o método
funcionando.</p>
</blockquote>

<h2 id="o-método-inteiro">O método inteiro</h2>

<ol>
  <li><strong>Conserte a baseline</strong> e copie o binário pra fora, pra não comparar um build contra ele mesmo.</li>
  <li><strong>Ache o <code class="language-plaintext highlighter-rouge">noise floor</code></strong> rodando o benchmark da baseline contra ela mesma. O meu foi ±2%. Sem isso, todo número aqui é opinião.</li>
  <li><strong>Faça profile antes de mexer em qualquer coisa.</strong> Agrupe por custo, descarte as threads ociosas, e use a árvore de chamadas pra descobrir de quem é o trabalho do símbolo quente.</li>
  <li><strong>Perfile mais de uma forma de carga</strong>, e faça uma delas pequena. Os 8,1% que eu consertei eram invisíveis no profile de 100 linhas.</li>
  <li><strong>Leia o fonte.</strong> O cache que eu “adicionei” já existia.</li>
  <li><strong>Use o benchmark pra condenar</strong>, e confira a <em>forma</em> do ganho contra o mecanismo que você alegou.</li>
</ol>

<p>A ordem importa: cada passo está ali pra manter o próximo honesto. E o passo 3 é
o que eu entregaria pra quem está começando: eu podia ter lido o <code class="language-plaintext highlighter-rouge">node_sqlite.cc</code>
por uma semana sem desconfiar daquelas quatro strings, e rodado a suíte de
benchmark cem vezes sem ela dizer uma palavra sobre elas.</p>

<p>Por hoje é só.</p>]]></content><author><name></name></author><category term="programacao" /><category term="nodejs" /><category term="sqlite" /><category term="performance" /><category term="profiling" /><category term="benchmarking" /><category term="node core" /><category term="cpp" /><summary type="html"><![CDATA[Um patch no Node.js deixou leituras de uma linha no node:sqlite 17% mais rápidas apagando código. O método que achou: noise floor, profiler, benchmark.]]></summary></entry><entry xml:lang="en-US"><title type="html">Making node:sqlite faster, step by step</title><link href="https://codesilva.com/programacao/2026/08/21/making-node-sqlite-faster-step-by-step.html" rel="alternate" type="text/html" title="Making node:sqlite faster, step by step" /><published>2026-08-21T00:00:00+00:00</published><updated>2026-08-21T00:00:00+00:00</updated><id>https://codesilva.com/programacao/2026/08/21/making-node-sqlite-faster-step-by-step</id><content type="html" xml:base="https://codesilva.com/programacao/2026/08/21/making-node-sqlite-faster-step-by-step.html"><![CDATA[<p>Reading one row out of SQLite in Node got about <strong>17% faster</strong>. A loop that used
to pull 1.48 million rows a second now pulls 1.73 million, same machine, same
query. Across the twenty read benchmarks Node ships, most got faster and none got
slower.</p>

<p>The patch that did it takes <strong>35 lines out</strong> of the file. It rewrites nothing and
adds nothing: the fast path was already sitting in there, used by exactly one of
the three functions that wanted it. It
<a href="https://github.com/nodejs/node/pull/65276">landed in Node.js core</a> last week. So
the part worth a post isn’t what the change was - it’s how something that cheap
sat in plain sight in a file I’d been living in for months.</p>

<p>Reading the code is not what found it. Measuring is. Nate Berkopec puts that
whole discipline in one sentence in <em>The Complete Guide to Rails Performance</em>,
and it’s the only part of that course I’d call mandatory:</p>

<blockquote>
  <p><strong>Repeat after me: I will not optimize anything in my application until my
metrics tell me so.</strong></p>
</blockquote>

<p>Brendan Gregg names the ways it goes wrong in <em>Systems Performance</em>: the <strong>Street
Light Anti-Method</strong> is investigating with whatever tool you already know, and the
<strong>Random Change Anti-Method</strong> is changing things until a number moves. Both
produce activity. Neither produces knowledge.</p>

<p>So here’s the alternative, in the order I actually ran it - and the punchline
lands at step 4, where it turned out that a tenth of every single-row read was
going into building the <em>names</em> of the columns. Four strings that never change,
rebuilt from scratch every time.</p>

<h2 id="1-make-the-baseline-honest">1. Make the baseline honest</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git rev-list --left-right --count main...upstream/main
0	91
</code></pre></div></div>

<p>Ninety-one commits behind. Merge, rebuild, and then the line that matters more
than it looks:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ make -j10
$ cp out/Release/node /tmp/node-main
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">make</code> leaves a <code class="language-plaintext highlighter-rouge">./node</code> symlink pointing at <code class="language-plaintext highlighter-rouge">out/Release/node</code>. Point a
benchmark at <code class="language-plaintext highlighter-rouge">./node</code> and it silently follows whatever you built most recently,
so you end up carefully comparing a binary against itself and calling the result
a win. <strong>Copy the real binary out before you touch anything.</strong></p>

<h2 id="2-find-the-noise-floor">2. Find the noise floor</h2>

<p>Now benchmark the baseline <strong>against itself</strong> - identical binary on both sides,
so every difference that comes back is measurement noise. Whatever spread you get
is your detection threshold, and any later “win” smaller than it is
unfalsifiable.</p>

<p>Same reason you step on the bathroom scale twice before believing you lost 200
grams.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./out/Release/node benchmark/compare.js \
    --old /tmp/node-main --new /tmp/node-main \
    --runs 10 --no-progress \
    --set n=20000 --set tableSeedSize=10000 \
    --filter sqlite-prepare-select-get.js \
    --filter sqlite-prepare-select-all.js sqlite &gt; noise-floor.csv
</code></pre></div></div>

<p>Node ships <code class="language-plaintext highlighter-rouge">benchmark/sqlite/</code> already, which is worth using: it’s the yardstick
a maintainer will judge your PR by, and it removes any argument about whether
your harness was fair.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>improvement  p-value    old rate    new rate   config
  -1.52%     0.1832      2.31M      2.28M  select-all 'SELECT text_column, integer_column FROM foo LIMIT 1'
  -0.31%     0.8124      1.52M      1.52M  select-get 'SELECT * FROM foo LIMIT 1'
+   1.30%     0.3721      1.49M      1.51M  select-get 'SELECT text,int,real,blob FROM foo LIMIT 1'
+   1.96%     0.2159      3.30M      3.37M  select-get 'SELECT 1'

configs: 20  significant: 0
geomean speedup (all configs): 0.38%
</code></pre></div></div>

<p>Three things in that output, if it’s new to you, because the rest of the post
leans on them. The <strong>p-value</strong> is the odds of seeing a gap that
big if the change did nothing at all - so a high one, like the 0.81 up there,
means “this could easily be nothing.” A result gets called <strong>significant</strong> when
that probability is small enough to bet against, and later in the post you’ll see
significant rows marked with stars. And <strong>geomean</strong> is the summary line: an
average across all twenty configurations, built so that one spectacular number
can’t carry the total on its own.</p>

<p>Here, zero results are significant, which is what identical binaries should
produce. The whole spread, -1.52% to +1.96%, is the machine talking to
itself.</p>

<p><strong>So: 2%.</strong> Anything under that, I’m not allowed to call a win for the rest of
this post.</p>

<blockquote>
  <p>“The first principle is that you must not fool yourself - and you are the
easiest person to fool.” Feynman was talking about physics, but the noise floor
is that sentence turned into a shell command.</p>
</blockquote>

<h2 id="3-benchmarking-and-profiling-answer-different-questions">3. Benchmarking and profiling answer different questions</h2>

<p>Berkopec tells the best version of this. He had benchmarked a change, found
<code class="language-plaintext highlighter-rouge">shuffle</code> 12x faster than <code class="language-plaintext highlighter-rouge">sort_by { rand }</code>, and took the number to Ryan Davis,
the author of <code class="language-plaintext highlighter-rouge">minitest</code>. The reply:</p>

<blockquote>
  <p>“you benchmarked it, but did you profile it?”</p>
</blockquote>

<p>A benchmark gives you one number per configuration. It will happily tell you your
change made things 3% faster and be completely wrong, because 3% is inside the
noise of the machine you ran it on. And it has nothing to say about <em>why</em>
anything is slow: run Node’s whole sqlite suite on clean <code class="language-plaintext highlighter-rouge">main</code> and you get
twenty rates and zero suspects.</p>

<p>A profiler gives you attribution. What it will not give you is whether fixing
that attribution is worth anything, because a profile has no control group.</p>

<p><strong>Profile to find a suspect, benchmark to convict it.</strong> In that order.</p>

<h2 id="4-profile-then-read-the-call-tree">4. Profile, then read the call tree</h2>

<p><code class="language-plaintext highlighter-rouge">sample</code> is built into macOS. Two things bite everyone the first time: it
<strong>attaches</strong> to a running process, it never launches one, and it matches partial
names, so <code class="language-plaintext highlighter-rouge">sample node</code> with a language server running may profile something else
entirely. Use the PID. And run <code class="language-plaintext highlighter-rouge">dsymutil out/Release/node</code> first, or you get bare
addresses instead of symbols.</p>

<p>Don’t profile the benchmark harness either - it mixes seeding, warmup and
measurement in one process. Write a workload that reaches a steady state and
holds it:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">stmt</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">prepare</span><span class="p">(</span><span class="s2">`SELECT </span><span class="p">${</span><span class="nx">cols</span><span class="p">}</span><span class="s2"> FROM foo LIMIT </span><span class="p">${</span><span class="nx">limit</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">run</span> <span class="o">=</span> <span class="nx">op</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">all</span><span class="dl">'</span> <span class="p">?</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">stmt</span><span class="p">.</span><span class="nf">all</span><span class="p">()</span> <span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">stmt</span><span class="p">.</span><span class="nf">get</span><span class="p">();</span>

<span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">20000</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="nf">run</span><span class="p">();</span>          <span class="c1">// warm up</span>
<span class="nx">process</span><span class="p">.</span><span class="nx">stderr</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="s2">`READY pid=</span><span class="p">${</span><span class="nx">process</span><span class="p">.</span><span class="nx">pid</span><span class="p">}</span><span class="s2">\n`</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">deadline</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">+</span> <span class="nx">seconds</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">;</span>   <span class="c1">// steady state</span>
<span class="k">while </span><span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">&lt;</span> <span class="nx">deadline</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">1000</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="nx">sink</span> <span class="o">=</span> <span class="nf">run</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Two traps in reading what comes back. The biggest symbols on the page are
<code class="language-plaintext highlighter-rouge">__psynch_cvwait</code>, <code class="language-plaintext highlighter-rouge">kevent</code> and <code class="language-plaintext highlighter-rouge">semaphore_wait_trap</code> - idle libuv threadpool
threads parked in the kernel. They mean nothing. And a single cost gets spread
across several symbol names, so group by category before comparing magnitudes.</p>

<p>Here’s <code class="language-plaintext highlighter-rouge">get()</code> on a four-column row, as a share of non-idle samples:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  1865   20.4%  pthread mutex
  1732   18.9%  sqlite VDBE + btree (real query work)
  1382   15.1%  V8 object construction (dictionary-mode rows)
  1067   11.7%  malloc/free
   739    8.1%  column-name interning (V8 strings)
   542    5.9%  V8 buffers (BLOB -&gt; Uint8Array)
   411    4.5%  sqlite C API entry points
   159    1.7%  node:sqlite binding
</code></pre></div></div>

<p>The line I want is the fifth: <strong>column-name interning, 8.1%.</strong></p>

<p>I had a suspicion about that one already. <code class="language-plaintext highlighter-rouge">iterate()</code> had been handing out cached
column names for a while, and I’d wondered more than once whether <code class="language-plaintext highlighter-rouge">get()</code> and
<code class="language-plaintext highlighter-rouge">all()</code> could pull from the same place. What I didn’t have was a reason to touch
it. A hunch isn’t a number, and Berkopec’s rule is that a hunch doesn’t get to
authorize a patch. I have a folder full of hunches about that file and most of
them are worth nothing. <strong>8.1% is what got this one out of the folder.</strong></p>

<blockquote>
  <p>The top line is a bigger number and it is not this post. That’s SQLite’s
per-connection mutex, it’s a compile-time flag away, and taking it means
writing the missing lock yourself. Different investigation, different PR, and I
don’t know yet if that one lands - which is why it doesn’t share a post with
a patch that already did.</p>
</blockquote>

<p>Now, 8.1% of samples in symbols like <code class="language-plaintext highlighter-rouge">StringTable::LookupKey</code> doesn’t say <em>whose</em>
strings those are, and V8 interns strings for a dozen reasons. That’s the call
tree’s job:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>141 node::sqlite::StatementExecutionHelper::Get(...) + 396
  | 121 v8::String::NewFromUtf8(...)
  | : 88 v8::internal::Factory::InternalizeUtf8String(...)
  | : | 54 v8::internal::FactoryBase&lt;...&gt;::InternalizeString(...)
  | : | + 36 v8::internal::StringTable::LookupKey&lt;...&gt;(...)
  | : | 23 v8::internal::FactoryBase&lt;...&gt;::InternalizeString(...)
  | : | + 23 v8::internal::StringHasher::HashSequentialString&lt;...&gt;(...)
  | : 30 v8::internal::Factory::InternalizeUtf8String(...)
  | : | 30 v8::internal::Utf8DecoderBase&lt;...&gt;::Utf8DecoderBase(...)
 78 node::sqlite::StatementExecutionHelper::Get(...) + 372
  | 29 columnName  (in node)
  | : 13 _pthread_mutex_lock_init_slow  (in libsystem_pthread.dylib)
  | 21 columnName  (in node)
  | 18 columnName  (in node)
</code></pre></div></div>

<p>Two adjacent instruction offsets inside the same function, and they’re the two
halves of one operation. <code class="language-plaintext highlighter-rouge">+372</code> calls <code class="language-plaintext highlighter-rouge">sqlite3_column_name()</code>, whose
implementation in <code class="language-plaintext highlighter-rouge">sqlite3.c</code> is <code class="language-plaintext highlighter-rouge">columnName</code>. <code class="language-plaintext highlighter-rouge">+396</code> calls <code class="language-plaintext highlighter-rouge">String::NewFromUtf8</code>
with <code class="language-plaintext highlighter-rouge">kInternalized</code>, and V8 does the honest work: decode the UTF-8, hash it,
look it up in the string table.</p>

<p>All of that <strong>per column, per call, for a prepared statement whose column names
cannot change.</strong></p>

<p>Here it is in the source, and there’s nothing wrong with it - it’s the obvious
way to write it:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">col_name</span> <span class="o">=</span> <span class="n">sqlite3_column_name</span><span class="p">(</span><span class="n">stmt</span><span class="p">,</span> <span class="n">column</span><span class="p">);</span>
  <span class="c1">// ...</span>
  <span class="k">return</span> <span class="n">String</span><span class="o">::</span><span class="n">NewFromUtf8</span><span class="p">(</span>
             <span class="n">env</span><span class="o">-&gt;</span><span class="n">isolate</span><span class="p">(),</span> <span class="n">col_name</span><span class="p">,</span> <span class="n">NewStringType</span><span class="o">::</span><span class="n">kInternalized</span><span class="p">)</span>
      <span class="p">.</span><span class="n">As</span><span class="o">&lt;</span><span class="n">Name</span><span class="o">&gt;</span><span class="p">();</span>
</code></pre></div></div>

<p>Nobody reading that function thinks “bottleneck”, because in isolation it isn’t.
The profiler is what puts it side by side with <code class="language-plaintext highlighter-rouge">sqlite3VdbeExec</code> and prices it:
building the keys costs a bit under half of what running the whole query costs.</p>

<h2 id="5-profile-more-than-one-workload-shape">5. Profile more than one workload shape</h2>

<p>Same binary, same code, profiling <code class="language-plaintext highlighter-rouge">all()</code> with <code class="language-plaintext highlighter-rouge">LIMIT 100</code> instead of one row.
Column-name interning is <strong>gone</strong> - not smaller, gone. Every symbol in the
category fell below <code class="language-plaintext highlighter-rouge">sample</code>’s 5-sample cutoff:</p>

<table>
  <thead>
    <tr>
      <th>symbol</th>
      <th><code class="language-plaintext highlighter-rouge">get()</code> LIMIT 1</th>
      <th><code class="language-plaintext highlighter-rouge">all()</code> LIMIT 100</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">StringTable::LookupKey</code></td>
      <td>150</td>
      <td>-</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Utf8DecoderBase</code></td>
      <td>126</td>
      <td>-</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">StringHasher::HashSequentialString</code></td>
      <td>102</td>
      <td>-</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">columnName</code></td>
      <td>88</td>
      <td>-</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">String::NewFromUtf8</code></td>
      <td>65</td>
      <td>-</td>
    </tr>
  </tbody>
</table>

<p>Nothing changed in the code. The cost is identical <em>per call</em> - <code class="language-plaintext highlighter-rouge">all()</code> builds
the keys once and reuses them for 100 rows, so what’s left disappears under
everything else. Meanwhile the mutex barely moved, 20.4% to 19.0%, because that
one is paid per <em>value</em>.</p>

<p>It’s shipping cost. Order one book online and the postage is half of what you
pay. Order a hundred and it’s a rounding error on the invoice - identical
postage, both times. If you only ever look at the hundred-book invoice, you
conclude that shipping is free.</p>

<p>So: a cost paid <em>once per call</em> vanishes when you profile many rows, and a cost
paid <em>once per row</em> is invisible when you profile one. The heaviest workload is
the natural one to profile, and it’s the one I’d have picked if I were picking
one. <strong>Make one of your shapes small.</strong></p>

<h2 id="6-read-the-source-of-the-thing-youre-changing">6. Read the source of the thing you’re changing</h2>

<p>So, back to the suspicion. <strong>The cache was already in the file</strong>, sitting on
<code class="language-plaintext highlighter-rouge">StatementSync</code>, keyed on SQLite’s re-prepare counter so it invalidates correctly
when a schema change forces a silent re-prepare:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">v8</span><span class="o">::</span><span class="n">Global</span><span class="o">&lt;</span><span class="n">v8</span><span class="o">::</span><span class="n">Name</span><span class="o">&gt;&gt;</span> <span class="n">cached_column_names_</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">cached_column_names_reprepare_count_</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
</code></pre></div></div>

<p>That counter is the part that matters. It’s the reason routing two more callers
into the cache is safe rather than clever - the invalidation was already written and
already shipping under <code class="language-plaintext highlighter-rouge">iterate()</code>.</p>

<p>So the change writes no cache. It deletes two loops:</p>

<div class="code-compare">
  <div class="code-compare-side">

    <p>Before, in both <code class="language-plaintext highlighter-rouge">get()</code> and <code class="language-plaintext highlighter-rouge">all()</code></p>

    <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">row_keys</span><span class="p">.</span><span class="n">reserve</span><span class="p">(</span><span class="n">num_cols</span><span class="p">);</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">num_cols</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">Local</span><span class="o">&lt;</span><span class="n">Name</span><span class="o">&gt;</span> <span class="n">key</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">ColumnNameToName</span><span class="p">(</span><span class="n">env</span><span class="p">,</span> <span class="n">stmt</span><span class="p">,</span> <span class="n">i</span><span class="p">)</span>
           <span class="p">.</span><span class="n">ToLocal</span><span class="p">(</span><span class="o">&amp;</span><span class="n">key</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">MaybeLocal</span><span class="o">&lt;</span><span class="n">Value</span><span class="o">&gt;</span><span class="p">();</span>
  <span class="p">}</span>
  <span class="n">row_keys</span><span class="p">.</span><span class="n">emplace_back</span><span class="p">(</span><span class="n">key</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>    </div>

  </div>
  <div class="code-compare-side">

    <p>After, the same thing <code class="language-plaintext highlighter-rouge">iterate()</code> does</p>

    <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">statement</span><span class="o">-&gt;</span><span class="n">GetCachedColumnNames</span><span class="p">(</span>
        <span class="o">&amp;</span><span class="n">row_keys</span><span class="p">))</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">MaybeLocal</span><span class="o">&lt;</span><span class="n">Value</span><span class="o">&gt;</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div>    </div>

  </div>
</div>

<p>Nine lines become four, in two places. The whole patch is 31 insertions against
66 deletions: it takes lines out of the file and makes reads faster.</p>

<p>And no profiler was going to hand me that. It pointed at the function; reading
the file is what turned “this is expensive” into “this is expensive <strong>and
avoidable, with code that’s already here and already trusted</strong>.”</p>

<h2 id="7-benchmark-to-convict">7. Benchmark to convict</h2>

<p>Rebuild, and measure against the baseline binary from step 1:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>improvement  p-value    old rate    new rate   config
  -0.97%     0.3266      26.7k      26.4k  select-all 'SELECT * FROM foo LIMIT 100'
+   0.92%     0.1861      14.8k      15.0k  select-all 'SELECT text_8kb_column FROM foo_large LIMIT 100'
+   2.57% *   0.0245      70.0k      71.8k  select-all 'SELECT text_column FROM foo LIMIT 100'
+   8.62% *** 0.0005      3.22M      3.50M  select-get 'SELECT 1'
+  12.88% *** 0.0000      1.40M      1.58M  select-all 'SELECT * FROM foo LIMIT 1'
+  15.51% *** 0.0000      1.48M      1.71M  select-get 'SELECT text,int,real,blob FROM foo LIMIT 1'
+  17.03% *** 0.0000      1.48M      1.73M  select-get 'SELECT * FROM foo LIMIT 1'
+  17.71% *** 0.0000      1.38M      1.63M  select-all 'SELECT text,int,real,blob FROM foo LIMIT 1'

configs: 20  significant: 14
geomean speedup (all configs): 8.25%
</code></pre></div></div>

<p>Sorted, it splits in two: everything with three stars is <code class="language-plaintext highlighter-rouge">LIMIT 1</code>, everything
near zero is <code class="language-plaintext highlighter-rouge">LIMIT 100</code>. And <code class="language-plaintext highlighter-rouge">select-all</code> is in both groups, so the divide isn’t
<code class="language-plaintext highlighter-rouge">get()</code> versus <code class="language-plaintext highlighter-rouge">all()</code> - it’s <strong>how many rows come back per call</strong>.</p>

<p>That’s the profile from step 5, confirmed by measurement. Which is the real
reason to run both: the benchmark didn’t just say “faster”, it said faster <em>in
the exact shape the mechanism predicts</em>. When the two agree, you understand your
own change.</p>

<p>The <code class="language-plaintext highlighter-rouge">-0.97%</code> row is not a regression, by the way. No stars, p-value 0.33, noise
floor ±2%. It’s zero, and step 2 is what lets me say so without arguing.</p>

<blockquote>
  <p>One idea died here, and it was the one I liked most. The biggest category in
the 100-row profile is V8 object construction at 22.5%, and <code class="language-plaintext highlighter-rouge">DictionaryTemplate</code>
exists to fix that - describe the row shape once, share a map across
instances. Reads got 23-29% faster and construction got 11-20% <em>slower</em>,
because <code class="language-plaintext highlighter-rouge">node:sqlite</code> rows have a null prototype, <code class="language-plaintext highlighter-rouge">DictionaryTemplate</code> hands
you <code class="language-plaintext highlighter-rouge">Object.prototype</code>, and the <code class="language-plaintext highlighter-rouge">SetPrototypeV2</code> per row costs more than the
shared map saves. Discarded. A hypothesis dying on a measurement is the method
working.</p>
</blockquote>

<h2 id="the-whole-method">The whole method</h2>

<ol>
  <li><strong>Fix the baseline</strong> and copy the binary out, so you’re not comparing a build against itself.</li>
  <li><strong>Find the noise floor</strong> by benchmarking the baseline against itself. Mine was ±2%. Without it, every number here is an opinion.</li>
  <li><strong>Profile before touching anything.</strong> Group by cost, discard the idle threads, and use the call tree to find out whose work the hot symbol is.</li>
  <li><strong>Profile more than one shape</strong>, and make one small. The 8.1% I fixed was invisible in the 100-row profile.</li>
  <li><strong>Read the source.</strong> The cache I “added” already existed.</li>
  <li><strong>Benchmark to convict</strong>, and check the <em>shape</em> of the win against the mechanism you claimed.</li>
</ol>

<p>The order matters: each step is there to keep the next one honest.
And step 3 is the one I’d hand to anyone starting out: I could have read
<code class="language-plaintext highlighter-rouge">node_sqlite.cc</code> for a week without suspecting those four strings, and run the
benchmark suite a hundred times without it saying a word about them.</p>

<p>Thanks for reading!</p>]]></content><author><name></name></author><category term="programacao" /><category term="nodejs" /><category term="sqlite" /><category term="performance" /><category term="profiling" /><category term="benchmarking" /><category term="node core" /><category term="cpp" /><summary type="html"><![CDATA[A merged Node.js patch made single-row node:sqlite reads 17% faster by deleting code. The method that found it: noise floor, profiler, call tree, benchmark.]]></summary></entry><entry xml:lang="pt-BR"><title type="html">Detectando full table scans do SQLite no Node.js</title><link href="https://codesilva.com/programacao/2026/08/13/detectando-full-table-scans-do-sqlite-no-nodejs.html" rel="alternate" type="text/html" title="Detectando full table scans do SQLite no Node.js" /><published>2026-08-13T00:00:00+00:00</published><updated>2026-08-13T00:00:00+00:00</updated><id>https://codesilva.com/programacao/2026/08/13/detectando-full-table-scans-do-sqlite-no-nodejs</id><content type="html" xml:base="https://codesilva.com/programacao/2026/08/13/detectando-full-table-scans-do-sqlite-no-nodejs.html"><![CDATA[<p>Em julho o Aaron Patterson <a href="https://tenderlovemaking.com/2026/07/15/detecting-full-table-scans-with-sqlite/">escreveu sobre detectar full table scans com SQLite</a>. O pulo do gato é que você não precisa de <code class="language-plaintext highlighter-rouge">EXPLAIN QUERY PLAN</code> pra isso. O SQLite já mantém um contador por statement de quantas linhas ele percorreu durante um scan, e dá pra ler esse número depois que a query roda. Se for maior que zero, aquele statement escaneou.</p>

<p>Um dia depois, o Kevin Gibbons abriu <a href="https://github.com/nodejs/node/issues/64540">uma issue no <code class="language-plaintext highlighter-rouge">nodejs/node</code></a> pedindo a mesma coisa no <code class="language-plaintext highlighter-rouge">node:sqlite</code>, citando o post. Não tinha como chegar no <code class="language-plaintext highlighter-rouge">sqlite3_stmt_status()</code> a partir do JavaScript. Eu peguei a issue, e <a href="https://github.com/nodejs/node/pull/64541">entrou hoje</a>. Dois métodos no <code class="language-plaintext highlighter-rouge">StatementSync</code>:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">statement</span><span class="p">.</span><span class="nf">stat</span><span class="p">(</span><span class="nx">counter</span><span class="p">)</span>   <span class="c1">// lê um contador</span>
<span class="nx">statement</span><span class="p">.</span><span class="nf">resetStats</span><span class="p">()</span>    <span class="c1">// zera todos</span>
</code></pre></div></div>

<h2 id="o-teste-de-scan">O teste de scan</h2>

<p>Mesmo formato do exemplo em Ruby do Aaron. Mil usuários, uma query numa coluna sem índice:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">DatabaseSync</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:sqlite</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">db</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DatabaseSync</span><span class="p">(</span><span class="dl">'</span><span class="s1">:memory:</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">db</span><span class="p">.</span><span class="nf">exec</span><span class="p">(</span><span class="dl">'</span><span class="s1">CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)</span><span class="dl">'</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">insert</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">prepare</span><span class="p">(</span><span class="dl">'</span><span class="s1">INSERT INTO users (name, age) VALUES (?, ?)</span><span class="dl">'</span><span class="p">);</span>
<span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">1000</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">insert</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span><span class="s2">`user-</span><span class="p">${</span><span class="nx">i</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="nx">i</span> <span class="o">%</span> <span class="mi">80</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">stmt</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">prepare</span><span class="p">(</span><span class="dl">'</span><span class="s1">SELECT * FROM users WHERE age = ?</span><span class="dl">'</span><span class="p">);</span>

<span class="kd">function</span> <span class="nf">query</span><span class="p">(</span><span class="nx">age</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">rows</span> <span class="o">=</span> <span class="nx">stmt</span><span class="p">.</span><span class="nf">all</span><span class="p">(</span><span class="nx">age</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">fullscanStep:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">stmt</span><span class="p">.</span><span class="nf">stat</span><span class="p">(</span><span class="dl">'</span><span class="s1">fullscanStep</span><span class="dl">'</span><span class="p">));</span>
  <span class="nx">stmt</span><span class="p">.</span><span class="nf">resetStats</span><span class="p">();</span>
  <span class="k">return</span> <span class="nx">rows</span><span class="p">;</span>
<span class="p">}</span>

<span class="nf">query</span><span class="p">(</span><span class="mi">30</span><span class="p">);</span>
<span class="nx">db</span><span class="p">.</span><span class="nf">exec</span><span class="p">(</span><span class="dl">'</span><span class="s1">CREATE INDEX users_age_idx ON users (age)</span><span class="dl">'</span><span class="p">);</span>
<span class="nf">query</span><span class="p">(</span><span class="mi">30</span><span class="p">);</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fullscanStep: 999
fullscanStep: 0
</code></pre></div></div>

<p>999 antes do índice, 0 depois. O <code class="language-plaintext highlighter-rouge">vmStep</code> também cai, de 3059 pra 101, com exatamente o mesmo result set.</p>

<p>Repare na chamada de <code class="language-plaintext highlighter-rouge">resetStats()</code>. Os contadores são cumulativos pelo tempo de vida do prepared statement. Ou seja, se você reusa o statement num loop de requisições - que é justamente o motivo de preparar ele - precisa zerar entre as medições, senão está lendo um total acumulado.</p>

<h2 id="os-contadores">Os contadores</h2>

<p>O <code class="language-plaintext highlighter-rouge">stat()</code> recebe um nome e devolve um número:</p>

<table>
  <thead>
    <tr>
      <th>Nome</th>
      <th>O que conta</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fullscanStep</code></td>
      <td>Linhas percorridas durante um full table scan</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sort</code></td>
      <td>Operações de ordenação executadas</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">autoindex</code></td>
      <td>Linhas inseridas em índices transientes que o SQLite criou pra acelerar um join</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">vmStep</code></td>
      <td>Operações da máquina virtual executadas</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">reprepare</code></td>
      <td>Re-prepares automáticos depois de uma mudança de schema</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">run</code></td>
      <td>Ciclos de execução iniciados</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">filterMiss</code></td>
      <td>Resultados de Bloom filter que ainda exigiram o passo do join</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">filterHit</code></td>
      <td>Passos de join pulados porque um Bloom filter retornou não-encontrado</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">memused</code></td>
      <td>Bytes de heap aproximados que o statement segura</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">filterMiss</code> e <code class="language-plaintext highlighter-rouge">filterHit</code> precisam do SQLite 3.38.0 ou mais novo. O Node embarca uma versão recente, então isso só te pega se você compilou com <code class="language-plaintext highlighter-rouge">--shared-sqlite</code> contra algo antigo. Nesse caso os nomes lançam <code class="language-plaintext highlighter-rouge">ERR_INVALID_ARG_VALUE</code>.</p>

<p>O <code class="language-plaintext highlighter-rouge">memused</code> é o esquisito da lista. Ele reporta uso <em>atual</em> em vez de uma contagem acumulada, então o SQLite ignora a flag de reset pra ele e o <code class="language-plaintext highlighter-rouge">resetStats()</code> não mexe nesse valor.</p>

<h2 id="um-bom-uso-pra-isso">Um bom uso pra isso</h2>

<p>O Aaron cogitou plugar isso no Rails pra avisar ou estourar erro em test e development. Mesma ideia aqui, e é barato - o <code class="language-plaintext highlighter-rouge">stat()</code> lê um inteiro que o SQLite já mantém. O guardrail inteiro cabe em seis linhas:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">assert</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:assert</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">function</span> <span class="nf">assertNoScan</span><span class="p">(</span><span class="nx">stmt</span><span class="p">,</span> <span class="p">...</span><span class="nx">params</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">stmt</span><span class="p">.</span><span class="nf">resetStats</span><span class="p">();</span>
  <span class="kd">const</span> <span class="nx">rows</span> <span class="o">=</span> <span class="nx">stmt</span><span class="p">.</span><span class="nf">all</span><span class="p">(...</span><span class="nx">params</span><span class="p">);</span>
  <span class="nx">assert</span><span class="p">.</span><span class="nf">strictEqual</span><span class="p">(</span>
    <span class="nx">stmt</span><span class="p">.</span><span class="nf">stat</span><span class="p">(</span><span class="dl">'</span><span class="s1">fullscanStep</span><span class="dl">'</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span>
    <span class="s2">`full table scan em: </span><span class="p">${</span><span class="nx">stmt</span><span class="p">.</span><span class="nx">sourceSQL</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
  <span class="p">);</span>
  <span class="k">return</span> <span class="nx">rows</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Passe o statement <code class="language-plaintext highlighter-rouge">SELECT * FROM users WHERE age = ?</code> lá de cima por essa função antes do índice existir e ela quebra com <code class="language-plaintext highlighter-rouge">999 !== 0</code>, imprimindo o SQL culpado.</p>

<p>Isso pesa mais agora que muito SQL sai da mão de um agente. Um modelo que emite <code class="language-plaintext highlighter-rouge">WHERE age = ?</code> não sabe se <code class="language-plaintext highlighter-rouge">age</code> tem índice, e nada na saída dele marca o chute.</p>

<p>O code review também deixa passar, porque a query está correta. O índice só passa a sustentar peso em produção, meses depois. O <code class="language-plaintext highlighter-rouge">fullscanStep</code> transforma isso numa asserção que a CI consegue quebrar.</p>

<p>Uma tabela indexada de cinco linhas ainda reporta zero, então fixture pequena não dá alarme falso. E os contadores são por statement, então você habilita query a query - o que é justamente o que você quer, já que um monte de query deve escanear mesmo.</p>

<p>Está na <code class="language-plaintext highlighter-rouge">main</code>, então sai na próxima release. Se você plugar a asserção num helper de teste, quero saber como foi.</p>

<p>Por hoje é só.</p>]]></content><author><name></name></author><category term="programacao" /><category term="nodejs" /><category term="sqlite" /><category term="performance" /><category term="node core" /><summary type="html"><![CDATA[Em julho o Aaron Patterson escreveu sobre detectar full table scans com SQLite. O pulo do gato é que você não precisa de EXPLAIN QUERY PLAN pra isso. O SQLite já mantém um contador por statement de quantas linhas ele percorreu durante um scan, e dá pra ler esse número depois que a query roda. Se for maior que zero, aquele statement escaneou.]]></summary></entry><entry xml:lang="en-US"><title type="html">Detecting SQLite Full Table Scans in Node.js</title><link href="https://codesilva.com/programacao/2026/08/13/detecting-sqlite-full-table-scans-in-nodejs.html" rel="alternate" type="text/html" title="Detecting SQLite Full Table Scans in Node.js" /><published>2026-08-13T00:00:00+00:00</published><updated>2026-08-13T00:00:00+00:00</updated><id>https://codesilva.com/programacao/2026/08/13/detecting-sqlite-full-table-scans-in-nodejs</id><content type="html" xml:base="https://codesilva.com/programacao/2026/08/13/detecting-sqlite-full-table-scans-in-nodejs.html"><![CDATA[<p>Back in July, Aaron Patterson <a href="https://tenderlovemaking.com/2026/07/15/detecting-full-table-scans-with-sqlite/">wrote about detecting full table scans with SQLite</a>. The trick is that you don’t need <code class="language-plaintext highlighter-rouge">EXPLAIN QUERY PLAN</code> for this. SQLite already keeps a per-statement counter of how many rows it walked during a scan, and you can read it after the query runs. If the number is greater than zero, that statement scanned.</p>

<p>One day later, Kevin Gibbons opened <a href="https://github.com/nodejs/node/issues/64540">an issue on <code class="language-plaintext highlighter-rouge">nodejs/node</code></a> asking for the same thing in <code class="language-plaintext highlighter-rouge">node:sqlite</code>, citing that post. There was no way to get at <code class="language-plaintext highlighter-rouge">sqlite3_stmt_status()</code> from JavaScript. I picked it up, and <a href="https://github.com/nodejs/node/pull/64541">it landed today</a>. Two methods on <code class="language-plaintext highlighter-rouge">StatementSync</code>:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">statement</span><span class="p">.</span><span class="nf">stat</span><span class="p">(</span><span class="nx">counter</span><span class="p">)</span>   <span class="c1">// read one counter</span>
<span class="nx">statement</span><span class="p">.</span><span class="nf">resetStats</span><span class="p">()</span>    <span class="c1">// zero all of them</span>
</code></pre></div></div>

<h2 id="the-scan-check">The scan check</h2>

<p>Same shape as Aaron’s Ruby example. A thousand users, a query on a column with no index:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">DatabaseSync</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:sqlite</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">db</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DatabaseSync</span><span class="p">(</span><span class="dl">'</span><span class="s1">:memory:</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">db</span><span class="p">.</span><span class="nf">exec</span><span class="p">(</span><span class="dl">'</span><span class="s1">CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)</span><span class="dl">'</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">insert</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">prepare</span><span class="p">(</span><span class="dl">'</span><span class="s1">INSERT INTO users (name, age) VALUES (?, ?)</span><span class="dl">'</span><span class="p">);</span>
<span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">1000</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">insert</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span><span class="s2">`user-</span><span class="p">${</span><span class="nx">i</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="nx">i</span> <span class="o">%</span> <span class="mi">80</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">stmt</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">prepare</span><span class="p">(</span><span class="dl">'</span><span class="s1">SELECT * FROM users WHERE age = ?</span><span class="dl">'</span><span class="p">);</span>

<span class="kd">function</span> <span class="nf">query</span><span class="p">(</span><span class="nx">age</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">rows</span> <span class="o">=</span> <span class="nx">stmt</span><span class="p">.</span><span class="nf">all</span><span class="p">(</span><span class="nx">age</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">fullscanStep:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">stmt</span><span class="p">.</span><span class="nf">stat</span><span class="p">(</span><span class="dl">'</span><span class="s1">fullscanStep</span><span class="dl">'</span><span class="p">));</span>
  <span class="nx">stmt</span><span class="p">.</span><span class="nf">resetStats</span><span class="p">();</span>
  <span class="k">return</span> <span class="nx">rows</span><span class="p">;</span>
<span class="p">}</span>

<span class="nf">query</span><span class="p">(</span><span class="mi">30</span><span class="p">);</span>
<span class="nx">db</span><span class="p">.</span><span class="nf">exec</span><span class="p">(</span><span class="dl">'</span><span class="s1">CREATE INDEX users_age_idx ON users (age)</span><span class="dl">'</span><span class="p">);</span>
<span class="nf">query</span><span class="p">(</span><span class="mi">30</span><span class="p">);</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fullscanStep: 999
fullscanStep: 0
</code></pre></div></div>

<p>999 before the index, 0 after. <code class="language-plaintext highlighter-rouge">vmStep</code> moves too, from 3059 down to 101 for the exact same result set.</p>

<p>Note the <code class="language-plaintext highlighter-rouge">resetStats()</code> call. The counters are cumulative for the lifetime of the prepared statement, so if you reuse a statement across a request loop - which is the whole point of preparing it - you need to zero them between measurements or you’re reading a running total.</p>

<h2 id="the-counters">The counters</h2>

<p><code class="language-plaintext highlighter-rouge">stat()</code> takes a name and returns a number:</p>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>What it counts</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fullscanStep</code></td>
      <td>Rows stepped through during a full table scan</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sort</code></td>
      <td>Sort operations performed</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">autoindex</code></td>
      <td>Rows inserted into transient indices SQLite built to speed up a join</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">vmStep</code></td>
      <td>Virtual machine operations executed</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">reprepare</code></td>
      <td>Automatic re-prepares after a schema change</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">run</code></td>
      <td>Execution cycles started</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">filterMiss</code></td>
      <td>Bloom filter results that still required the join step</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">filterHit</code></td>
      <td>Join steps skipped because a Bloom filter returned not-found</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">memused</code></td>
      <td>Approximate heap bytes held by the statement</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">filterMiss</code> and <code class="language-plaintext highlighter-rouge">filterHit</code> need SQLite 3.38.0 or newer. Node bundles a recent one, so this only bites if you built with <code class="language-plaintext highlighter-rouge">--shared-sqlite</code> against something old. In that case the names throw <code class="language-plaintext highlighter-rouge">ERR_INVALID_ARG_VALUE</code>.</p>

<p><code class="language-plaintext highlighter-rouge">memused</code> is the odd one. It reports <em>current</em> usage rather than an accumulated count, so SQLite ignores the reset flag for it and <code class="language-plaintext highlighter-rouge">resetStats()</code> leaves it alone.</p>

<h2 id="a-good-usage-for-it">A good usage for it</h2>

<p>Aaron floated wiring this into Rails to warn or raise in test and development. Same idea here, and it’s cheap - <code class="language-plaintext highlighter-rouge">stat()</code> reads an integer SQLite already maintains. The whole guardrail is six lines:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">assert</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:assert</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">function</span> <span class="nf">assertNoScan</span><span class="p">(</span><span class="nx">stmt</span><span class="p">,</span> <span class="p">...</span><span class="nx">params</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">stmt</span><span class="p">.</span><span class="nf">resetStats</span><span class="p">();</span>
  <span class="kd">const</span> <span class="nx">rows</span> <span class="o">=</span> <span class="nx">stmt</span><span class="p">.</span><span class="nf">all</span><span class="p">(...</span><span class="nx">params</span><span class="p">);</span>
  <span class="nx">assert</span><span class="p">.</span><span class="nf">strictEqual</span><span class="p">(</span>
    <span class="nx">stmt</span><span class="p">.</span><span class="nf">stat</span><span class="p">(</span><span class="dl">'</span><span class="s1">fullscanStep</span><span class="dl">'</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span>
    <span class="s2">`full table scan in: </span><span class="p">${</span><span class="nx">stmt</span><span class="p">.</span><span class="nx">sourceSQL</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
  <span class="p">);</span>
  <span class="k">return</span> <span class="nx">rows</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Run the <code class="language-plaintext highlighter-rouge">SELECT * FROM users WHERE age = ?</code> statement from above through it before the index exists and it fails with <code class="language-plaintext highlighter-rouge">999 !== 0</code>, printing the offending SQL.</p>

<p>This matters more now that a lot of SQL gets written by an agent. A model emitting <code class="language-plaintext highlighter-rouge">WHERE age = ?</code> doesn’t know whether <code class="language-plaintext highlighter-rouge">age</code> is indexed, and nothing in its output marks the guess.</p>

<p>Review misses it too, because the query is correct. The index only becomes load-bearing in production, months later. <code class="language-plaintext highlighter-rouge">fullscanStep</code> turns that into an assertion CI can fail on.</p>

<p>A five-row indexed table still reports zero, so small fixtures don’t false-alarm. And the counters are per-statement, so you opt in query by query - which you want, since plenty of queries are supposed to scan.</p>

<p>It’s on <code class="language-plaintext highlighter-rouge">main</code>, so it ships in the next release. If you wire the assertion into a test helper, I’d like to hear how it goes.</p>

<p>Thanks for reading!</p>]]></content><author><name></name></author><category term="programacao" /><category term="nodejs" /><category term="sqlite" /><category term="performance" /><category term="node core" /><summary type="html"><![CDATA[Back in July, Aaron Patterson wrote about detecting full table scans with SQLite. The trick is that you don’t need EXPLAIN QUERY PLAN for this. SQLite already keeps a per-statement counter of how many rows it walked during a scan, and you can read it after the query runs. If the number is greater than zero, that statement scanned.]]></summary></entry><entry xml:lang="pt-BR"><title type="html">Como usar o harness do Claude Code via SDK</title><link href="https://codesilva.com/ai/claude/developer-tools/2026/08/11/claude-code-harness-via-sdk.html" rel="alternate" type="text/html" title="Como usar o harness do Claude Code via SDK" /><published>2026-08-11T00:00:00+00:00</published><updated>2026-08-11T00:00:00+00:00</updated><id>https://codesilva.com/ai/claude/developer-tools/2026/08/11/claude-code-harness-via-sdk</id><content type="html" xml:base="https://codesilva.com/ai/claude/developer-tools/2026/08/11/claude-code-harness-via-sdk.html"><![CDATA[<p>A maioria das pessoas usa o Claude Code como CLI. Você abre o terminal, conversa com o agente, e ele edita arquivos, roda comandos e faz review de PR.</p>

<p>Esse mesmo harness dá pra usar de dentro do seu código: tools, system prompt, agent loop e skills. Sem construir nada do zero.</p>

<h2 id="o-problema">O problema</h2>

<p>Se você quer um agente que opera no seu codebase, o caminho óbvio é pegar a API da Anthropic e montar tudo na mão. Você define as tools, escreve o system prompt, implementa o loop de <code class="language-plaintext highlighter-rouge">tool-use</code>, cuida do contexto e das permissões. É muito código antes de chegar no que interessa.</p>

<p>E no fim você tem uma versão pior do que o Claude Code já faz.</p>

<h2 id="o-preset">O preset</h2>

<p>O <code class="language-plaintext highlighter-rouge">@anthropic-ai/claude-agent-sdk</code> tem um preset que liga o Claude Code inteiro com uma linha pra cada peça:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">query</span><span class="p">,</span> <span class="nx">startup</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@anthropic-ai/claude-agent-sdk</span><span class="dl">"</span><span class="p">;</span>

<span class="k">await</span> <span class="nf">startup</span><span class="p">({</span> <span class="na">initializeTimeoutMs</span><span class="p">:</span> <span class="mi">10</span><span class="nx">_000</span> <span class="p">});</span>

<span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nf">query</span><span class="p">({</span>
  <span class="na">prompt</span><span class="p">:</span> <span class="dl">"</span><span class="s2">review PR #42</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">options</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">tools</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">preset</span><span class="dl">'</span><span class="p">,</span> <span class="na">preset</span><span class="p">:</span> <span class="dl">'</span><span class="s1">claude_code</span><span class="dl">'</span> <span class="p">},</span>
    <span class="na">systemPrompt</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">preset</span><span class="dl">'</span><span class="p">,</span> <span class="na">preset</span><span class="p">:</span> <span class="dl">'</span><span class="s1">claude_code</span><span class="dl">'</span> <span class="p">},</span>
    <span class="na">skills</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">pr-review</span><span class="dl">'</span><span class="p">],</span>
    <span class="na">permissionMode</span><span class="p">:</span> <span class="dl">'</span><span class="s1">bypassPermissions</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">},</span>
<span class="p">});</span>

<span class="k">for</span> <span class="k">await </span><span class="p">(</span><span class="kd">const</span> <span class="nx">message</span> <span class="k">of</span> <span class="nx">result</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// handle messages: assistant turns, tool results, etc.</span>
<span class="p">}</span>
</code></pre></div></div>

<p>O harness vem em duas linhas: <code class="language-plaintext highlighter-rouge">{ type: 'preset', preset: 'claude_code' }</code> em <code class="language-plaintext highlighter-rouge">tools</code> e em <code class="language-plaintext highlighter-rouge">systemPrompt</code>. O resto é iterar sobre as mensagens.</p>

<h2 id="como-funciona-por-baixo">Como funciona por baixo</h2>

<p>O SDK <strong>não</strong> chama a API da Anthropic direto. Ele sobe o <code class="language-plaintext highlighter-rouge">claude</code> CLI como subprocesso e conversa com ele por stdin/stdout, em JSON. Daí saem três consequências:</p>

<ul>
  <li>Você precisa do <code class="language-plaintext highlighter-rouge">claude</code> CLI instalado.</li>
  <li>As tools são as mesmas que você usa no CLI.</li>
  <li>Atualizou o CLI, mudou o comportamento do seu agente.</li>
</ul>

<h2 id="o-que-vem-de-graça">O que vem de graça</h2>

<ul>
  <li>Tools built-in: Read, Write, Edit, Bash, Grep, Glob, Agent, TodoWrite, WebSearch e outras.</li>
  <li>Skills: carrega do <code class="language-plaintext highlighter-rouge">~/.claude/skills/</code> com <code class="language-plaintext highlighter-rouge">skills: ['nome-da-skill']</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">Session management</code>: retomar conversas e manter contexto.</li>
  <li><code class="language-plaintext highlighter-rouge">Context compaction</code>: o SDK gerencia o contexto sozinho.</li>
  <li>Hooks de ciclo de vida, pra interceptar e mudar comportamento.</li>
  <li>Suporte a servidores MCP (Model Context Protocol).</li>
</ul>

<h2 id="referência">Referência</h2>

<p>O <a href="https://github.com/chatml/chatml">ChatML</a> é um projeto real que faz exatamente isso. É um app desktop pra macOS que usa o mesmo SDK com o preset <code class="language-plaintext highlighter-rouge">claude_code</code> pra entregar a experiência do Claude Code numa interface nativa.</p>

<p>No time a gente usa um wrapper interno, o cc-harness, que não é público. São ~300 linhas sobre o SDK que fazem a primeira passada de code review nos PRs de um projeto. Ele carrega as skills de review, abre o app no Chrome via MCP pra testar as mudanças de frontend e comenta no PR.</p>

<p>Ele nunca aprova nada. A IA faz a primeira passada, e uma pessoa faz a segunda.</p>

<hr />

<p>Se você já usa Claude Code e quer automatizar ou embutir num produto, o SDK é o caminho mais curto. O que já existe não precisa ser reimplementado.</p>

<p>Por hoje é só.</p>]]></content><author><name></name></author><category term="ai" /><category term="claude" /><category term="developer-tools" /><category term="claude-code" /><category term="sdk" /><category term="agent-sdk" /><summary type="html"><![CDATA[A maioria das pessoas usa o Claude Code como CLI. Você abre o terminal, conversa com o agente, e ele edita arquivos, roda comandos e faz review de PR.]]></summary></entry><entry xml:lang="en-US"><title type="html">How to Use the Claude Code Harness via the SDK</title><link href="https://codesilva.com/ai/claude/developer-tools/2026/08/11/how-to-use-the-claude-code-harness-via-the-sdk.html" rel="alternate" type="text/html" title="How to Use the Claude Code Harness via the SDK" /><published>2026-08-11T00:00:00+00:00</published><updated>2026-08-11T00:00:00+00:00</updated><id>https://codesilva.com/ai/claude/developer-tools/2026/08/11/how-to-use-the-claude-code-harness-via-the-sdk</id><content type="html" xml:base="https://codesilva.com/ai/claude/developer-tools/2026/08/11/how-to-use-the-claude-code-harness-via-the-sdk.html"><![CDATA[<p>Most people use Claude Code as a CLI. You open a terminal, talk to the agent, and it edits files, runs commands and reviews PRs.</p>

<p>You can drive that same harness from your own code: tools, system prompt, agent loop and skills. Nothing built from scratch.</p>

<h2 id="the-problem">The problem</h2>

<p>If you want an agent that works on your codebase, the obvious path is to take the Anthropic API and wire everything by hand. You define the tools, write the system prompt, implement the tool-use loop, and handle context and permissions. That is a lot of code before you reach the part you care about.</p>

<p>And you end up with a worse version of what Claude Code already does.</p>

<h2 id="the-preset">The preset</h2>

<p><code class="language-plaintext highlighter-rouge">@anthropic-ai/claude-agent-sdk</code> has a preset that turns on all of Claude Code with one line per piece:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">query</span><span class="p">,</span> <span class="nx">startup</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@anthropic-ai/claude-agent-sdk</span><span class="dl">"</span><span class="p">;</span>

<span class="k">await</span> <span class="nf">startup</span><span class="p">({</span> <span class="na">initializeTimeoutMs</span><span class="p">:</span> <span class="mi">10</span><span class="nx">_000</span> <span class="p">});</span>

<span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nf">query</span><span class="p">({</span>
  <span class="na">prompt</span><span class="p">:</span> <span class="dl">"</span><span class="s2">review PR #42</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">options</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">tools</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">preset</span><span class="dl">'</span><span class="p">,</span> <span class="na">preset</span><span class="p">:</span> <span class="dl">'</span><span class="s1">claude_code</span><span class="dl">'</span> <span class="p">},</span>
    <span class="na">systemPrompt</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">preset</span><span class="dl">'</span><span class="p">,</span> <span class="na">preset</span><span class="p">:</span> <span class="dl">'</span><span class="s1">claude_code</span><span class="dl">'</span> <span class="p">},</span>
    <span class="na">skills</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">pr-review</span><span class="dl">'</span><span class="p">],</span>
    <span class="na">permissionMode</span><span class="p">:</span> <span class="dl">'</span><span class="s1">bypassPermissions</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">},</span>
<span class="p">});</span>

<span class="k">for</span> <span class="k">await </span><span class="p">(</span><span class="kd">const</span> <span class="nx">message</span> <span class="k">of</span> <span class="nx">result</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// handle messages: assistant turns, tool results, etc.</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The harness comes in two lines: <code class="language-plaintext highlighter-rouge">{ type: 'preset', preset: 'claude_code' }</code> for <code class="language-plaintext highlighter-rouge">tools</code> and for <code class="language-plaintext highlighter-rouge">systemPrompt</code>. The rest is iterating over messages.</p>

<h2 id="how-it-works-underneath">How it works underneath</h2>

<p>The SDK does <strong>not</strong> call the Anthropic API directly. It spawns the <code class="language-plaintext highlighter-rouge">claude</code> CLI as a subprocess and talks to it over stdin/stdout in JSON. That has three consequences:</p>

<ul>
  <li>You need the <code class="language-plaintext highlighter-rouge">claude</code> CLI installed.</li>
  <li>The tools are the same ones you use in the CLI.</li>
  <li>Update the CLI and your agent’s behavior changes with it.</li>
</ul>

<h2 id="what-you-get-for-free">What you get for free</h2>

<ul>
  <li>Built-in tools: Read, Write, Edit, Bash, Grep, Glob, Agent, TodoWrite, WebSearch and more.</li>
  <li>Skills, loaded from <code class="language-plaintext highlighter-rouge">~/.claude/skills/</code> with <code class="language-plaintext highlighter-rouge">skills: ['skill-name']</code>.</li>
  <li>Session management: resuming conversations and keeping context.</li>
  <li>Context compaction: the SDK manages context on its own.</li>
  <li>Lifecycle hooks to intercept and change behavior.</li>
  <li>Support for MCP (Model Context Protocol) servers.</li>
</ul>

<h2 id="reference">Reference</h2>

<p><a href="https://github.com/chatml/chatml">ChatML</a> is a real project doing exactly this. It is a macOS desktop app that uses the same SDK with the <code class="language-plaintext highlighter-rouge">claude_code</code> preset to offer the Claude Code experience in a native interface.</p>

<p>On our team we use an internal wrapper called cc-harness, which is not public. It is ~300 lines on top of the SDK that do the first pass of code review on a project’s PRs. It loads the review skills, opens the app in Chrome through MCP to test frontend changes, and comments on the PR.</p>

<p>It never approves anything. The AI does the first pass, and a person does the second.</p>

<hr />

<p>If you already use Claude Code and want to automate it or embed it in a product, the SDK is the shortest path. There is no need to reimplement what already exists.</p>

<p>Thanks for reading!</p>]]></content><author><name></name></author><category term="ai" /><category term="claude" /><category term="developer-tools" /><category term="claude-code" /><category term="sdk" /><category term="agent-sdk" /><summary type="html"><![CDATA[Most people use Claude Code as a CLI. You open a terminal, talk to the agent, and it edits files, runs commands and reviews PRs.]]></summary></entry><entry xml:lang="en-US"><title type="html">Amdahl’s Law: TypeScript 7 is 10x faster. Your CI is still slow</title><link href="https://codesilva.com/programacao/2026/08/07/amdahls-law-typescript-7-is-10x-faster-your-ci-is-still-slow.html" rel="alternate" type="text/html" title="Amdahl’s Law: TypeScript 7 is 10x faster. Your CI is still slow" /><published>2026-08-07T00:00:00+00:00</published><updated>2026-08-07T00:00:00+00:00</updated><id>https://codesilva.com/programacao/2026/08/07/amdahls-law-typescript-7-is-10x-faster-your-ci-is-still-slow</id><content type="html" xml:base="https://codesilva.com/programacao/2026/08/07/amdahls-law-typescript-7-is-10x-faster-your-ci-is-still-slow.html"><![CDATA[<p>Your application’s CI keeps getting slower over time. A little more every month, until the day it stops being an annoyance and turns into a blocker.</p>

<p>What’s your first move?</p>

<p>I watched this happen last week.</p>

<p>A Node + TypeScript service had a 15-minute CI. Unit tests with coverage, around 1,200 of them.</p>

<p>And the reasoning that shows up in the moment is always the same: if it’s slow, something is wrong, so it must be possible to optimize. Upgrade TypeScript to the Go version, which typechecks much faster. Swap eslint and prettier for ox, which are much faster.</p>

<p>None of that is false. TypeScript in Go <strong>is</strong> absurdly faster. Oxlint <strong>is</strong> faster than eslint.</p>

<p>And it still wasn’t going to fix anything.</p>

<h2 id="typescript-7-really-is-that-fast">TypeScript 7 really is that fast</h2>

<p>The <code class="language-plaintext highlighter-rouge">tsc</code> you know was a TypeScript compiler written in TypeScript, running on Node. Version 7 is a faithful port of it to Go, shipped as a native binary, with type-checking parallelized across 4 threads.</p>

<p>Microsoft’s numbers are not modest: VS Code went from 125.7s to 10.6s, Sentry from 139.8s to 15.7s. In the editor, opening a file with errors dropped from ~17.5s to under 1.3s.</p>

<p>The emitted JavaScript is the same. The type system is the same. This is a compile-time story, full stop.</p>

<p>One detail if you plan to migrate: <strong>7.0 has no programmatic API</strong>, and it only arrives in 7.1. Any tool that embeds the compiler - typescript-eslint, Volar, and by extension Vue, Svelte, Astro, Angular - stays on TypeScript 6.</p>

<h2 id="the-japanese-knife-and-the-pot-of-beans">The Japanese knife and the pot of beans</h2>

<p><a href="https://en.wikipedia.org/wiki/Amdahl%27s_law">Amdahl’s Law</a> explains why swapping the compiler was never going to move the needle:</p>

<figure class="post-figure">
  <img src="/assets/images/amdahl/amdahls-law-en.png" alt="Amdahl's Law: S equals 1 divided by ((1 - p) + p / s). Two bars of equal length compare before and after: in BEFORE, a narrow slice p and a very wide slice 1 - p; in AFTER, the p slice is a hair-thin sliver and the 1 - p slice is unchanged. As s grows, S approaches 1 / (1 - p)." />
  <figcaption>The slice you optimized shrinks to a sliver. The other one doesn't move - and it's the one setting the total.</figcaption>
</figure>

<p><code class="language-plaintext highlighter-rouge">p</code> is the fraction of the work you improved, <code class="language-plaintext highlighter-rouge">s</code> is how much you improved it. The painful part is the limit: as <code class="language-plaintext highlighter-rouge">s</code> approaches infinity, the maximum speedup of the whole system becomes <code class="language-plaintext highlighter-rouge">1 / (1 - p)</code>.</p>

<p>In other words: <strong>the part you didn’t optimize is your ceiling.</strong> And it doesn’t move.</p>

<p>Think about Sunday lunch. It takes you 40 minutes. You buy an expensive Japanese knife that chops onions 10x faster, and the marketing wasn’t lying. Except chopping onions took 2 minutes, and now it takes 12 seconds.</p>

<p>Lunch still takes a little over 38 minutes.</p>

<p>Because what takes time is the pot of beans on the stove. And beans are not a chopping problem, they are a waiting problem. No knife solves waiting.</p>

<h2 id="they-did-the-upgrade">They did the upgrade</h2>

<p>Everything went up at once: eslint 8.57 -&gt; oxlint 1.76, prettier 3.0 -&gt; oxfmt 0.61, TypeScript 5.9 -&gt; TypeScript 7.</p>

<table>
  <thead>
    <tr>
      <th>step</th>
      <th>before</th>
      <th>after</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lint</code></td>
      <td>5.56s</td>
      <td>3.73s</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">format:check</code></td>
      <td>6.51s</td>
      <td>3.42s</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">typecheck</code></td>
      <td>4.46s</td>
      <td>4.04s</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">typecheck</code> for tests</td>
      <td>4.50s</td>
      <td>3.93s</td>
    </tr>
    <tr>
      <td><strong>total</strong></td>
      <td><strong>21.03s</strong></td>
      <td><strong>15.12s</strong></td>
    </tr>
  </tbody>
</table>

<p>The new engines delivered the promised 6x to 10x. <code class="language-plaintext highlighter-rouge">tsc</code> 7 compiles the entire project in 1.22s. What eats the difference is the fixed overhead of each invocation: spinning up the process and loading the tool costs more than the actual work.</p>

<p>Net result: <strong>5.91 seconds.</strong> Out of 900.</p>

<p>And note that those four steps together were 21s in a 900s pipeline, which puts <code class="language-plaintext highlighter-rouge">p</code> at 2.3%. If the tools were infinitely fast, zero cost:</p>

<figure class="post-figure">
  <img src="/assets/images/amdahl/concrete-case-en.png" alt="S equals 1 divided by (1 - 0.023) equals 1.024x. A long bar labeled 900s pipeline with a hair-thin red sliver at its left end, marked as 21s of lint, format, typecheck (p = 2.3%). Below it, a bar of almost the same length: 879s in the best possible case. Net gain: 21 seconds." />
  <figcaption>With infinitely fast tooling at zero cost, the 900s pipeline drops to 879s. That's the ceiling.</figcaption>
</figure>

<p><strong>In the impossible scenario, CI drops from 15m00s to 14m39s.</strong> That was the ceiling, and it was available before a single line of code was written, from one thirty-second division.</p>

<p>Microsoft’s numbers aren’t a lie, either. Slack cut CI type-checking from 7.5 minutes to 1.25 minutes, but there it’s a dedicated <code class="language-plaintext highlighter-rouge">tsc</code> step, where the compiler is 100% of the work. Same compiler, same engine gain, opposite outcome: <strong>the difference isn’t in the tool, it’s in the denominator.</strong></p>

<h2 id="measuring-costs-five-minutes">Measuring costs five minutes</h2>

<p>GitHub Actions already shows the duration of every step in the UI, for free. Open the last run and read it before forming any theory. Then divide the step’s time by the total: that is the maximum you can gain by attacking it. If it comes out to 2%, you just saved yourself a week.</p>

<p>If you need to dig deeper, the tools already exist and are criminally underused:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">jest --verbose</code> gives you the duration per file. Three files usually account for half the time.</li>
  <li><code class="language-plaintext highlighter-rouge">jest --detectOpenHandles</code> finds promises that never resolve and timers that never clear, which leave the runner sitting around waiting for the event loop to drain after the tests already finished. It has existed for years.</li>
</ul>

<p>Knuth wrote this in 1974, on the same page of <a href="https://pic.plover.com/knuth-GOTO.pdf"><em>Structured Programming with go to Statements</em></a> that gave us the “premature optimization is the root of all evil” everyone quotes at half length:</p>

<blockquote>
  <p>“It is often a mistake to make a priori judgments about what parts of a program are really critical, since the universal experience of programmers who have been using measurement tools has been that their intuitive guesses fail.”</p>
</blockquote>

<h2 id="the-problem-is-the-jump">The problem is the jump</h2>

<p>None of this means “don’t upgrade.” Do upgrade - TypeScript 7 is an impressive piece of engineering and I plan to migrate everything I can. Those 6 seconds don’t move CI, but they do move the agent loop, which runs <code class="language-plaintext highlighter-rouge">typecheck</code> and <code class="language-plaintext highlighter-rouge">lint</code> dozens of times an hour. That is a real gain, just in a different number.</p>

<p>What’s wrong is something else: going from “it’s slow” straight to “swap it for something faster,” without the thirty-second division in between. <strong>Tool speed is not system speed.</strong></p>

<p>And swapping tools is comfortable because it looks productive: there’s a PR, there’s a changelog, there’s a nice benchmark to show. Measuring first looks like bureaucracy.</p>

<p>But measuring is what separates engineering from cheering.</p>

<p>Thanks for reading!</p>

<hr />

<ul>
  <li><a href="https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/">Announcing TypeScript 7.0</a> - the official announcement, with the benchmarks and the full list of breaking changes.</li>
  <li><a href="https://dev.to/nazar-boyko/typescript-7-went-native-what-actually-changes-and-what-doesnt-6b3">TypeScript 7 Went Native: What Actually Changes and What Doesn’t</a> - a good read on what the migration does not change.</li>
  <li><em>Systems Performance</em>, by Brendan Gregg. If you only read one chapter, read chapter 2.</li>
</ul>]]></content><author><name></name></author><category term="programacao" /><category term="typescript" /><category term="performance" /><category term="ci" /><category term="testing" /><category term="software engineering" /><summary type="html"><![CDATA[Your application’s CI keeps getting slower over time. A little more every month, until the day it stops being an annoyance and turns into a blocker.]]></summary></entry><entry xml:lang="pt-BR"><title type="html">Lei de Amdahl: TypeScript 7 é 10x mais rápido. Sua CI continua lenta</title><link href="https://codesilva.com/programacao/2026/08/07/lei-de-amdahl-typescript-7-e-10x-mais-rapido-sua-ci-continua-lenta.html" rel="alternate" type="text/html" title="Lei de Amdahl: TypeScript 7 é 10x mais rápido. Sua CI continua lenta" /><published>2026-08-07T00:00:00+00:00</published><updated>2026-08-07T00:00:00+00:00</updated><id>https://codesilva.com/programacao/2026/08/07/lei-de-amdahl-typescript-7-e-10x-mais-rapido-sua-ci-continua-lenta</id><content type="html" xml:base="https://codesilva.com/programacao/2026/08/07/lei-de-amdahl-typescript-7-e-10x-mais-rapido-sua-ci-continua-lenta.html"><![CDATA[<p>A CI da sua aplicação vai ficando mais lenta com o tempo. Um pouco a cada mês, até o dia em que ela deixa de ser um incômodo e vira um impedimento.</p>

<p>Qual é o seu primeiro passo?</p>

<p>Vi isso acontecer semana passada.</p>

<p>A CI de um serviço Node + TypeScript levava 15 minutos. Testes de unidade com coverage, uns 1.200 testes.</p>

<p>E o raciocínio que aparece na hora é sempre o mesmo: se está lento, tem algo errado, deve dar pra otimizar. Atualiza o TypeScript pra versão em Go, que faz typecheck muito mais rápido. Troca o eslint e o prettier pelo ox, que são muito mais rápidos.</p>

<p>Nada disso é falso. O TypeScript em Go <strong>é</strong> absurdamente mais rápido. O oxlint <strong>é</strong> mais rápido que o eslint.</p>

<p>E mesmo assim não ia consertar nada.</p>

<h2 id="o-typescript-7-é-tudo-isso-mesmo">O TypeScript 7 é tudo isso mesmo</h2>

<p>O <code class="language-plaintext highlighter-rouge">tsc</code> era um compilador de TypeScript escrito em TypeScript, rodando em Node. A 7 é uma porta fiel dele para Go, em binário nativo, com type-checking paralelo em 4 threads.</p>

<p>Os números da Microsoft não são modestos: VS Code caiu de 125,7s pra 10,6s, Sentry de 139,8s pra 15,7s. No editor, abrir um arquivo com erros saiu de ~17,5s pra menos de 1,3s.</p>

<p>O JavaScript emitido é o mesmo, o sistema de tipos é o mesmo. Isso é uma história de compile-time, ponto final.</p>

<p>Um detalhe pra quem for migrar: <strong>a 7.0 não tem API programática</strong>, ela só chega na 7.1. Qualquer ferramenta que embute o compilador - typescript-eslint, Volar, e por tabela Vue, Svelte, Astro, Angular - continua no TypeScript 6.</p>

<h2 id="a-faca-japonesa-e-o-feijão">A faca japonesa e o feijão</h2>

<p>A <a href="https://pt.wikipedia.org/wiki/Lei_de_Amdahl">lei de Amdahl</a> explica por que trocar o compilador não ia mudar nada:</p>

<figure class="post-figure">
  <img src="/assets/images/amdahl/lei-de-amdahl-pt.png" alt="A lei de Amdahl: S igual a 1 dividido por ((1 - p) + p / s). Duas barras de mesmo comprimento comparam antes e depois: no ANTES, uma fatia estreita p e uma fatia larga 1 - p; no DEPOIS, a fatia p virou um risco fininho e a fatia 1 - p continua idêntica. Quando s cresce, S tende a 1 / (1 - p)." />
  <figcaption>A fatia que você otimizou encolhe até virar um risco. A outra não se move - e é ela que define o total.</figcaption>
</figure>

<p><code class="language-plaintext highlighter-rouge">p</code> é a fração do trabalho que você melhorou, <code class="language-plaintext highlighter-rouge">s</code> é o quanto melhorou. A parte que dói é o limite: se <code class="language-plaintext highlighter-rouge">s</code> tender ao infinito, o speedup máximo do sistema inteiro vira <code class="language-plaintext highlighter-rouge">1 / (1 - p)</code>.</p>

<p>Ou seja: <strong>o pedaço que você não otimizou é o seu teto.</strong> E ele não se move.</p>

<p>Pensa no almoço de domingo. Você leva 40 minutos. Compra uma faca japonesa cara que corta cebola 10x mais rápido, e a propaganda não mentiu. Só que cortar cebola levava 2 minutos, e agora leva 12 segundos.</p>

<p>O almoço continua levando 38 minutos e pouco.</p>

<p>Porque o que demora é o feijão na panela. E feijão não é problema de corte, é problema de espera. Faca nenhuma resolve espera.</p>

<h2 id="fizeram-a-atualização">Fizeram a atualização</h2>

<p>Subiram tudo junto: eslint 8.57 -&gt; oxlint 1.76, prettier 3.0 -&gt; oxfmt 0.61, TypeScript 5.9 -&gt; TypeScript 7.</p>

<table>
  <thead>
    <tr>
      <th>passo</th>
      <th>antes</th>
      <th>depois</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lint</code></td>
      <td>5,56s</td>
      <td>3,73s</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">format:check</code></td>
      <td>6,51s</td>
      <td>3,42s</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">typecheck</code></td>
      <td>4,46s</td>
      <td>4,04s</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">typecheck</code> dos testes</td>
      <td>4,50s</td>
      <td>3,93s</td>
    </tr>
    <tr>
      <td><strong>total</strong></td>
      <td><strong>21,03s</strong></td>
      <td><strong>15,12s</strong></td>
    </tr>
  </tbody>
</table>

<p>As engines novas entregaram os 6x a 10x prometidos. O <code class="language-plaintext highlighter-rouge">tsc</code> 7 compila o projeto inteiro em 1,22s. O que come a diferença é o overhead fixo de cada invocação: subir o processo e carregar a ferramenta custa mais que o trabalho em si.</p>

<p>Resultado líquido: <strong>5,91 segundos.</strong> De 900.</p>

<p>E olha que esses quatro passos somados eram 21s numa pipeline de 900s, ou seja <code class="language-plaintext highlighter-rouge">p = 2,3%</code>. Se as ferramentas fossem infinitamente rápidas, custo zero:</p>

<figure class="post-figure">
  <img src="/assets/images/amdahl/caso-concreto-pt.png" alt="S igual a 1 dividido por (1 - 0,023) igual a 1,024x. Uma barra longa de 900s de pipeline com um risco vermelho fininho na ponta esquerda, marcado como 21s de lint, format e typecheck (p = 2,3%). Abaixo, uma barra quase do mesmo tamanho: 879s no melhor caso possível. Ganho líquido: 21 segundos." />
  <figcaption>Com ferramenta infinitamente rápida e custo zero, a pipeline de 900s cai pra 879s. Esse é o teto.</figcaption>
</figure>

<p><strong>No cenário impossível, a CI cairia de 15min00 pra 14min39.</strong> Esse era o teto, e ele estava disponível antes de qualquer linha de código, com uma divisão de trinta segundos.</p>

<p>Isso não faz do benchmark da Microsoft mentira. O Slack cortou type-checking na CI de 7,5 minutos pra 1,25 minuto, só que lá é um passo dedicado de <code class="language-plaintext highlighter-rouge">tsc</code>, onde o compilador é 100% do trabalho. Mesmo compilador, mesmo ganho de engine, resultado oposto: <strong>a diferença não está na ferramenta, está no denominador.</strong></p>

<h2 id="medir-custa-cinco-minutos">Medir custa cinco minutos</h2>

<p>O GitHub Actions já mostra a duração de cada step na UI, de graça. Abra a última run e leia antes de qualquer teoria. Depois divida o tempo do passo pelo tempo total: aquilo é o máximo que você pode ganhar atacando ele. Se der 2%, você acabou de economizar uma semana.</p>

<p>Se precisar cavar mais fundo, as ferramentas já existem e são subutilizadas de um jeito criminoso:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">jest --verbose</code> te dá a duração por arquivo. Quase sempre 3 arquivos respondem por metade do tempo.</li>
  <li><code class="language-plaintext highlighter-rouge">jest --detectOpenHandles</code> acha promise que não resolve e timer que não limpa, que deixam o runner sentado esperando o event loop drenar depois que os testes já acabaram. Existe faz anos.</li>
</ul>

<p>O Knuth escreveu isso em 1974, na mesma página do <a href="https://pic.plover.com/knuth-GOTO.pdf"><em>Structured Programming with go to Statements</em></a> de onde saiu o “premature optimization is the root of all evil” que todo mundo cita pela metade:</p>

<blockquote>
  <p>“É frequentemente um erro fazer julgamentos a priori sobre quais partes de um programa são realmente críticas, já que a experiência universal dos programadores que usam ferramentas de medição é que os palpites intuitivos deles falham.”</p>
</blockquote>

<h2 id="o-problema-é-o-pulo">O problema é o pulo</h2>

<p>Nada disso quer dizer “não atualize”. Atualize - o TypeScript 7 é um feito de engenharia impressionante e eu vou migrar tudo que der. Aqueles 6 segundos não mudam a CI, mas mudam o loop do agente, que roda <code class="language-plaintext highlighter-rouge">typecheck</code> e <code class="language-plaintext highlighter-rouge">lint</code> dezenas de vezes por hora. É ganho real, só que em outro número.</p>

<p>O que está errado é outra coisa: ir de “está lento” direto pra “troca por algo mais rápido”, sem a divisão de trinta segundos no meio. <strong>Velocidade de ferramenta não é velocidade de sistema.</strong></p>

<p>E trocar ferramenta é confortável porque parece produtivo: tem PR, tem changelog, tem benchmark bonito pra mostrar. Medir primeiro parece burocracia.</p>

<p>Mas medir é o que separa engenharia de torcida.</p>

<p>Por hoje é só.</p>

<hr />

<ul>
  <li><a href="https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/">Announcing TypeScript 7.0</a> - o anúncio oficial, com os benchmarks e a lista completa de breaking changes.</li>
  <li><a href="https://dev.to/nazar-boyko/typescript-7-went-native-what-actually-changes-and-what-doesnt-6b3">TypeScript 7 Went Native: What Actually Changes and What Doesn’t</a> - boa leitura sobre o que a migração não muda.</li>
  <li><em>Systems Performance</em>, do Brendan Gregg. Se você só for ler um capítulo, leia o 2.</li>
</ul>]]></content><author><name></name></author><category term="programacao" /><category term="typescript" /><category term="performance" /><category term="ci" /><category term="testes" /><category term="engenharia de software" /><summary type="html"><![CDATA[A CI da sua aplicação vai ficando mais lenta com o tempo. Um pouco a cada mês, até o dia em que ela deixa de ser um incômodo e vira um impedimento.]]></summary></entry><entry xml:lang="pt-BR"><title type="html">Em Java, usar equals sempre foi remédio. Ninguém te contou a doença</title><link href="https://codesilva.com/programacao/2026/08/02/em-java-usar-equals-sempre-foi-remedio-ninguem-te-contou-a-doenca.html" rel="alternate" type="text/html" title="Em Java, usar equals sempre foi remédio. Ninguém te contou a doença" /><published>2026-08-02T00:00:00+00:00</published><updated>2026-08-02T00:00:00+00:00</updated><id>https://codesilva.com/programacao/2026/08/02/em-java-usar-equals-sempre-foi-remedio-ninguem-te-contou-a-doenca</id><content type="html" xml:base="https://codesilva.com/programacao/2026/08/02/em-java-usar-equals-sempre-foi-remedio-ninguem-te-contou-a-doenca.html"><![CDATA[<p>Em algum momento da sua vida com Java você escreveu isso e levou um susto:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Integer</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">1</span><span class="o">,</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">1</span><span class="o">;</span>
<span class="n">i</span> <span class="o">==</span> <span class="n">j</span>        <span class="c1">// true</span>

<span class="nc">Integer</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">1996</span><span class="o">,</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">1996</span><span class="o">;</span>
<span class="n">x</span> <span class="o">==</span> <span class="n">y</span>        <span class="c1">// false</span>
</code></pre></div></div>

<p>Aí você fez o que todo mundo faz: jogou no Google.</p>

<p>E achou rápido, porque <a href="https://stackoverflow.com/questions/1700081/why-is-128-128-false-but-127-127-is-true-when-comparing-integer-wrappers-in-ja">a pergunta está no Stack Overflow desde novembro de 2009</a>, com duzentos e poucos votos. A resposta é sempre a mesma: <code class="language-plaintext highlighter-rouge">Integer x = 1996</code> não chama <code class="language-plaintext highlighter-rouge">new Integer(1996)</code>, chama <code class="language-plaintext highlighter-rouge">Integer.valueOf(1996)</code>. E o <code class="language-plaintext highlighter-rouge">valueOf</code> tem um cache de instâncias prontas, de -128 até 127. Dentro dessa faixa você recebe sempre o mesmo objeto de volta. Fora dela, um objeto novo a cada chamada.</p>

<p>Fez sentido. Você anotou mentalmente que wrapper se compara com <code class="language-plaintext highlighter-rouge">equals</code>, fechou a aba e seguiu a vida.</p>

<p>E é fácil descartar esse caso. Boxing é escolha sua: usa <code class="language-plaintext highlighter-rouge">int</code>, para de comparar wrapper com <code class="language-plaintext highlighter-rouge">==</code>, e o problema evapora. Pegadinha de entrevista, curiosidade de laboratório.</p>

<p>Mas e quando não dá pra usar primitivo?</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">LocalDate</span> <span class="n">d1</span> <span class="o">=</span> <span class="nc">LocalDate</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">1996</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="mi">23</span><span class="o">);</span>
<span class="nc">LocalDate</span> <span class="n">d2</span> <span class="o">=</span> <span class="n">d1</span><span class="o">.</span><span class="na">plusYears</span><span class="o">(</span><span class="mi">30</span><span class="o">);</span>      <span class="c1">// 2026-01-23</span>
<span class="nc">LocalDate</span> <span class="n">d3</span> <span class="o">=</span> <span class="n">d2</span><span class="o">.</span><span class="na">minusYears</span><span class="o">(</span><span class="mi">30</span><span class="o">);</span>     <span class="c1">// 1996-01-23</span>

<span class="n">d1</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">d3</span><span class="o">)</span>   <span class="c1">// true</span>
<span class="n">d1</span> <span class="o">==</span> <span class="n">d3</span>        <span class="c1">// false</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">LocalDate</code> não tem literal. Não tem versão primitiva. Se você precisa de uma data, você é obrigado a usar objeto.</p>

<p>E aqui não tem cache nenhum na história pra culpar. <code class="language-plaintext highlighter-rouge">d1</code> e <code class="language-plaintext highlighter-rouge">d3</code> são a mesma data, com o mesmo ano, o mesmo mês e o mesmo dia. O <code class="language-plaintext highlighter-rouge">equals</code> concorda. O <code class="language-plaintext highlighter-rouge">==</code> diz que não.</p>

<p>Então “usa <code class="language-plaintext highlighter-rouge">equals</code>” é remédio, não diagnóstico. A pergunta que ninguém respondeu é por que <code class="language-plaintext highlighter-rouge">==</code> compara endereço de memória em primeiro lugar.</p>

<p>E ela tem resposta. No dia 31 de julho de 2026 um commit de <strong>208 mil linhas</strong> entrou no OpenJDK e mudou essa resposta pela primeira vez desde o Java 1.0.</p>

<h2 id="identidade-é-conseguir-distinguir-duas-coisas-idênticas">Identidade é conseguir distinguir duas coisas idênticas</h2>

<p>Antes do Java, pensa em duas coisas do mundo físico.</p>

<p>Ninguém liga pra qual cédula de cinquenta reais você recebeu de troco. Se eu trocar a sua por outra igual enquanto você não olha, não aconteceu nada. Duas notas de cinquenta são intercambiáveis.</p>

<p>Agora o seu carro. Você empresta pro vizinho e quer AQUELE carro de volta, não um igualzinho.</p>

<p>A diferença entre os dois casos é o que a gente chama de <strong>identidade</strong>: a capacidade de distinguir duas coisas que têm exatamente o mesmo conteúdo.</p>

<p>E repara numa coisa. Identidade só serve pra coisa que muda.</p>

<p>O carro importa individualmente porque ele acumula história: quilometragem, amassado, tanque vazio. Se dois carros fossem congelados e nunca mudassem, tanto faria qual você recebe de volta. Vira nota de cinquenta.</p>

<p>Daí sai a frase de onde vem todo o resto: <strong>identidade é uma capacidade que só dado mutável usa.</strong></p>

<h2 id="você-já-conhece-isso-por-outro-nome">Você já conhece isso por outro nome</h2>

<p>Se você faz DDD, essa distinção não é novidade nenhuma. É <strong>Entity versus Value Object</strong>, uma ideia que o Ward Cunningham já descrevia em 1994 e que o <a href="https://martinfowler.com/eaaCatalog/valueObject.html">Fowler catalogou</a> antes do Evans colocar ela no centro da modelagem.</p>

<p>Entity é o carro. O pedido <code class="language-plaintext highlighter-rouge">#4712</code> continua sendo o mesmo pedido depois de mudar de status três vezes.</p>

<p>Value Object é a nota de cinquenta. <code class="language-plaintext highlighter-rouge">R$ 50,00</code> é <code class="language-plaintext highlighter-rouge">R$ 50,00</code>, um <code class="language-plaintext highlighter-rouge">CPF</code> é definido pelos onze dígitos.</p>

<p>Só que em DDD isso sempre foi disciplina de design, e nada mais. Você escrevia <code class="language-plaintext highlighter-rouge">Money</code> imutável, sem setter, com <code class="language-plaintext highlighter-rouge">equals</code> na mão, e documentava que aquilo era um value object. O time entendia. O compilador não. A JVM muito menos.</p>

<p>Em runtime, o seu value object era uma entity igual a todas as outras: endereço próprio, header, identidade, e <code class="language-plaintext highlighter-rouge">==</code> mentindo pra você. Você desenhava a distinção no diagrama de domínio e pagava o preço cheio na memória.</p>

<p><strong>É essa distância que o JEP 401 fecha.</strong> Pela primeira vez <code class="language-plaintext highlighter-rouge">value</code> é uma palavra que o compilador lê, e não um comentário na documentação.</p>

<h2 id="o-java-achou-que-tudo-era-carro">O Java achou que tudo era carro</h2>

<p>E aqui está a decisão que o Java tomou: <strong>todo objeto tem identidade</strong>. Sem exceção, desde 1995.</p>

<p>Foi uma escolha de design, e na época ela era razoável, porque objeto em Java nasceu mutável por padrão.</p>

<p>O problema é que ela virou uma lei da física da linguagem. E lei da física tem consequências que caem por gravidade, querendo você ou não.</p>

<p><strong><code class="language-plaintext highlighter-rouge">==</code> compara endereço</strong> porque o endereço <em>é</em> a identidade. Dois objetos distintos precisam morar em lugares distintos, senão não dá pra distinguir.</p>

<p><strong>Todo objeto carrega um header</strong>, e ele existe porque identidade precisa morar em algum lugar. É nele que ficam o estado de lock e o identity hash.</p>

<p>Não precisa acreditar em mim. O <a href="https://github.com/openjdk/jol">JOL</a>, de <em>Java Object Layout</em>, é uma ferramenta do OpenJDK que lê o layout que a JVM de verdade escolheu pro objeto, campo por campo, em vez de estimar. Passei ela num <code class="language-plaintext highlighter-rouge">LocalDate</code> do JDK 28:</p>

<p><img src="https://codesilva.com/assets/images/java-object-header-anatomy.png" alt="Anatomia de um objeto LocalDate na memória, com as faixas desenhadas em escala. No topo, uma faixa escura e alta rotulada header, de 8 bytes, com a anotação lock state mais identity hash. Abaixo, três faixas azuis agrupadas por uma chave rotulada your data: y igual a 1996 com 4 bytes, m igual a 1 com 1 byte e d igual a 23 com 1 byte. Por último, uma faixa hachurada de padding com 2 bytes. Embaixo, o total: 16 bytes." /></p>

<p>Dezesseis bytes de objeto pra carregar seis bytes de data. O header sozinho é maior que o dado, e ainda sobram dois bytes de padding.</p>

<p>E essa já é a versão magra. No JDK 28 o HotSpot liga os <em>compact object headers</em> por padrão, que dobram o ponteiro de classe pra dentro da mark word. Rodando com <code class="language-plaintext highlighter-rouge">-XX:-UseCompactObjectHeaders</code>, o mesmo <code class="language-plaintext highlighter-rouge">LocalDate</code> volta a ocupar 24 bytes. A JVM já vinha brigando com esse custo por outro caminho, e mesmo depois de encolher o header ele continua sendo o maior pedaço do objeto.</p>

<p>Guarda essa figura, porque o header volta no fim do post. É nele que mora o lock, e é por isso que <code class="language-plaintext highlighter-rouge">synchronized</code> vai deixar de funcionar.</p>

<p><strong>Array de objeto é array de ponteiro.</strong> Se cada elemento precisa de endereço próprio, o array não consegue guardar os dados: ele guarda o caminho até eles.</p>

<p>Compara um <code class="language-plaintext highlighter-rouge">int[5]</code> com um <code class="language-plaintext highlighter-rouge">LocalDate[5]</code>, que é o exemplo que o próprio JEP usa:</p>

<p><img src="https://codesilva.com/assets/images/java-int-array-vs-localdate-array.png" alt="Comparação de layout de memória. À esquerda, int de cinco posições: um único bloco contíguo com os valores 1996, 2006, 1996, 1 e 23, marcado como contiguous. À direita, LocalDate de cinco posições: um bloco de células onde cada uma guarda uma seta apontando pra fora, e as setas se cruzam até objetos soltos e espalhados, cada um com uma faixa escura de header no topo e os campos y, m e d embaixo. Marcado como pointers, scattered." /></p>

<p>O array de <code class="language-plaintext highlighter-rouge">int</code> é um bloco só. O de <code class="language-plaintext highlighter-rouge">LocalDate</code> não guarda datas, guarda ponteiros, e cada objeto foi parar onde o alocador achou espaço, carregando o próprio header junto. Percorrer isso é uma sequência de saltos de memória com cache miss em cada um.</p>

<p>Medindo os dois com o JOL: 32 bytes contra 80. Duas vezes e meia mais memória pra representar a mesma coisa.</p>

<p>Tudo pra carregar um <code class="language-plaintext highlighter-rouge">int</code> e dois <code class="language-plaintext highlighter-rouge">byte</code>s de informação útil por data.</p>

<h2 id="o-cache-do-integer-é-uma-gambiarra-pra-não-pagar-identidade">O cache do Integer é uma gambiarra pra não pagar identidade</h2>

<p>Agora dá pra entender de onde veio o susto do começo.</p>

<p>Identidade custa. Cada <code class="language-plaintext highlighter-rouge">new</code> é uma alocação, um header, um endereço, e mais um objeto pro GC visitar depois. Como boxing de <code class="language-plaintext highlighter-rouge">int</code> acontece o tempo todo, alguém decidiu que valia a pena reaproveitar as instâncias mais comuns em vez de criar objeto novo toda vez.</p>

<p>Daí o cache de -128 a 127.</p>

<p>O detalhe é que reaproveitar instância significa reaproveitar identidade. E identidade é observável por <code class="language-plaintext highlighter-rouge">==</code>.</p>

<p>Ou seja: <strong>a pegadinha do <code class="language-plaintext highlighter-rouge">Integer</code> é o modelo aparecendo.</strong> Uma otimização de alocação vazou pra semântica da linguagem, e só pôde vazar porque <code class="language-plaintext highlighter-rouge">==</code> fala de identidade em vez de valor.</p>

<p>Você não estava confuso. O modelo é que estava estranho. Eu já escrevi sobre <a href="/carreira/2025/05/05/perguntaram-me-porque-java-e-dificil.html">por que Java parece difícil</a>, e boa parte da resposta é essa: a linguagem cobra que você entenda decisões antigas que ninguém te conta.</p>

<h2 id="mas-dado-imutável-nunca-precisou-disso">Mas dado imutável nunca precisou disso</h2>

<p><code class="language-plaintext highlighter-rouge">LocalDate</code> é imutável. <code class="language-plaintext highlighter-rouge">Integer</code> é imutável. <code class="language-plaintext highlighter-rouge">Optional</code>, <code class="language-plaintext highlighter-rouge">Duration</code>, <code class="language-plaintext highlighter-rouge">BigDecimal</code>, o <code class="language-plaintext highlighter-rouge">Money</code> que você escreveu semana passada.</p>

<p>Nenhum deles é carro. São todos nota de cinquenta.</p>

<p>Você nunca, em nenhum código que já escreveu, precisou saber <em>qual</em> instância de 23 de janeiro de 1996 você tem na mão. Só que a JVM não tinha como saber disso, então ela cobrava identidade de todo mundo, no preço cheio, por garantia.</p>

<p>É isso que o JEP 401 conserta. Ele te dá uma saída.</p>

<h2 id="o-jep-401-deixa-você-abrir-mão-da-identidade">O JEP 401 deixa você abrir mão da identidade</h2>

<p>O commit <a href="https://github.com/openjdk/jdk/commit/cc278dbb8a1ca0754d5842708b9029441055d361"><code class="language-plaintext highlighter-rouge">cc278dbb</code></a> implementa dois JEPs de uma vez, os dois como preview no JDK 28: o <a href="https://openjdk.org/jeps/401">JEP 401 (Value Objects)</a> e o <a href="https://openjdk.org/jeps/539">JEP 539 (Strict Field Initialization)</a>. São 208.011 linhas adicionadas, 13.161 removidas, 300 arquivos, 64 co-autores e 14 revisores. É a maior entrega do Project Valhalla até hoje.</p>

<p>E a API disso é uma palavra:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">jshell</span><span class="o">&gt;</span> <span class="n">value</span> <span class="kd">record</span> <span class="nf">Point</span><span class="o">(</span><span class="kt">int</span> <span class="n">x</span><span class="o">,</span> <span class="kt">int</span> <span class="n">y</span><span class="o">)</span> <span class="o">{}</span>
<span class="o">|</span>  <span class="n">created</span> <span class="kd">record</span> <span class="nc">Point</span>

<span class="n">jshell</span><span class="o">&gt;</span> <span class="nc">Point</span> <span class="n">p</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Point</span><span class="o">(</span><span class="mi">17</span><span class="o">,</span> <span class="mi">3</span><span class="o">)</span>
<span class="n">p</span> <span class="o">==&gt;</span> <span class="nc">Point</span><span class="o">[</span><span class="n">x</span><span class="o">=</span><span class="mi">17</span><span class="o">,</span> <span class="n">y</span><span class="o">=</span><span class="mi">3</span><span class="o">]</span>

<span class="n">jshell</span><span class="o">&gt;</span> <span class="nc">Objects</span><span class="o">.</span><span class="na">hasIdentity</span><span class="o">(</span><span class="n">p</span><span class="o">)</span>
<span class="err">$</span><span class="mi">3</span> <span class="o">==&gt;</span> <span class="kc">false</span>

<span class="n">jshell</span><span class="o">&gt;</span> <span class="k">new</span> <span class="nf">Point</span><span class="o">(</span><span class="mi">17</span><span class="o">,</span> <span class="mi">3</span><span class="o">)</span> <span class="o">==</span> <span class="n">p</span>
<span class="err">$</span><span class="mi">4</span> <span class="o">==&gt;</span> <span class="kc">true</span>
</code></pre></div></div>

<p>O modificador <code class="language-plaintext highlighter-rouge">value</code> traz três coisas de graça. Os campos viram <code class="language-plaintext highlighter-rouge">final</code>, a classe vira <code class="language-plaintext highlighter-rouge">final</code>, e <code class="language-plaintext highlighter-rouge">==</code> passa a comparar campo a campo.</p>

<p>Os termos são parecidos e significam coisas diferentes:</p>

<ul>
  <li><strong>value class</strong> é o que você declara, com o modificador</li>
  <li><strong>value object</strong> é a instância dela, o objeto sem identidade</li>
  <li><strong>Value Objects</strong> é o nome da feature, e <strong>Project Valhalla</strong> é o guarda-chuva</li>
</ul>

<p>E a plataforma já migrou 30 classes:</p>

<table>
  <thead>
    <tr>
      <th>Pacote</th>
      <th>Classes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">java.lang</code></td>
      <td><code class="language-plaintext highlighter-rouge">Integer</code>, <code class="language-plaintext highlighter-rouge">Long</code>, <code class="language-plaintext highlighter-rouge">Float</code>, <code class="language-plaintext highlighter-rouge">Double</code>, <code class="language-plaintext highlighter-rouge">Byte</code>, <code class="language-plaintext highlighter-rouge">Short</code>, <code class="language-plaintext highlighter-rouge">Character</code>, <code class="language-plaintext highlighter-rouge">Boolean</code>, <code class="language-plaintext highlighter-rouge">Number</code>, <code class="language-plaintext highlighter-rouge">Record</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">java.util</code></td>
      <td><code class="language-plaintext highlighter-rouge">Optional</code>, <code class="language-plaintext highlighter-rouge">OptionalInt</code>, <code class="language-plaintext highlighter-rouge">OptionalLong</code>, <code class="language-plaintext highlighter-rouge">OptionalDouble</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">java.time</code></td>
      <td><code class="language-plaintext highlighter-rouge">LocalDate</code>, <code class="language-plaintext highlighter-rouge">LocalTime</code>, <code class="language-plaintext highlighter-rouge">LocalDateTime</code>, <code class="language-plaintext highlighter-rouge">ZonedDateTime</code>, <code class="language-plaintext highlighter-rouge">OffsetTime</code>, <code class="language-plaintext highlighter-rouge">OffsetDateTime</code>, <code class="language-plaintext highlighter-rouge">Duration</code>, <code class="language-plaintext highlighter-rouge">Instant</code>, <code class="language-plaintext highlighter-rouge">Period</code>, <code class="language-plaintext highlighter-rouge">Year</code>, <code class="language-plaintext highlighter-rouge">YearMonth</code>, <code class="language-plaintext highlighter-rouge">MonthDay</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">java.time.chrono</code></td>
      <td><code class="language-plaintext highlighter-rouge">MinguoDate</code>, <code class="language-plaintext highlighter-rouge">HijrahDate</code>, <code class="language-plaintext highlighter-rouge">JapaneseDate</code>, <code class="language-plaintext highlighter-rouge">ThaiBuddhistDate</code></td>
    </tr>
  </tbody>
</table>

<p>Que é o que responde o susto do começo. Output de verdade, rodado num build com o JEP 401 ligado:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Integer 1996 == 1996      : true
d1 == d3                  : true
Objects.hasIdentity(d1)   : false
Objects.hasIdentity("abcd"): true
</code></pre></div></div>

<p>Segura essa última linha, ela volta no fim do post.</p>

<h2 id="escrevendo-as-suas-próprias">Escrevendo as suas próprias</h2>

<p>Se o seu dado já é um record, é uma palavra:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">value</span> <span class="kd">record</span> <span class="nf">Point</span><span class="o">(</span><span class="kt">int</span> <span class="n">x</span><span class="o">,</span> <span class="kt">int</span> <span class="n">y</span><span class="o">)</span> <span class="o">{}</span>
</code></pre></div></div>

<p>Record é <strong>transparente</strong>: os campos são exatamente os componentes do construtor. Quando você guarda o estado de um jeito e expõe de outro, tipo dinheiro como um <code class="language-plaintext highlighter-rouge">long</code> de centavos, aí é <code class="language-plaintext highlighter-rouge">value class</code> normal:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">value</span> <span class="kd">class</span> <span class="nc">EURCurrency</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kt">long</span> <span class="n">cs</span><span class="o">;</span>  <span class="c1">// implicitamente final</span>
    <span class="kd">public</span> <span class="nf">EURCurrency</span><span class="o">(</span><span class="kt">long</span> <span class="n">e</span><span class="o">,</span> <span class="kt">int</span> <span class="n">c</span><span class="o">)</span> <span class="o">{</span> <span class="n">cs</span> <span class="o">=</span> <span class="n">e</span> <span class="o">*</span> <span class="mi">100</span> <span class="o">+</span> <span class="n">c</span><span class="o">;</span> <span class="o">}</span>
    <span class="kd">public</span> <span class="kt">long</span> <span class="nf">euros</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="n">cs</span> <span class="o">/</span> <span class="mi">100</span><span class="o">;</span> <span class="o">}</span>
    <span class="kd">public</span> <span class="kt">int</span> <span class="nf">cents</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">cs</span> <span class="o">%</span> <span class="mi">100</span><span class="o">;</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>O <code class="language-plaintext highlighter-rouge">value</code> fecha portas, e o compilador é direto sobre quais:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>error: cannot assign a value to final variable x
error: cannot inherit from final V
error: The concrete class Base is not allowed to be a super class
       of the value class E either directly or indirectly
</code></pre></div></div>

<p>Campo vira <code class="language-plaintext highlighter-rouge">final</code>, classe vira <code class="language-plaintext highlighter-rouge">final</code>, e herdar de uma classe com identidade seria herdar identidade junto. Hierarquia você ainda tem: dá pra implementar interface, e dá pra estender uma <code class="language-plaintext highlighter-rouge">abstract value class</code>, que é como <code class="language-plaintext highlighter-rouge">Integer</code> e <code class="language-plaintext highlighter-rouge">BigInteger</code> convivem hoje debaixo de <code class="language-plaintext highlighter-rouge">Number</code>.</p>

<p>A regra que mais pega gente é a do construtor. Value object precisa estar completo antes de qualquer um ver ele, então o corpo inteiro roda antes do <code class="language-plaintext highlighter-rouge">super()</code>, e ali <code class="language-plaintext highlighter-rouge">this</code> não existe:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">value</span> <span class="kd">class</span> <span class="nc">Name</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">name</span><span class="o">;</span>
    <span class="kt">int</span> <span class="n">length</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="nf">strLength</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="n">name</span><span class="o">.</span><span class="na">length</span><span class="o">();</span> <span class="o">}</span>

    <span class="nc">Name</span><span class="o">(</span><span class="nc">String</span> <span class="n">n</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">name</span> <span class="o">=</span> <span class="n">n</span><span class="o">;</span>
        <span class="n">length</span> <span class="o">=</span> <span class="n">strLength</span><span class="o">();</span>   <span class="c1">// error: reference to strLength() may only</span>
    <span class="o">}</span>                           <span class="c1">// appear after an explicit constructor invocation</span>
<span class="o">}</span>
</code></pre></div></div>

<p>A saída é tornar o método <code class="language-plaintext highlighter-rouge">static</code>, ou chamar <code class="language-plaintext highlighter-rouge">super()</code> na mão depois de setar todos os campos. E com preview ligado isso vale pra <strong>todos</strong> os records, value ou não, então record que usa <code class="language-plaintext highlighter-rouge">this</code> no construtor canônico para de compilar.</p>

<p>Pra saber o que marcar, a regra é curta: estado imutável e você nunca precisa distinguir duas instâncias com o mesmo conteúdo. Se você modela por DDD, <strong>a sua pasta de value objects é o primeiro lugar pra olhar</strong>. Fica fora o que é mutável, o que serve de lock e o que guarda dado sensível, já que <code class="language-plaintext highlighter-rouge">==</code> compara campo privado.</p>

<h2 id="sem-identidade-a-jvm-não-precisa-mais-dar-endereço">Sem identidade, a JVM não precisa mais dar endereço</h2>

<p>Volta na lista de consequências lá de cima e inverte cada uma.</p>

<p>Se o objeto não tem identidade, ele não precisa ser distinguível. Se não precisa ser distinguível, <strong>não precisa de endereço próprio</strong>. E aí a JVM ganha duas liberdades.</p>

<p><strong>Flattening</strong> é jogar os campos direto pra dentro do array ou do campo que referencia o objeto:</p>

<p><img src="https://codesilva.com/assets/images/java-value-objects-flattening.png" alt="Diagrama de antes e depois. À esquerda, BEFORE: um array cujas células apontam com setas pra objetos soltos e espalhados, cada um com faixa escura de header e os campos y, m e d. Uma seta grande aponta pra direita. À direita, AFTER: um único bloco contíguo de cinco linhas, cada linha com os valores 1, 1996, 01 e 23 escritos direto dentro dela, sem seta nenhuma e sem header. Marcado como flattened." /></p>

<p>Zero ponteiro, zero header, tudo contíguo, com o primeiro bit dizendo se a referência é <code class="language-plaintext highlighter-rouge">null</code>. O JEP diz que esse array pode passar a ter características de performance parecidas com as de um <code class="language-plaintext highlighter-rouge">int[]</code>.</p>

<p>Isso é o que o JEP descreve. Eu quis ver acontecendo.</p>

<p>Aloquei um <code class="language-plaintext highlighter-rouge">LocalDate[]</code> de dois milhões de posições, todas com datas diferentes, e medi a heap. Mesmo programa, mesmo JDK, mesma máquina, mudando só o <code class="language-plaintext highlighter-rouge">--enable-preview</code>:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>bytes por elemento</th>
      <th>total</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>sem preview, <code class="language-plaintext highlighter-rouge">LocalDate</code> é identity</td>
      <td>28,5</td>
      <td>56,9 MB</td>
    </tr>
    <tr>
      <td>com preview, <code class="language-plaintext highlighter-rouge">LocalDate</code> é value</td>
      <td><strong>8,4</strong></td>
      <td><strong>16,8 MB</strong></td>
    </tr>
  </tbody>
</table>

<p>Oito bytes por elemento, que é exatamente a palavra de 64 bits que o JEP previu.</p>

<p>E não precisa confiar na minha medição de heap pra aceitar isso, porque a aritmética fecha sozinha: se cada elemento ainda fosse um ponteiro pra um objeto separado, só os objetos já ocupariam dois milhões vezes 16 bytes, que é o tamanho mínimo de um objeto na heap. Dá 32 MB. Não cabe em 16,8.</p>

<p>Os objetos não estão lá. Só os valores.</p>

<p>Agora, uma ressalva que vale mais que a medição: <strong>isso não está na spec.</strong> O JEP 401 não tem sequer uma seção de Specification, e diz com todas as letras que flattening e scalarization são “optimizations, not language features”, feitas a critério da JVM. Garantir layout de memória é não-objetivo declarado.</p>

<p>O que o JEP garante é semântica. Value object não tem identidade, <code class="language-plaintext highlighter-rouge">==</code> compara campos, sincronizar lança exceção. Isso é contrato.</p>

<p>O array contíguo é permissão, não promessa. O JEP tira o que impedia a JVM de achatar, e cada implementação decide se acha. Eu mostrei uma que achatou.</p>

<p><strong>Scalarization</strong> é o passo seguinte, dentro do JIT. Quando o objeto está numa variável local ou num parâmetro, ele é decomposto em valores soltos. O <code class="language-plaintext highlighter-rouge">plusYears</code> compilado para de receber um ponteiro e passa a receber <code class="language-plaintext highlighter-rouge">(boolean isNull, int year, byte month, byte day)</code>, devolvendo outra tupla igual.</p>

<p>O objeto simplesmente nunca existe na memória.</p>

<p>Escape analysis já fazia algo parecido com objeto comum, mas basta um caminho de código comparar identidade pra otimização evaporar. Com value class a garantia é estática, e ela atravessa fronteira de método.</p>

<h2 id="menos-alocação-é-menos-gc">Menos alocação é menos GC</h2>

<p>Aqui é onde isso encosta no seu Grafana.</p>

<p>Cada objeto que a JVM não aloca é um objeto que o GC não precisa marcar, varrer nem mover. Um <code class="language-plaintext highlighter-rouge">LocalDate[]</code> de um milhão de posições deixa de ser um milhão de objetos vivos na heap e passa a ser um bloco de memória.</p>

<p>O laço que você escreveu sem pensar, criando um <code class="language-plaintext highlighter-rouge">LocalDate</code> por iteração pra jogar fora logo em seguida, para de gerar lixo. Não é que o GC ficou mais rápido: é que não tem mais o que coletar. E dado contíguo ainda é dado que o CPU busca com menos cache miss, o que costuma valer mais que o tempo de alocação em si.</p>

<p>E como não é promessa, tem jeito de não acontecer. Três coisas atrapalham na prática:</p>

<ul>
  <li><strong>Campo mutável tem teto de 64 bits</strong>, porque leitura e escrita precisam ser atômicas. Um <code class="language-plaintext highlighter-rouge">LocalDateTime</code> não cabe e volta a ser ponteiro.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Object</code> mata o flattening.</strong> <code class="language-plaintext highlighter-rouge">Integer[]</code> é achatável, <code class="language-plaintext highlighter-rouge">Object[]</code> não é, e genérico apagado cai no mesmo caso. Não muda semântica, só layout.</li>
  <li><strong>Código antigo precisa recompilar</strong>, porque a JVM depende de um atributo novo no class file pra saber a tempo que a classe é value class.</li>
</ul>

<h2 id="o-que-você-perde-é-exatamente-o-que-dependia-de-identidade">O que você perde é exatamente o que dependia de identidade</h2>

<p>E o preço tem uma lógica por trás: é a mesma decisão de design cobrando na saída o que ela cobrava na entrada. Tudo que quebra é coisa que precisava distinguir instância. Rodei cada um pra pegar a mensagem real:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>synchronized via Object : java.lang.IdentityException:
                          Cannot synchronize on an instance of value class java.time.LocalDate
d.notify()              : java.lang.IllegalMonitorStateException: java.time.LocalDate
new WeakReference&lt;&gt;(d)  : java.lang.IdentityException:
                          java.time.LocalDate is not an identity class
weakHashMap.put(d, "x") : java.lang.IdentityException:
                          java.time.LocalDate is not an identity class
</code></pre></div></div>

<p>Lock mora no header, naquela mark word lá do começo, então <code class="language-plaintext highlighter-rouge">synchronized</code> deixa de funcionar. O <em>Java Concurrency in Practice</em> inteiro parte do princípio de que qualquer objeto serve de lock, e agora não serve mais. Junto com ele caem <code class="language-plaintext highlighter-rouge">wait</code> e <code class="language-plaintext highlighter-rouge">notify</code>, que dependem desse mesmo lock, e também o <code class="language-plaintext highlighter-rouge">WeakHashMap</code> e o <code class="language-plaintext highlighter-rouge">java.lang.ref</code> inteiro, porque referência fraca precisa apontar pra uma instância específica.</p>

<p>Fora isso, tem o que continua funcionando mas diferente do que você espera.</p>

<p>O <code class="language-plaintext highlighter-rouge">==</code> agora compara os campos internos, então ele pode divergir do seu <code class="language-plaintext highlighter-rouge">equals</code>, que talvez olhe outra coisa. Virou também uma operação com custo, porque a comparação é recursiva e árvore profunda de value objects chega a estourar <code class="language-plaintext highlighter-rouge">StackOverflowError</code>. E como ele lê campo privado, virou um canal de inferência. O JEP avisa na lata: value object não foi feito pra proteger dado sensível.</p>

<p>E até o <code class="language-plaintext highlighter-rouge">==</code> de identity object ficou um tiquinho mais caro, porque o bytecode <code class="language-plaintext highlighter-rouge">if_acmpeq</code> agora precisa de um teste extra pra detectar value object. O caminho de identidade virou fast path, mas ele existe, e é cobrado do código que não usa nada disso.</p>

<h2 id="a-peça-que-faltava-jep-539">A peça que faltava: JEP 539</h2>

<p>Ainda tinha um buraco. Value object promete que o valor nunca muda, mas em Java um campo pode ser lido <strong>antes</strong> de ser inicializado, valendo <code class="language-plaintext highlighter-rouge">0</code> ou <code class="language-plaintext highlighter-rouge">null</code>.</p>

<p>O exemplo do JEP é uma dependência circular:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">App</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">long</span> <span class="n">appID</span> <span class="o">=</span> <span class="nc">Log</span><span class="o">.</span><span class="na">currentPID</span><span class="o">();</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">()</span> <span class="o">{</span>
        <span class="no">IO</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"App["</span> <span class="o">+</span> <span class="n">appID</span> <span class="o">+</span> <span class="s">"] has started"</span><span class="o">);</span>
        <span class="nc">Log</span><span class="o">.</span><span class="na">log</span><span class="o">(</span><span class="s">"Completed 'main'"</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">class</span> <span class="nc">Log</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="n">prefix</span> <span class="o">=</span> <span class="s">"App["</span> <span class="o">+</span> <span class="nc">App</span><span class="o">.</span><span class="na">appID</span> <span class="o">+</span> <span class="s">"]: "</span><span class="o">;</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">log</span><span class="o">(</span><span class="nc">String</span> <span class="n">msg</span><span class="o">)</span> <span class="o">{</span> <span class="no">IO</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">prefix</span> <span class="o">+</span> <span class="n">msg</span><span class="o">);</span> <span class="o">}</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">long</span> <span class="nf">currentPID</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="nc">ProcessHandle</span><span class="o">.</span><span class="na">current</span><span class="o">().</span><span class="na">pid</span><span class="o">();</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Rodando, sai isso:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>App[8145] has started
App[0]: Completed 'main'
</code></pre></div></div>

<p>Duas leituras do mesmo campo <code class="language-plaintext highlighter-rouge">final</code>, dois valores diferentes. <code class="language-plaintext highlighter-rouge">Log</code> é inicializada no meio da inicialização de <code class="language-plaintext highlighter-rouge">App</code>, lê <code class="language-plaintext highlighter-rouge">appID</code> valendo o default <code class="language-plaintext highlighter-rouge">0</code>, e cola o zero no <code class="language-plaintext highlighter-rouge">prefix</code>. E repara na sacanagem: se <code class="language-plaintext highlighter-rouge">Log</code> fosse inicializada primeiro, o bug sumia. É o tipo de bug que desaparece quando você vai investigar.</p>

<p>Um campo <code class="language-plaintext highlighter-rouge">final</code> que dá dois valores diferentes destrói a premissa inteira de value object. Por isso o JEP 539 cria o flag <code class="language-plaintext highlighter-rouge">ACC_STRICT_INIT</code>: campo marcado assim não tem valor default e precisa ser escrito antes de qualquer leitura. O <code class="language-plaintext highlighter-rouge">javac</code> marca <strong>todos</strong> os campos de value class com ele, e é por isso que os dois JEPs entraram no mesmo commit.</p>

<p>Se for rodar o exemplo, não espere ele consertar sozinho: eu liguei o <code class="language-plaintext highlighter-rouge">--enable-preview</code> e o <code class="language-plaintext highlighter-rouge">App[0]</code> continua lá. Impor inicialização estrita ao código que já existe é não-objetivo declarado do JEP 539, então só campo de value class recebe o flag.</p>

<h2 id="testando-hoje">Testando hoje</h2>

<p>Aqui tem uma pegadinha de logística, e eu só descobri porque fui rodar.</p>

<p>O caminho óbvio é pegar o early-access do JDK 28 em <a href="https://jdk.java.net/28/">jdk.java.net/28</a>. <strong>Não funciona ainda.</strong> O build 9 saiu em 31/07/2026, mesmo dia da integração, e foi cortado antes dela entrar: <code class="language-plaintext highlighter-rouge">value record</code> dá erro de sintaxe, <code class="language-plaintext highlighter-rouge">Objects.hasIdentity</code> não existe, e o <code class="language-plaintext highlighter-rouge">Integer 1996 == 1996</code> continua <code class="language-plaintext highlighter-rouge">false</code>.</p>

<p>O que roda hoje é o early-access do próprio Valhalla, em <a href="https://jdk.java.net/valhalla/">jdk.java.net/valhalla</a>. O build <code class="language-plaintext highlighter-rouge">27-jep401ea3+1-1</code> implementa o JEP 401, e foi nele que eu rodei tudo que tem output neste post.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>javac <span class="nt">--release</span> 27 <span class="nt">--enable-preview</span> Demo.java
java <span class="nt">--enable-preview</span> Demo
</code></pre></div></div>

<p>Preview precisa estar ligado dos dois lados, e não dá pra escolher a versão identity do <code class="language-plaintext highlighter-rouge">LocalDate</code> nesse modo: ou é tudo, ou é nada.</p>

<h2 id="o-que-não-mudou">O que não mudou</h2>

<p>Duas coisas ficaram para trás, e as duas são engraçadas.</p>

<p><strong>O cache do <code class="language-plaintext highlighter-rouge">Integer</code> continua existindo.</strong> O <a href="https://github.com/openjdk/jdk/blob/cc278dbb8a1ca0754d5842708b9029441055d361/doc/value-class-preview.md"><code class="language-plaintext highlighter-rouge">doc/value-class-preview.md</code></a> que veio no commit conta que ele foi mantido de propósito, por performance, e que agora não tem mais impacto semântico nenhum. A gambiarra que criou a pegadinha continua rodando embaixo do capô. Você é que não consegue mais enxergar ela.</p>

<p><strong>E <code class="language-plaintext highlighter-rouge">String</code> não migrou.</strong> A classe tem dependências de identidade na API e na implementação, então <code class="language-plaintext highlighter-rouge">Objects.hasIdentity("abcd")</code> continua devolvendo <code class="language-plaintext highlighter-rouge">true</code>. A pegadinha mais famosa do Java, <code class="language-plaintext highlighter-rouge">==</code> em <code class="language-plaintext highlighter-rouge">String</code>, segue de pé.</p>

<p>Fora isso, o resto é fundação. O JEP 402 vai melhorar o boxing de primitivos em cima disso, e o JEP 218 vai deixar genérico especializar layout quando parametrizado com value class, que é o <code class="language-plaintext highlighter-rouge">List&lt;int&gt;</code> sem boxing.</p>

<p>Mas a mudança grande já aconteceu, e ela é conceitual antes de ser técnica.</p>

<p><code class="language-plaintext highlighter-rouge">==</code> deixou de perguntar “vocês dois moram no mesmo endereço?” e passou a perguntar “dá pra distinguir vocês dois?”. Pra carro, a resposta continua sendo o endereço. Pra nota de cinquenta, agora é o valor.</p>

<p>Aquele susto que você levou lá no começo nunca teve como resposta “usa <code class="language-plaintext highlighter-rouge">equals</code>”, e muito menos “usa primitivo”. Pro <code class="language-plaintext highlighter-rouge">LocalDate</code> nunca existiu primitivo pra usar.</p>

<p>A resposta era que aquela data nunca precisou de identidade, e que você vinha pagando por ela desde sempre.</p>

<p>Por hoje é só.</p>

<hr />

<h2 id="referências">Referências</h2>

<ul>
  <li><a href="https://openjdk.org/jeps/401">JEP 401: Value Objects</a> e <a href="https://openjdk.org/jeps/539">JEP 539: Strict Field Initialization</a>, os dois Integrated no JDK 28</li>
  <li><a href="https://github.com/openjdk/jdk/commit/cc278dbb8a1ca0754d5842708b9029441055d361">Commit <code class="language-plaintext highlighter-rouge">cc278dbb</code></a>, de 31/07/2026, e o <a href="https://github.com/openjdk/jdk/blob/cc278dbb8a1ca0754d5842708b9029441055d361/doc/value-class-preview.md"><code class="language-plaintext highlighter-rouge">doc/value-class-preview.md</code></a> que veio junto</li>
  <li><a href="https://jdk.java.net/valhalla/">EA do Valhalla</a>, build <code class="language-plaintext highlighter-rouge">27-jep401ea3+1-1</code>, usado nos testes deste post</li>
  <li><a href="https://stackoverflow.com/questions/1700081/why-is-128-128-false-but-127-127-is-true-when-comparing-integer-wrappers-in-ja">A pergunta original no Stack Overflow</a>, de 2009</li>
</ul>]]></content><author><name></name></author><category term="programacao" /><category term="java" /><category term="jvm" /><category term="value objects" /><category term="valhalla" /><category term="performance" /><category term="low-level" /><summary type="html"><![CDATA[Em algum momento da sua vida com Java você escreveu isso e levou um susto:]]></summary></entry><entry xml:lang="en-US"><title type="html">In Java, equals was always the medicine. Nobody told you the disease</title><link href="https://codesilva.com/programacao/2026/08/02/in-java-equals-was-always-the-medicine-nobody-told-you-the-disease.html" rel="alternate" type="text/html" title="In Java, equals was always the medicine. Nobody told you the disease" /><published>2026-08-02T00:00:00+00:00</published><updated>2026-08-02T00:00:00+00:00</updated><id>https://codesilva.com/programacao/2026/08/02/in-java-equals-was-always-the-medicine-nobody-told-you-the-disease</id><content type="html" xml:base="https://codesilva.com/programacao/2026/08/02/in-java-equals-was-always-the-medicine-nobody-told-you-the-disease.html"><![CDATA[<p>At some point in your Java career you wrote this and did a double take:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Integer</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">1</span><span class="o">,</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">1</span><span class="o">;</span>
<span class="n">i</span> <span class="o">==</span> <span class="n">j</span>        <span class="c1">// true</span>

<span class="nc">Integer</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">1996</span><span class="o">,</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">1996</span><span class="o">;</span>
<span class="n">x</span> <span class="o">==</span> <span class="n">y</span>        <span class="c1">// false</span>
</code></pre></div></div>

<p>So you did what everyone does: you searched for it.</p>

<p>You found the answer fast, because <a href="https://stackoverflow.com/questions/1700081/why-is-128-128-false-but-127-127-is-true-when-comparing-integer-wrappers-in-ja">the question has been on Stack Overflow since November 2009</a> with a couple hundred votes. The explanation is always the same: <code class="language-plaintext highlighter-rouge">Integer x = 1996</code> doesn’t call <code class="language-plaintext highlighter-rouge">new Integer(1996)</code>, it calls <code class="language-plaintext highlighter-rouge">Integer.valueOf(1996)</code>. And <code class="language-plaintext highlighter-rouge">valueOf</code> keeps a cache of ready-made instances from -128 to 127. Inside that range you get the same object back every time. Outside it, a fresh object per call.</p>

<p>Fair enough. You filed away “compare wrappers with <code class="language-plaintext highlighter-rouge">equals</code>”, closed the tab, and moved on.</p>

<p>And it’s easy to dismiss this case. Boxing is your choice: use <code class="language-plaintext highlighter-rouge">int</code>, stop comparing wrappers with <code class="language-plaintext highlighter-rouge">==</code>, and the problem evaporates. Interview trivia, lab curiosity.</p>

<p>But what happens when there’s no primitive to fall back on?</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">LocalDate</span> <span class="n">d1</span> <span class="o">=</span> <span class="nc">LocalDate</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">1996</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="mi">23</span><span class="o">);</span>
<span class="nc">LocalDate</span> <span class="n">d2</span> <span class="o">=</span> <span class="n">d1</span><span class="o">.</span><span class="na">plusYears</span><span class="o">(</span><span class="mi">30</span><span class="o">);</span>      <span class="c1">// 2026-01-23</span>
<span class="nc">LocalDate</span> <span class="n">d3</span> <span class="o">=</span> <span class="n">d2</span><span class="o">.</span><span class="na">minusYears</span><span class="o">(</span><span class="mi">30</span><span class="o">);</span>     <span class="c1">// 1996-01-23</span>

<span class="n">d1</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">d3</span><span class="o">)</span>   <span class="c1">// true</span>
<span class="n">d1</span> <span class="o">==</span> <span class="n">d3</span>        <span class="c1">// false</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">LocalDate</code> has no literal syntax. It has no primitive version. If you need a date, you’re using an object.</p>

<p>And there’s no cache in this story to blame. <code class="language-plaintext highlighter-rouge">d1</code> and <code class="language-plaintext highlighter-rouge">d3</code> are the same date, same year, same month, same day. <code class="language-plaintext highlighter-rouge">equals</code> agrees. <code class="language-plaintext highlighter-rouge">==</code> doesn’t.</p>

<p>So “use <code class="language-plaintext highlighter-rouge">equals</code>” is the medicine, not the diagnosis. The question nobody answered is why <code class="language-plaintext highlighter-rouge">==</code> compares memory addresses in the first place.</p>

<p>It has an answer. On July 31st, 2026, a commit of <strong>208 thousand lines</strong> landed in the OpenJDK and changed that answer for the first time since Java 1.0.</p>

<h2 id="identity-is-being-able-to-tell-two-identical-things-apart">Identity is being able to tell two identical things apart</h2>

<p>Before Java, think about two things in the physical world.</p>

<p>Nobody cares which twenty-dollar bill they got as change. If I swap yours for another one while you look away, nothing happened. Two twenties are interchangeable.</p>

<p>Now your car. You lend it to a neighbor and you want THAT car back, not an identical one.</p>

<p>The difference between those two cases is what we call <strong>identity</strong>: the ability to distinguish two things that hold exactly the same content.</p>

<p>And here’s the thing. Identity is only useful for things that change.</p>

<p>The car matters individually because it accumulates history: mileage, a dent, an empty tank. If two cars were frozen and never changed, it wouldn’t matter which one you got back. It becomes a twenty-dollar bill.</p>

<p>That gives you the one sentence everything else follows from: <strong>identity is a capability only mutable data uses.</strong></p>

<h2 id="you-already-know-this-by-another-name">You already know this by another name</h2>

<p>If you do DDD, this distinction is nothing new. It’s <strong>Entity versus Value Object</strong>, an idea Ward Cunningham was already describing in 1994 and that <a href="https://martinfowler.com/eaaCatalog/valueObject.html">Fowler catalogued</a> before Evans put it at the center of modeling.</p>

<p>Entity is the car. Order <code class="language-plaintext highlighter-rouge">#4712</code> is still the same order after changing status three times.</p>

<p>Value Object is the twenty-dollar bill. <code class="language-plaintext highlighter-rouge">$50.00</code> is <code class="language-plaintext highlighter-rouge">$50.00</code>, and an <code class="language-plaintext highlighter-rouge">EmailAddress</code> is the string it carries.</p>

<p>Except in DDD this was always design discipline and nothing more. You wrote <code class="language-plaintext highlighter-rouge">Money</code> immutable, no setters, <code class="language-plaintext highlighter-rouge">equals</code> by hand, and documented that it was a value object. The team understood. The compiler didn’t. The JVM even less.</p>

<p>At runtime, your value object was an entity like every other one: its own address, a header, an identity, and <code class="language-plaintext highlighter-rouge">==</code> lying to you. You drew the distinction in the domain diagram and paid full price in memory.</p>

<p><strong>That gap is what JEP 401 closes.</strong> For the first time <code class="language-plaintext highlighter-rouge">value</code> is a word the compiler reads, not a comment in the documentation.</p>

<h2 id="java-assumed-everything-was-a-car">Java assumed everything was a car</h2>

<p>Here’s the decision Java made: <strong>every object has identity</strong>. No exceptions, since 1995.</p>

<p>It was a design choice, and it was reasonable at the time, because objects in Java were born mutable by default.</p>

<p>The problem is that it became a law of physics for the language. And laws of physics have consequences that fall out by gravity, whether you want them or not.</p>

<p><strong><code class="language-plaintext highlighter-rouge">==</code> compares addresses</strong> because the address <em>is</em> the identity. Two distinct objects have to live in distinct places, otherwise you can’t tell them apart.</p>

<p><strong>Every object carries a header</strong>, and it exists because identity needs somewhere to live. That’s where lock state and the identity hash sit.</p>

<p>You don’t have to take my word for it. <a href="https://github.com/openjdk/jol">JOL</a>, short for Java Object Layout, is an OpenJDK tool that reads the layout the JVM actually chose for an object, field by field, instead of estimating it. I ran it against a <code class="language-plaintext highlighter-rouge">LocalDate</code> on JDK 28:</p>

<p><img src="https://codesilva.com/assets/images/java-object-header-anatomy.png" alt="Anatomy of a LocalDate object in memory, bands drawn to scale. On top, a tall dark band labelled header, 8 bytes, annotated lock state plus identity hash. Below it, three blue bands grouped by a brace labelled your data: y equals 1996 at 4 bytes, m equals 1 at 1 byte, and d equals 23 at 1 byte. Last, a hatched padding band at 2 bytes. At the bottom, the total: 16 bytes." /></p>

<p>Sixteen bytes of object to carry six bytes of date. The header alone is bigger than the data, and there are still two bytes of padding on top.</p>

<p>And that’s already the slim version. On JDK 28 HotSpot enables <em>compact object headers</em> by default, folding the class pointer into the mark word. Run with <code class="language-plaintext highlighter-rouge">-XX:-UseCompactObjectHeaders</code> and the same <code class="language-plaintext highlighter-rouge">LocalDate</code> goes back to 24 bytes. The JVM was already fighting this cost from another angle, and even after shrinking the header it remains the largest piece of the object.</p>

<p>Hold on to that figure, because the header comes back at the end of this post. The lock lives in it, and that’s why <code class="language-plaintext highlighter-rouge">synchronized</code> is about to stop working.</p>

<p><strong>An array of objects is an array of pointers.</strong> If every element needs its own address, the array can’t hold the data. It holds the path to it.</p>

<p>Compare an <code class="language-plaintext highlighter-rouge">int[5]</code> with a <code class="language-plaintext highlighter-rouge">LocalDate[5]</code>, which is the example the JEP itself uses:</p>

<p><img src="https://codesilva.com/assets/images/java-int-array-vs-localdate-array.png" alt="Memory layout comparison. On the left, an int array of five slots: one contiguous block holding the values 1996, 2006, 1996, 1 and 23, marked contiguous. On the right, a LocalDate array of five slots: a block of cells where each one holds an arrow pointing outward, the arrows crossing over to loose scattered objects, each with a dark header band on top and the fields y, m and d below. Marked pointers, scattered." /></p>

<p>The <code class="language-plaintext highlighter-rouge">int</code> array is a single block. The <code class="language-plaintext highlighter-rouge">LocalDate</code> one doesn’t hold dates, it holds pointers, and each object ended up wherever the allocator found room, carrying its own header along. Walking that array is a sequence of memory jumps with a cache miss on each one.</p>

<p>Measured with JOL: 32 bytes against 80. Two and a half times the memory to represent the same thing.</p>

<p>All of it to carry an <code class="language-plaintext highlighter-rouge">int</code> and two <code class="language-plaintext highlighter-rouge">byte</code>s of useful information per date.</p>

<h2 id="the-integer-cache-is-a-workaround-for-not-paying-identity">The Integer cache is a workaround for not paying identity</h2>

<p>Now the surprise from the beginning makes sense.</p>

<p>Identity costs. Every <code class="language-plaintext highlighter-rouge">new</code> is an allocation, a header, an address, and one more object for the GC to visit later. Since boxing <code class="language-plaintext highlighter-rouge">int</code> happens constantly, somebody decided it was worth reusing the most common instances instead of creating a new object every time.</p>

<p>Hence the cache from -128 to 127.</p>

<p>The catch is that reusing an instance means reusing an identity. And identity is observable through <code class="language-plaintext highlighter-rouge">==</code>.</p>

<p>In other words: <strong>the <code class="language-plaintext highlighter-rouge">Integer</code> gotcha is the model showing through.</strong> An allocation optimization leaked into the semantics of the language, and it could only leak because <code class="language-plaintext highlighter-rouge">==</code> talks about identity instead of value.</p>

<p>You weren’t confused. The model was strange. I’ve written before about <a href="/carreira/2025/05/05/perguntaram-me-porque-java-e-dificil.html">why Java feels hard</a> (in Portuguese), and a good part of the answer is exactly this: the language asks you to understand old decisions nobody tells you about.</p>

<h2 id="but-immutable-data-never-needed-it">But immutable data never needed it</h2>

<p><code class="language-plaintext highlighter-rouge">LocalDate</code> is immutable. <code class="language-plaintext highlighter-rouge">Integer</code> is immutable. So are <code class="language-plaintext highlighter-rouge">Optional</code>, <code class="language-plaintext highlighter-rouge">Duration</code>, <code class="language-plaintext highlighter-rouge">BigDecimal</code>, and the <code class="language-plaintext highlighter-rouge">Money</code> class you wrote last week.</p>

<p>None of them is a car. They’re all twenty-dollar bills.</p>

<p>You’ve never, in any code you’ve written, needed to know <em>which</em> instance of January 23rd, 1996 you were holding. The JVM had no way to know that, so it charged identity to everyone, at full price, as a guarantee.</p>

<p>That’s what JEP 401 fixes. It gives you a way out.</p>

<h2 id="jep-401-lets-you-opt-out-of-identity">JEP 401 lets you opt out of identity</h2>

<p>Commit <a href="https://github.com/openjdk/jdk/commit/cc278dbb8a1ca0754d5842708b9029441055d361"><code class="language-plaintext highlighter-rouge">cc278dbb</code></a> implements two JEPs at once, both as preview in JDK 28: <a href="https://openjdk.org/jeps/401">JEP 401 (Value Objects)</a> and <a href="https://openjdk.org/jeps/539">JEP 539 (Strict Field Initialization)</a>. That’s 208,011 lines added, 13,161 removed, 300 files, 64 co-authors and 14 reviewers. It’s the largest Project Valhalla delivery so far.</p>

<p>And the API for it’s one word:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">jshell</span><span class="o">&gt;</span> <span class="n">value</span> <span class="kd">record</span> <span class="nf">Point</span><span class="o">(</span><span class="kt">int</span> <span class="n">x</span><span class="o">,</span> <span class="kt">int</span> <span class="n">y</span><span class="o">)</span> <span class="o">{}</span>
<span class="o">|</span>  <span class="n">created</span> <span class="kd">record</span> <span class="nc">Point</span>

<span class="n">jshell</span><span class="o">&gt;</span> <span class="nc">Point</span> <span class="n">p</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Point</span><span class="o">(</span><span class="mi">17</span><span class="o">,</span> <span class="mi">3</span><span class="o">)</span>
<span class="n">p</span> <span class="o">==&gt;</span> <span class="nc">Point</span><span class="o">[</span><span class="n">x</span><span class="o">=</span><span class="mi">17</span><span class="o">,</span> <span class="n">y</span><span class="o">=</span><span class="mi">3</span><span class="o">]</span>

<span class="n">jshell</span><span class="o">&gt;</span> <span class="nc">Objects</span><span class="o">.</span><span class="na">hasIdentity</span><span class="o">(</span><span class="n">p</span><span class="o">)</span>
<span class="err">$</span><span class="mi">3</span> <span class="o">==&gt;</span> <span class="kc">false</span>

<span class="n">jshell</span><span class="o">&gt;</span> <span class="k">new</span> <span class="nf">Point</span><span class="o">(</span><span class="mi">17</span><span class="o">,</span> <span class="mi">3</span><span class="o">)</span> <span class="o">==</span> <span class="n">p</span>
<span class="err">$</span><span class="mi">4</span> <span class="o">==&gt;</span> <span class="kc">true</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">value</code> modifier gives you three things for free. Fields become <code class="language-plaintext highlighter-rouge">final</code>, the class becomes <code class="language-plaintext highlighter-rouge">final</code>, and <code class="language-plaintext highlighter-rouge">==</code> starts comparing field by field.</p>

<p>The terms look alike and mean different things:</p>

<ul>
  <li><strong>value class</strong> is what you declare, with the modifier</li>
  <li><strong>value object</strong> is an instance of it, the object without identity</li>
  <li><strong>Value Objects</strong> is the name of the feature, and <strong>Project Valhalla</strong> is the umbrella</li>
</ul>

<p>The platform has already migrated 30 classes:</p>

<table>
  <thead>
    <tr>
      <th>Package</th>
      <th>Classes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">java.lang</code></td>
      <td><code class="language-plaintext highlighter-rouge">Integer</code>, <code class="language-plaintext highlighter-rouge">Long</code>, <code class="language-plaintext highlighter-rouge">Float</code>, <code class="language-plaintext highlighter-rouge">Double</code>, <code class="language-plaintext highlighter-rouge">Byte</code>, <code class="language-plaintext highlighter-rouge">Short</code>, <code class="language-plaintext highlighter-rouge">Character</code>, <code class="language-plaintext highlighter-rouge">Boolean</code>, <code class="language-plaintext highlighter-rouge">Number</code>, <code class="language-plaintext highlighter-rouge">Record</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">java.util</code></td>
      <td><code class="language-plaintext highlighter-rouge">Optional</code>, <code class="language-plaintext highlighter-rouge">OptionalInt</code>, <code class="language-plaintext highlighter-rouge">OptionalLong</code>, <code class="language-plaintext highlighter-rouge">OptionalDouble</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">java.time</code></td>
      <td><code class="language-plaintext highlighter-rouge">LocalDate</code>, <code class="language-plaintext highlighter-rouge">LocalTime</code>, <code class="language-plaintext highlighter-rouge">LocalDateTime</code>, <code class="language-plaintext highlighter-rouge">ZonedDateTime</code>, <code class="language-plaintext highlighter-rouge">OffsetTime</code>, <code class="language-plaintext highlighter-rouge">OffsetDateTime</code>, <code class="language-plaintext highlighter-rouge">Duration</code>, <code class="language-plaintext highlighter-rouge">Instant</code>, <code class="language-plaintext highlighter-rouge">Period</code>, <code class="language-plaintext highlighter-rouge">Year</code>, <code class="language-plaintext highlighter-rouge">YearMonth</code>, <code class="language-plaintext highlighter-rouge">MonthDay</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">java.time.chrono</code></td>
      <td><code class="language-plaintext highlighter-rouge">MinguoDate</code>, <code class="language-plaintext highlighter-rouge">HijrahDate</code>, <code class="language-plaintext highlighter-rouge">JapaneseDate</code>, <code class="language-plaintext highlighter-rouge">ThaiBuddhistDate</code></td>
    </tr>
  </tbody>
</table>

<p>Which answers the surprise from the opening. Real output, run on a build with JEP 401 enabled:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Integer 1996 == 1996      : true
d1 == d3                  : true
Objects.hasIdentity(d1)   : false
Objects.hasIdentity("abcd"): true
</code></pre></div></div>

<p>Keep an eye on that last line. It comes back at the end.</p>

<h2 id="writing-your-own">Writing your own</h2>

<p>If your data is already a record, it’s one word:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">value</span> <span class="kd">record</span> <span class="nf">Point</span><span class="o">(</span><span class="kt">int</span> <span class="n">x</span><span class="o">,</span> <span class="kt">int</span> <span class="n">y</span><span class="o">)</span> <span class="o">{}</span>
</code></pre></div></div>

<p>A record is <strong>transparent</strong>: its fields are exactly the constructor components. When you store state one way and expose it another, say money as a <code class="language-plaintext highlighter-rouge">long</code> of cents, you need a plain <code class="language-plaintext highlighter-rouge">value class</code>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">value</span> <span class="kd">class</span> <span class="nc">EURCurrency</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kt">long</span> <span class="n">cs</span><span class="o">;</span>  <span class="c1">// implicitly final</span>
    <span class="kd">public</span> <span class="nf">EURCurrency</span><span class="o">(</span><span class="kt">long</span> <span class="n">e</span><span class="o">,</span> <span class="kt">int</span> <span class="n">c</span><span class="o">)</span> <span class="o">{</span> <span class="n">cs</span> <span class="o">=</span> <span class="n">e</span> <span class="o">*</span> <span class="mi">100</span> <span class="o">+</span> <span class="n">c</span><span class="o">;</span> <span class="o">}</span>
    <span class="kd">public</span> <span class="kt">long</span> <span class="nf">euros</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="n">cs</span> <span class="o">/</span> <span class="mi">100</span><span class="o">;</span> <span class="o">}</span>
    <span class="kd">public</span> <span class="kt">int</span> <span class="nf">cents</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">cs</span> <span class="o">%</span> <span class="mi">100</span><span class="o">;</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">value</code> modifier closes doors, and the compiler is direct about which ones:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>error: cannot assign a value to final variable x
error: cannot inherit from final V
error: The concrete class Base is not allowed to be a super class
       of the value class E either directly or indirectly
</code></pre></div></div>

<p>Fields become <code class="language-plaintext highlighter-rouge">final</code>, the class becomes <code class="language-plaintext highlighter-rouge">final</code>, and inheriting from a class with identity would mean inheriting the identity along with it. You still get hierarchy: you can implement interfaces, and you can extend an <code class="language-plaintext highlighter-rouge">abstract value class</code>, which is how <code class="language-plaintext highlighter-rouge">Integer</code> and <code class="language-plaintext highlighter-rouge">BigInteger</code> now coexist under <code class="language-plaintext highlighter-rouge">Number</code>.</p>

<p>The rule that catches most people is the constructor one. A value object has to be complete before anyone can observe it, so the entire body runs before <code class="language-plaintext highlighter-rouge">super()</code>, and <code class="language-plaintext highlighter-rouge">this</code> doesn’t exist there yet:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">value</span> <span class="kd">class</span> <span class="nc">Name</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">name</span><span class="o">;</span>
    <span class="kt">int</span> <span class="n">length</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="nf">strLength</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="n">name</span><span class="o">.</span><span class="na">length</span><span class="o">();</span> <span class="o">}</span>

    <span class="nc">Name</span><span class="o">(</span><span class="nc">String</span> <span class="n">n</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">name</span> <span class="o">=</span> <span class="n">n</span><span class="o">;</span>
        <span class="n">length</span> <span class="o">=</span> <span class="n">strLength</span><span class="o">();</span>   <span class="c1">// error: reference to strLength() may only</span>
    <span class="o">}</span>                           <span class="c1">// appear after an explicit constructor invocation</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The way out is making the method <code class="language-plaintext highlighter-rouge">static</code>, or calling <code class="language-plaintext highlighter-rouge">super()</code> by hand after setting every field. And with preview enabled this applies to <strong>all</strong> records, value or not, so a record that uses <code class="language-plaintext highlighter-rouge">this</code> in its canonical constructor stops compiling.</p>

<p>As for what to mark, the rule is short: immutable state, and you never need to distinguish two instances holding the same content. If you model with DDD, <strong>your value objects package is the first place to look</strong>. What stays out is anything mutable, anything used as a lock, and anything holding sensitive data, since <code class="language-plaintext highlighter-rouge">==</code> compares private fields.</p>

<h2 id="without-identity-the-jvm-no-longer-needs-to-hand-out-addresses">Without identity, the JVM no longer needs to hand out addresses</h2>

<p>Go back to the list of consequences above and invert each one.</p>

<p>If the object has no identity, it doesn’t need to be distinguishable. If it doesn’t need to be distinguishable, <strong>it doesn’t need its own address</strong>. Which buys the JVM two freedoms.</p>

<p><strong>Flattening</strong> means putting the fields directly inside the array or the field that references the object:</p>

<p><img src="https://codesilva.com/assets/images/java-value-objects-flattening.png" alt="Before and after diagram. On the left, BEFORE: an array whose cells point with arrows to loose scattered objects, each with a dark header band and the fields y, m and d. A large arrow points right. On the right, AFTER: a single contiguous block of five rows, each row holding the values 1, 1996, 01 and 23 written directly inside it, with no arrows and no headers. Marked flattened." /></p>

<p>No pointers, no headers, all contiguous, with the first bit saying whether the reference is <code class="language-plaintext highlighter-rouge">null</code>. The JEP says this array may end up with performance characteristics similar to an <code class="language-plaintext highlighter-rouge">int[]</code>.</p>

<p>That’s what the JEP describes. I wanted to watch it happen.</p>

<p>I allocated a <code class="language-plaintext highlighter-rouge">LocalDate[]</code> of two million slots, all distinct dates, and measured the heap. Same program, same JDK, same machine, changing only <code class="language-plaintext highlighter-rouge">--enable-preview</code>:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>bytes per element</th>
      <th>total</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>no preview, <code class="language-plaintext highlighter-rouge">LocalDate</code> is identity</td>
      <td>28.5</td>
      <td>56.9 MB</td>
    </tr>
    <tr>
      <td>with preview, <code class="language-plaintext highlighter-rouge">LocalDate</code> is value</td>
      <td><strong>8.4</strong></td>
      <td><strong>16.8 MB</strong></td>
    </tr>
  </tbody>
</table>

<p>Eight bytes per element, which is exactly the 64-bit word the JEP predicted.</p>

<p>And you don’t need to trust my heap measurement to accept it, because the arithmetic closes on its own: if each element were still a pointer to a separate object, the objects alone would take two million times 16 bytes, the minimum size of an object on the heap. That’s 32 MB. It doesn’t fit in 16.8.</p>

<p>The objects aren’t there. Only the values.</p>

<p>Now, a caveat worth more than the measurement: <strong>none of this is in the spec.</strong> JEP 401 doesn’t even have a Specification section, and it states outright that flattening and scalarization are “optimizations, not language features”, done at the discretion of the JVM. Guaranteeing memory layout is a declared non-goal.</p>

<p>What the JEP guarantees is semantics. A value object has no identity, <code class="language-plaintext highlighter-rouge">==</code> compares fields, synchronizing throws. That’s the contract.</p>

<p>The contiguous array is permission, not a promise. The JEP removes what was stopping the JVM from flattening, and each implementation decides whether it does. I showed you one that did.</p>

<p><strong>Scalarization</strong> is the next step, inside the JIT. When the object sits in a local variable or a parameter, it gets decomposed into loose values. The compiled <code class="language-plaintext highlighter-rouge">plusYears</code> stops taking a pointer and starts taking <code class="language-plaintext highlighter-rouge">(boolean isNull, int year, byte month, byte day)</code>, returning another tuple like it.</p>

<p>The object simply never exists in memory.</p>

<p>Escape analysis already did something similar for ordinary objects, but a single code path comparing identity makes the optimization evaporate. With a value class the guarantee is static, and it crosses method boundaries.</p>

<h2 id="less-allocation-is-less-gc">Less allocation is less GC</h2>

<p>This is where it shows up in your Grafana.</p>

<p>Every object the JVM doesn’t allocate is an object the GC doesn’t have to mark, sweep or move. A <code class="language-plaintext highlighter-rouge">LocalDate[]</code> of a million slots stops being a million live objects on the heap and becomes a block of memory.</p>

<p>That loop you wrote without thinking, creating a <code class="language-plaintext highlighter-rouge">LocalDate</code> per iteration only to throw it away, stops generating garbage. It isn’t that the GC got faster: there’s nothing left to collect. And contiguous data is still data the CPU fetches with fewer cache misses, which usually matters more than the allocation time itself.</p>

<p>And since it isn’t a promise, there are ways for it not to happen. Three things get in the way in practice:</p>

<ul>
  <li><strong>A mutable field has a 64-bit ceiling</strong>, because reads and writes need to be atomic. A <code class="language-plaintext highlighter-rouge">LocalDateTime</code> doesn’t fit and goes back to being a pointer.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Object</code> kills flattening.</strong> <code class="language-plaintext highlighter-rouge">Integer[]</code> is flattenable, <code class="language-plaintext highlighter-rouge">Object[]</code> isn’t, and erased generics fall in the same bucket. It changes no semantics, only layout.</li>
  <li><strong>Old code needs recompiling</strong>, because the JVM relies on a new class file attribute to learn in time that a class is a value class.</li>
</ul>

<h2 id="what-you-lose-is-exactly-what-depended-on-identity">What you lose is exactly what depended on identity</h2>

<p>And there’s a logic to the price: the same design decision charging you on the way out what it charged on the way in. Everything that breaks is something that needed to distinguish instances. I ran each one to get the real message:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>synchronized via Object : java.lang.IdentityException:
                          Cannot synchronize on an instance of value class java.time.LocalDate
d.notify()              : java.lang.IllegalMonitorStateException: java.time.LocalDate
new WeakReference&lt;&gt;(d)  : java.lang.IdentityException:
                          java.time.LocalDate is not an identity class
weakHashMap.put(d, "x") : java.lang.IdentityException:
                          java.time.LocalDate is not an identity class
</code></pre></div></div>

<p>The lock lives in the header, in that mark word from earlier, so <code class="language-plaintext highlighter-rouge">synchronized</code> stops working. All of <em>Java Concurrency in Practice</em> assumes any object can serve as a lock, and now it can’t. <code class="language-plaintext highlighter-rouge">wait</code> and <code class="language-plaintext highlighter-rouge">notify</code> fall with it, since they depend on that same lock, and so do <code class="language-plaintext highlighter-rouge">WeakHashMap</code> and all of <code class="language-plaintext highlighter-rouge">java.lang.ref</code>, because a weak reference needs to point at one specific instance.</p>

<p>Beyond that, some things keep working but not the way you expect.</p>

<p><code class="language-plaintext highlighter-rouge">==</code> now compares internal fields, so it can diverge from your <code class="language-plaintext highlighter-rouge">equals</code>, which might look at something else. It also became an operation with a cost, because the comparison is recursive and a deep tree of value objects can hit <code class="language-plaintext highlighter-rouge">StackOverflowError</code>. And since it reads private fields, it became an inference channel. The JEP says it plainly: value objects weren’t designed to protect sensitive data.</p>

<p>Even <code class="language-plaintext highlighter-rouge">==</code> on identity objects got marginally more expensive, because the <code class="language-plaintext highlighter-rouge">if_acmpeq</code> bytecode now needs an extra test to detect value objects. The identity path became a fast path, but it exists, and it’s charged to code that uses none of this.</p>

<h2 id="the-missing-piece-jep-539">The missing piece: JEP 539</h2>

<p>There was still a hole. A value object promises its value never changes, but in Java a field can be read <strong>before</strong> it’s initialized, holding <code class="language-plaintext highlighter-rouge">0</code> or <code class="language-plaintext highlighter-rouge">null</code>.</p>

<p>The JEP’s example is a circular dependency:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">App</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">long</span> <span class="n">appID</span> <span class="o">=</span> <span class="nc">Log</span><span class="o">.</span><span class="na">currentPID</span><span class="o">();</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">()</span> <span class="o">{</span>
        <span class="no">IO</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"App["</span> <span class="o">+</span> <span class="n">appID</span> <span class="o">+</span> <span class="s">"] has started"</span><span class="o">);</span>
        <span class="nc">Log</span><span class="o">.</span><span class="na">log</span><span class="o">(</span><span class="s">"Completed 'main'"</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">class</span> <span class="nc">Log</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="n">prefix</span> <span class="o">=</span> <span class="s">"App["</span> <span class="o">+</span> <span class="nc">App</span><span class="o">.</span><span class="na">appID</span> <span class="o">+</span> <span class="s">"]: "</span><span class="o">;</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">log</span><span class="o">(</span><span class="nc">String</span> <span class="n">msg</span><span class="o">)</span> <span class="o">{</span> <span class="no">IO</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">prefix</span> <span class="o">+</span> <span class="n">msg</span><span class="o">);</span> <span class="o">}</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">long</span> <span class="nf">currentPID</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="nc">ProcessHandle</span><span class="o">.</span><span class="na">current</span><span class="o">().</span><span class="na">pid</span><span class="o">();</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Running it prints:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>App[8145] has started
App[0]: Completed 'main'
</code></pre></div></div>

<p>Two reads of the same <code class="language-plaintext highlighter-rouge">final</code> field, two different values. <code class="language-plaintext highlighter-rouge">Log</code> gets initialized in the middle of initializing <code class="language-plaintext highlighter-rouge">App</code>, reads <code class="language-plaintext highlighter-rouge">appID</code> holding the default <code class="language-plaintext highlighter-rouge">0</code>, and bakes that zero into <code class="language-plaintext highlighter-rouge">prefix</code>. And here’s the nasty part: if <code class="language-plaintext highlighter-rouge">Log</code> were initialized first, the bug would disappear. It’s the kind of bug that vanishes when you go looking for it.</p>

<p>A <code class="language-plaintext highlighter-rouge">final</code> field that yields two different values destroys the entire premise of a value object. That’s why JEP 539 introduces the <code class="language-plaintext highlighter-rouge">ACC_STRICT_INIT</code> flag: a field marked with it has no default value and must be written before any read. <code class="language-plaintext highlighter-rouge">javac</code> marks <strong>every</strong> field of a value class with it, which is why both JEPs landed in the same commit.</p>

<p>If you go run that example, don’t expect it to fix itself: I turned on <code class="language-plaintext highlighter-rouge">--enable-preview</code> and <code class="language-plaintext highlighter-rouge">App[0]</code> is still there. Imposing strict initialization on existing code is a declared non-goal of JEP 539, so only value class fields get the flag.</p>

<h2 id="trying-it-today">Trying it today</h2>

<p>There’s a logistics trap here, and I only found it because I went and ran things.</p>

<p>The obvious path is grabbing the JDK 28 early access from <a href="https://jdk.java.net/28/">jdk.java.net/28</a>. <strong>It doesn’t work yet.</strong> Build 9 shipped on July 31st, 2026, the same day as the integration, and it was cut before that landed: <code class="language-plaintext highlighter-rouge">value record</code> is a syntax error, <code class="language-plaintext highlighter-rouge">Objects.hasIdentity</code> doesn’t exist, and <code class="language-plaintext highlighter-rouge">Integer 1996 == 1996</code> is still <code class="language-plaintext highlighter-rouge">false</code>.</p>

<p>What runs today is Valhalla’s own early access, at <a href="https://jdk.java.net/valhalla/">jdk.java.net/valhalla</a>. Build <code class="language-plaintext highlighter-rouge">27-jep401ea3+1-1</code> implements JEP 401, and it’s where I ran everything in this post that produces output.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>javac <span class="nt">--release</span> 27 <span class="nt">--enable-preview</span> Demo.java
java <span class="nt">--enable-preview</span> Demo
</code></pre></div></div>

<p>Preview has to be on at both ends, and you can’t pick the identity version of <code class="language-plaintext highlighter-rouge">LocalDate</code> in that mode: it’s all or nothing.</p>

<h2 id="what-didnt-change">What didn’t change</h2>

<p>Two things got left behind, and both are funny.</p>

<p><strong>The <code class="language-plaintext highlighter-rouge">Integer</code> cache still exists.</strong> The <a href="https://github.com/openjdk/jdk/blob/cc278dbb8a1ca0754d5842708b9029441055d361/doc/value-class-preview.md"><code class="language-plaintext highlighter-rouge">doc/value-class-preview.md</code></a> that shipped with the commit says it was kept on purpose, for performance, and that it now has no semantic impact whatsoever. The workaround that created the gotcha keeps running under the hood. You just can’t observe it anymore.</p>

<p><strong>And <code class="language-plaintext highlighter-rouge">String</code> didn’t migrate.</strong> The class has identity dependencies in its API and its implementation, so <code class="language-plaintext highlighter-rouge">Objects.hasIdentity("abcd")</code> still returns <code class="language-plaintext highlighter-rouge">true</code>. Java’s most famous gotcha, <code class="language-plaintext highlighter-rouge">==</code> on <code class="language-plaintext highlighter-rouge">String</code>, is still standing.</p>

<p>Past that, the rest is foundation. JEP 402 will improve primitive boxing on top of this, and JEP 218 will let generics specialize layout when parameterized with a value class, which is <code class="language-plaintext highlighter-rouge">List&lt;int&gt;</code> without boxing.</p>

<p>But the big change already happened, and it’s conceptual before it’s technical.</p>

<p><code class="language-plaintext highlighter-rouge">==</code> stopped asking “do you two live at the same address?” and started asking “can you two be told apart?”. For a car, the answer is still the address. For a twenty-dollar bill, it’s now the value.</p>

<p>That surprise you had at the beginning never had “use <code class="language-plaintext highlighter-rouge">equals</code>” as its answer, let alone “use a primitive”. For <code class="language-plaintext highlighter-rouge">LocalDate</code> there was never a primitive to use.</p>

<p>The answer was that the date never needed identity, and you had been paying for it all along.</p>

<p>Thanks for reading!</p>

<hr />

<h2 id="references">References</h2>

<ul>
  <li><a href="https://openjdk.org/jeps/401">JEP 401: Value Objects</a> and <a href="https://openjdk.org/jeps/539">JEP 539: Strict Field Initialization</a>, both Integrated in JDK 28</li>
  <li><a href="https://github.com/openjdk/jdk/commit/cc278dbb8a1ca0754d5842708b9029441055d361">Commit <code class="language-plaintext highlighter-rouge">cc278dbb</code></a>, July 31st, 2026, and the <a href="https://github.com/openjdk/jdk/blob/cc278dbb8a1ca0754d5842708b9029441055d361/doc/value-class-preview.md"><code class="language-plaintext highlighter-rouge">doc/value-class-preview.md</code></a> that shipped with it</li>
  <li><a href="https://jdk.java.net/valhalla/">Valhalla early access</a>, build <code class="language-plaintext highlighter-rouge">27-jep401ea3+1-1</code>, used for the measurements in this post</li>
  <li><a href="https://stackoverflow.com/questions/1700081/why-is-128-128-false-but-127-127-is-true-when-comparing-integer-wrappers-in-ja">The original Stack Overflow question</a>, from 2009</li>
</ul>]]></content><author><name></name></author><category term="programacao" /><category term="java" /><category term="jvm" /><category term="value objects" /><category term="valhalla" /><category term="performance" /><category term="low-level" /><summary type="html"><![CDATA[At some point in your Java career you wrote this and did a double take:]]></summary></entry></feed>