logo hsb.horse
← Voltar para o índice de snippets

Snippets

Caminhada de Repositório em Duas Fases com Orçamento de Bytes

Dividir o processamento em scan path/size → análise de estrutura → leitura apenas dos chunks necessários → parsing para tornar os limites de memória controláveis mesmo para grandes repositórios.

Publicado: Atualizado:

Ao analisar grandes bases de código, carregar todos os arquivos na memória de uma vez leva à falha. Ao coletar primeiro apenas caminhos e stats, completar a análise de estrutura e então dividir por orçamento de bytes para ler/parsear/liberar apenas arquivos necessários sequencialmente, você pode manter o uso de memória previsível.

Código

import { promises as fs } from 'node:fs'
import { join, extname } from 'node:path'
interface FileEntry {
path: string
size: number
}
interface ParsedFile {
path: string
content: string
ast?: unknown // AST ou resultado de análise
}
interface WalkOptions {
/** Extensões para parsear (ex: ['.ts', '.js']) */
parseableExtensions: string[]
/** Padrões para ignorar (ex: node_modules, .git) */
ignorePatterns: RegExp[]
/** Bytes máximos por chunk */
chunkBudget: number
/** Tamanho máximo de arquivo único (pula arquivos que excedem) */
maxFileSize: number
}
/**
* Fase 1: Escanear apenas caminhos e tamanhos
*/
async function scanPaths(
rootPath: string,
options: WalkOptions
): Promise<FileEntry[]> {
const entries: FileEntry[] = []
async function walk(dir: string): Promise<void> {
const items = await fs.readdir(dir, { withFileTypes: true })
for (const item of items) {
const fullPath = join(dir, item.name)
// Verificar padrões de ignorar (julgamento precoce para eficiência de memória)
if (options.ignorePatterns.some((pattern) => pattern.test(fullPath))) {
continue
}
if (item.isDirectory()) {
await walk(fullPath)
} else if (item.isFile()) {
const stat = await fs.stat(fullPath)
// Verificação de tamanho (excluir arquivos enormes cedo)
if (stat.size > options.maxFileSize) {
continue
}
entries.push({
path: fullPath,
size: stat.size
})
}
}
}
await walk(rootPath)
return entries
}
/**
* Fase 2: Análise de estrutura e filtragem
*/
function analyzeStructure(
entries: FileEntry[],
options: WalkOptions
): FileEntry[] {
// Filtrar por extensão
const parseableFiles = entries.filter((entry) => {
const ext = extname(entry.path)
return options.parseableExtensions.includes(ext)
})
// Ordenar por tamanho (processar arquivos menores primeiro gera resultados precoces)
return parseableFiles.toSorted((a, b) => a.size - b.size)
}
/**
* Fase 3: Dividir por orçamento de bytes e ler/parsear sequencialmente
*/
async function parseWithBudget(
entries: FileEntry[],
options: WalkOptions,
parseFile: (content: string, path: string) => unknown
): Promise<ParsedFile[]> {
const results: ParsedFile[] = []
let currentBudget = 0
let chunk: FileEntry[] = []
for (const entry of entries) {
// Processar chunk quando orçamento excedido
if (currentBudget + entry.size > options.chunkBudget) {
// Processar chunk atual
const parsed = await processChunk(chunk, parseFile)
results.push(...parsed)
// Reiniciar (permite ao GC recuperar memória)
chunk = []
currentBudget = 0
}
chunk.push(entry)
currentBudget += entry.size
}
// Processar último chunk
if (chunk.length > 0) {
const parsed = await processChunk(chunk, parseFile)
results.push(...parsed)
}
return results
}
/**
* Leitura e parsing em lote de arquivos no chunk
*/
async function processChunk(
chunk: FileEntry[],
parseFile: (content: string, path: string) => unknown
): Promise<ParsedFile[]> {
const results: ParsedFile[] = []
// Leitura paralela (execução concorrente no chunk para eficiência de I/O)
const readPromises = chunk.map(async (entry) => {
try {
const content = await fs.readFile(entry.path, 'utf-8')
const ast = parseFile(content, entry.path)
return { path: entry.path, content, ast }
} catch (error) {
console.warn(`Failed to parse ${entry.path}:`, error)
return null
}
})
const parsed = await Promise.all(readPromises)
for (const result of parsed) {
if (result) {
results.push(result)
}
}
return results
}
/**
* Interface unificada
*/
export async function walkRepository(
rootPath: string,
options: WalkOptions,
parseFile: (content: string, path: string) => unknown
): Promise<ParsedFile[]> {
// Fase 1: Escanear caminhos e tamanhos
const entries = await scanPaths(rootPath, options)
// Fase 2: Análise de estrutura
const parseableFiles = analyzeStructure(entries, options)
// Fase 3: Parsear sequencialmente por orçamento de bytes
const results = await parseWithBudget(parseableFiles, options, parseFile)
return results
}

Uso

import { parse } from '@typescript-eslint/typescript-estree'
// Analisar repositório TypeScript
const results = await walkRepository(
'./my-project',
{
parseableExtensions: ['.ts', '.tsx', '.js', '.jsx'],
ignorePatterns: [
/node_modules/,
/\.git/,
/dist/,
/build/
],
chunkBudget: 50 * 1024 * 1024, // 50MB
maxFileSize: 10 * 1024 * 1024 // 10MB
},
(content, path) => {
// Aplicar parser AST
return parse(content, {
filePath: path,
jsx: true
})
}
)
console.log(`Parsed ${results.length} files`)

Como Funciona

  1. Fase 1 (Scan): Percorrer recursivamente diretórios com fs.readdir e fs.stat, coletando apenas path e size. Conteúdos de arquivos não são lidos.
  2. Fase 2 (Análise de Estrutura): Extrair arquivos parseáveis dos metadados coletados e otimizar ordem de processamento (por tamanho, prioridade, etc.).
  3. Fase 3 (Chunking + Parsing): Dividir arquivos em chunks não excedendo orçamento de bytes, então read → parse → free cada chunk.
  4. Leitura Paralela: Arquivos dentro de um chunk são lidos simultaneamente com Promise.all para reduzir tempo de espera de I/O.
  5. Liberação de Memória: Reiniciar variáveis após processamento de chunk para que o GC possa recuperar memória.

Benefícios

  • Controle de Memória: Evitar ler todos os arquivos de uma vez; definir limite superior com orçamento de bytes
  • Filtragem Precoce: Excluir arquivos desnecessários usando apenas stats, reduzindo I/O
  • Rastreamento de Progresso: Relatar progresso por chunk, facilitando atualização de UI ou manipulação de cancelamento
  • Eficiência de I/O: Otimizar tempo de espera de disco com leitura paralela em chunks

Precauções

Para determinar “quais arquivos priorizar” na análise de estrutura da Fase 2 requer algum conhecimento de domínio. Por exemplo, ler package.json ou arquivos de configuração primeiro pode informar a estratégia de processamento para outros arquivos. Além disso, chunkBudget e maxFileSize precisam de ajuste de acordo com os limites de memória do ambiente.

Aplicações

  • Indexador de código: Extrair e indexar definições de função/tipo no repositório
  • Analisador estático: Aplicar regras de lint em massa a grande base de código
  • Ferramenta de migração baseada em AST: Aplicar refatoração automática a toda base de código
  • Linter em massa: Verificar dezenas de milhares de arquivos sem falha de memória
  • Ingestão de grande repositório: Analisar mudanças em grandes repositórios em CI/CD