Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/superpowers/pendencias.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Rascunho feito a partir da seção 4.2 da verificação jurídica de 28/09/2026.
| Hard link em caminho de rede | O identificador vem do servidor e pode repetir entre discos dele. Em rede, o hard link soma mais de uma vez | Aceito. Reavaliar se aparecer caso real |
| Unidade de rede mapeada e desligada pode atrasar a lista de unidades | O `DriveInfo.IsReady` espera a rede responder | Aceito na fatia 1. Reavaliar se atrapalhar |
| Rótulo "sem acesso" repetido na linha da pasta | A coluna do valor e o rótulo dizem a mesma coisa | Fechado em 29/09/2026 na fatia 2: a pasta sem acesso e a não lida ficam sem rótulo |
| Memória acima da meta | A spec pede menos de 300 MB para 1 milhão de arquivos. No teste manual de 29/09/2026, num disco com pouco mais de 1 milhão de arquivos, o pico passou de 300 MB. O tempo ficou dentro da meta de 60 s. Suspeitos: o dicionário de identificadores de hard link, com uma entrada por arquivo, e os nomes guardados em cada arquivo | Reduzido na fatia 2 (buffer e listas reaproveitados, conjunto de identificadores em faixas, coletor em modo de economia): o pico caiu cerca de um terço. Na fatia 3, os nomes de arquivo repetidos passaram a ser guardados uma vez só, com mais uns 10% de ganho na medida. Segue um pouco acima de 300 MB num disco com pouco mais de 1 milhão de arquivos. Próximo passo, se fizer falta: guardar a data do arquivo em 4 bytes em vez de 8 e medir de novo com `ferramentas/medir-memoria.ps1` |
| Memória acima da meta | A spec pede menos de 300 MB para 1 milhão de arquivos. No teste manual de 29/09/2026, num disco com pouco mais de 1 milhão de arquivos, o pico passou de 300 MB. O tempo ficou dentro da meta de 60 s. Suspeitos: o dicionário de identificadores de hard link, com uma entrada por arquivo, e os nomes guardados em cada arquivo | Reduzido na fatia 2 (buffer e listas reaproveitados, conjunto de identificadores em faixas, coletor em modo de economia): o pico caiu cerca de um terço. Na fatia 3, os nomes de arquivo repetidos passaram a ser guardados uma vez só, com mais uns 10% de ganho na medida. Segue um pouco acima de 300 MB num disco com pouco mais de 1 milhão de arquivos. Fechado para a varredura em 01/10/2026, PR #18: o `.exe` deixou de ser comprimido, por decisão do Manfred. Comprimido, o .NET descomprimia as bibliotecas na memória ao abrir. Os conjuntos de nomes e de identificadores viraram tabelas compactas. Pela linha de comando, o pico caiu para 250 a 260 MB num disco com 1,1 milhão de arquivos. O `.exe` passou de 67 MB para 149 MB |
| Uma varredura do C: com menos arquivos | No teste manual de 29/09/2026, uma de seis varreduras do mesmo disco veio com cerca de 4% a menos de arquivos e de pastas. As outras cinco, pela janela e pela linha de comando, bateram entre si. Não se repetiu. O teste `Muitas_varreduras_seguidas_dao_sempre_o_mesmo_total` varre 30 vezes uma árvore de mais de mil pastas com 16 tarefas, e passou em 90 varreduras. Pode ter sido o disco mudando naquele minuto ou uma corrida rara no motor | Fechado em 29/09/2026 na fatia 2: ao terminar, a varredura conta as pastas sem leitura e avisa se houver alguma. Um teste de nome de arquivo que falhava às vezes foi achado e corrigido no caminho |

## Fatia 2: navegação, elevação e acertos da fatia 1
Expand Down Expand Up @@ -95,3 +95,9 @@ Rascunho feito a partir da seção 4.2 da verificação jurídica de 28/09/2026.
| Publicação da página | Publicar pede autorização. O roteiro do cPanel fica fora da pasta do projeto | Passo da versão 1.0.0, depois do teste de lançamento, com autorização |
| Teste do item do Explorer | Ligar grava no registro do Windows do Manfred. O agente testou só com as chaves em memória | Fechado: teste do Manfred no PR #17, em 01/10/2026 |
| Exportar pela janela | O diálogo de salvar não foi operado pelo agente. O relatório e o CSV foram conferidos pela linha de comando, que usa o mesmo código | Fechado: teste do Manfred no PR #17, em 01/10/2026 |

## Memória (depois da fatia 8)

| Item | Motivo | O que fecha |
|---|---|---|
| Memória da janela com 1 milhão de arquivos | A janela parada já usa cerca de 150 MB (WPF e fontes), e a árvore do disco ocupa cerca de 145 MB. Varrendo um disco com 1,1 milhão de arquivos, o pico ficou entre 390 e 430 MB, contra cerca de 480 MB com o `.exe` comprimido. O que sobra é lixo temporário da varredura e das análises, que o coletor junta antes de limpar. Um ajuste do coletor (`GCgen0MaxBudget`) baixa uns 40 MB, mas só vale por variável de ambiente: o programa não aceita esse ajuste na própria configuração | Aceito para a versão 1. Se fizer falta: criar menos objetos de vida curta nas análises (a extensão de cada arquivo vira um texto novo) e guardar a data do arquivo em 4 bytes |
79 changes: 73 additions & 6 deletions src/mapdisk.nucleo/varredura/conjunto-ids.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,89 @@
namespace MapDisk.Nucleo;

/// <summary>
/// Identificadores de arquivo já contados, para o hard link somar uma vez. Dividido em faixas
/// com trava própria: ocupa menos memória que um dicionário concorrente e aguenta as tarefas
/// da varredura ao mesmo tempo.
/// Identificadores de arquivo já contados, para o hard link somar uma vez. Recebe um
/// identificador por arquivo da unidade, então cada byte conta: em vez de HashSet, que gasta
/// cerca de 20 bytes por item, cada faixa é uma tabela aberta só de números, com 8 bytes por
/// posição. As faixas têm trava própria e aguentam as tarefas da varredura ao mesmo tempo.
/// </summary>
internal sealed class ConjuntoIds
{
private const int Faixas = 64;

private readonly HashSet<long>[] _faixas = Enumerable.Range(0, Faixas).Select(_ => new HashSet<long>()).ToArray();
private readonly Faixa[] _faixas = Enumerable.Range(0, Faixas).Select(_ => new Faixa()).ToArray();
private int _zero;

public bool Acrescentar(long id)
{
var faixa = _faixas[(int)((ulong)id % Faixas)];
// O zero marca posição vazia na tabela, então fica numa marca à parte.
if (id == 0)
{
return Interlocked.Exchange(ref _zero, 1) == 0;
}

var mistura = Misturar(id);
var faixa = _faixas[(int)(mistura % Faixas)];
lock (faixa)
{
return faixa.Add(id);
return faixa.Acrescentar(id, mistura / Faixas);
}
}

// Espalha os identificadores, que no NTFS vêm em sequência, pelas faixas e posições.
private static ulong Misturar(long id)
{
var x = (ulong)id;
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9UL;
x = (x ^ (x >> 27)) * 0x94D049BB133111EBUL;
return x ^ (x >> 31);
}

private sealed class Faixa
{
private long[] _tabela = new long[64];
private int _quantos;

public bool Acrescentar(long id, ulong mistura)
{
var mascara = _tabela.Length - 1;
var i = (int)(mistura & (ulong)mascara);
while (_tabela[i] != 0)
{
if (_tabela[i] == id)
{
return false;
}

i = (i + 1) & mascara;
}

_tabela[i] = id;
if (++_quantos * 4 > _tabela.Length * 3)
{
Crescer();
}

return true;
}

private void Crescer()
{
var antiga = _tabela;
_tabela = new long[antiga.Length * 2];
var mascara = _tabela.Length - 1;
foreach (var id in antiga)
{
if (id != 0)
{
var i = (int)((Misturar(id) / Faixas) & (ulong)mascara);
while (_tabela[i] != 0)
{
i = (i + 1) & mascara;
}

_tabela[i] = id;
}
}
}
}
}
68 changes: 59 additions & 9 deletions src/mapdisk.nucleo/varredura/conjunto-nomes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,78 @@ namespace MapDisk.Nucleo;

/// <summary>
/// Guarda cada nome de arquivo uma vez só por varredura. Numa unidade de trabalho, o mesmo
/// nome se repete milhares de vezes (index.js, desktop.ini, thumbs.db). Dividido em faixas com
/// trava, como o conjunto de identificadores.
/// nome se repete milhares de vezes (index.js, desktop.ini, thumbs.db). Cada faixa é uma tabela
/// aberta só de referências, com 8 bytes por posição, e o texto do nome só é criado quando o
/// nome é novo. Dividido em faixas com trava, como o conjunto de identificadores.
/// </summary>
internal sealed class ConjuntoNomes
{
private const int Faixas = 64;

private readonly HashSet<string>[] _faixas = Enumerable.Range(0, Faixas).Select(_ => new HashSet<string>(StringComparer.Ordinal)).ToArray();
private readonly Faixa[] _faixas = Enumerable.Range(0, Faixas).Select(_ => new Faixa()).ToArray();

public string Guardar(ReadOnlySpan<char> nome)
{
var texto = new string(nome);
var faixa = _faixas[(int)((uint)StringComparer.Ordinal.GetHashCode(texto) % Faixas)];
if (nome.IsEmpty)
{
return string.Empty;
}

var resumo = (uint)string.GetHashCode(nome);
var faixa = _faixas[(int)(resumo % Faixas)];
lock (faixa)
{
if (faixa.TryGetValue(texto, out var existente))
return faixa.Guardar(nome, resumo / Faixas);
}
}

private sealed class Faixa
{
private string?[] _tabela = new string?[64];
private int _quantos;

public string Guardar(ReadOnlySpan<char> nome, uint resumo)
{
var mascara = _tabela.Length - 1;
var i = (int)(resumo & (uint)mascara);
while (_tabela[i] is { } existente)
{
return existente;
if (nome.SequenceEqual(existente))
{
return existente;
}

i = (i + 1) & mascara;
}

faixa.Add(texto);
return texto;
var novo = new string(nome);
_tabela[i] = novo;
if (++_quantos * 4 > _tabela.Length * 3)
{
Crescer();
}

return novo;
}

private void Crescer()
{
var antiga = _tabela;
_tabela = new string?[antiga.Length * 2];
var mascara = _tabela.Length - 1;
foreach (var nome in antiga)
{
if (nome is not null)
{
var i = (int)(((uint)string.GetHashCode(nome.AsSpan()) / Faixas) & (uint)mascara);
while (_tabela[i] is not null)
{
i = (i + 1) & mascara;
}

_tabela[i] = nome;
}
}
}
}
}
5 changes: 4 additions & 1 deletion src/mapdisk/mapdisk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<!-- Sem compressao: comprimido, o .exe fica com 67 MB, mas o .NET descomprime as bibliotecas
na memoria ao abrir, cerca de 80 MB a mais. Sem ela, o .exe tem uns 149 MB e o pico de
uma varredura com 1,1 milhao de arquivos fica abaixo de 300 MB. Decisao do Manfred em 01/10/2026 -->
<EnableCompressionInSingleFile>false</EnableCompressionInSingleFile>
</PropertyGroup>

<!-- Fonte e logo da MT embutidos no .exe e lidos pela tela com pack:// -->
Expand Down
18 changes: 18 additions & 0 deletions testes/mapdisk.testes/conjunto-ids-testes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,22 @@ public void Muitas_tarefas_ao_mesmo_tempo_nao_perdem_nem_repetem()

Assert.Equal(100_000, aceitos);
}

[Fact]
public void Zero_e_muitos_ids_cabem_e_nao_repetem()
{
var ids = new ConjuntoIds();

Assert.True(ids.Acrescentar(0));
Assert.False(ids.Acrescentar(0));
for (long i = 1; i <= 300_000; i++)
{
Assert.True(ids.Acrescentar(i * 7919));
}

for (long i = 1; i <= 300_000; i++)
{
Assert.False(ids.Acrescentar(i * 7919));
}
}
}
14 changes: 14 additions & 0 deletions testes/mapdisk.testes/conjunto-nomes-testes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,18 @@ public void Muitas_tarefas_ao_mesmo_tempo()

Assert.Equal(10, guardados.Distinct(ReferenceEqualityComparer.Instance).Count());
}

[Fact]
public void Muitos_nomes_diferentes_crescem_sem_perder_nenhum()
{
var nomes = new ConjuntoNomes();
var primeiros = Enumerable.Range(0, 50_000).Select(i => nomes.Guardar($"arquivo-{i}.txt".AsSpan())).ToList();

for (var i = 0; i < 50_000; i++)
{
Assert.Same(primeiros[i], nomes.Guardar($"arquivo-{i}.txt".AsSpan()));
}

Assert.Equal(string.Empty, nomes.Guardar(ReadOnlySpan<char>.Empty));
}
}
Loading