logo hsb.horse
← Zur Snippets-Übersicht

Snippets

Zweiphasiger Repository-Walk mit Byte-Budget

Verarbeitung in path/size Scan → Strukturanalyse → nur benötigte Chunks lesen → Parsen aufteilen, um Speicherlimits auch für große Repositories kontrollierbar zu machen.

Veröffentlicht: Aktualisiert:

Bei der Analyse großer Codebasen führt das gleichzeitige Laden aller Dateien in den Speicher zum Scheitern. Durch das vorherige Sammeln nur von Pfaden und Stats, Abschließen der Strukturanalyse und anschließendes Chunking nach Byte-Budget zum sequentiellen Lesen/Parsen/Freigeben nur benötigter Dateien kann die Speichernutzung vorhersagbar gehalten werden.

Code

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 oder Analyseergebnis
}
interface WalkOptions {
/** Zu parsende Erweiterungen (z.B. ['.ts', '.js']) */
parseableExtensions: string[]
/** Zu ignorierende Muster (z.B. node_modules, .git) */
ignorePatterns: RegExp[]
/** Maximale Bytes pro Chunk */
chunkBudget: number
/** Maximale Einzeldateigröße (Dateien darüber werden übersprungen) */
maxFileSize: number
}
/**
* Phase 1: Nur Pfade und Größen scannen
*/
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)
// Ignorier-Muster prüfen (frühe Beurteilung für Speichereffizienz)
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)
// Größenprüfung (riesige Dateien früh ausschließen)
if (stat.size > options.maxFileSize) {
continue
}
entries.push({
path: fullPath,
size: stat.size
})
}
}
}
await walk(rootPath)
return entries
}
/**
* Phase 2: Strukturanalyse und Filterung
*/
function analyzeStructure(
entries: FileEntry[],
options: WalkOptions
): FileEntry[] {
// Nach Erweiterung filtern
const parseableFiles = entries.filter((entry) => {
const ext = extname(entry.path)
return options.parseableExtensions.includes(ext)
})
// Nach Größe sortieren (kleinere Dateien zuerst zu verarbeiten liefert frühe Ergebnisse)
return parseableFiles.toSorted((a, b) => a.size - b.size)
}
/**
* Phase 3: Nach Byte-Budget chunken und sequentiell lesen/parsen
*/
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) {
// Chunk verarbeiten wenn Budget überschritten
if (currentBudget + entry.size > options.chunkBudget) {
// Aktuellen Chunk verarbeiten
const parsed = await processChunk(chunk, parseFile)
results.push(...parsed)
// Zurücksetzen (ermöglicht GC Speicher freizugeben)
chunk = []
currentBudget = 0
}
chunk.push(entry)
currentBudget += entry.size
}
// Letzten Chunk verarbeiten
if (chunk.length > 0) {
const parsed = await processChunk(chunk, parseFile)
results.push(...parsed)
}
return results
}
/**
* Batch-Lesen und Parsen von Dateien im Chunk
*/
async function processChunk(
chunk: FileEntry[],
parseFile: (content: string, path: string) => unknown
): Promise<ParsedFile[]> {
const results: ParsedFile[] = []
// Paralleles Lesen (gleichzeitige Ausführung im Chunk für I/O-Effizienz)
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
}
/**
* Einheitliche Schnittstelle
*/
export async function walkRepository(
rootPath: string,
options: WalkOptions,
parseFile: (content: string, path: string) => unknown
): Promise<ParsedFile[]> {
// Phase 1: Pfade und Größen scannen
const entries = await scanPaths(rootPath, options)
// Phase 2: Strukturanalyse
const parseableFiles = analyzeStructure(entries, options)
// Phase 3: Sequentiell nach Byte-Budget parsen
const results = await parseWithBudget(parseableFiles, options, parseFile)
return results
}

Verwendung

import { parse } from '@typescript-eslint/typescript-estree'
// TypeScript-Repository analysieren
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) => {
// AST-Parser anwenden
return parse(content, {
filePath: path,
jsx: true
})
}
)
console.log(`Parsed ${results.length} files`)

Funktionsweise

  1. Phase 1 (Scan): Verzeichnisse rekursiv mit fs.readdir und fs.stat durchlaufen, dabei nur path und size sammeln. Dateiinhalte werden nicht gelesen.
  2. Phase 2 (Strukturanalyse): Parsbare Dateien aus gesammelten Metadaten extrahieren und Verarbeitungsreihenfolge optimieren (nach Größe, Priorität, etc.).
  3. Phase 3 (Chunking + Parsen): Dateien in Chunks aufteilen, die Byte-Budget nicht überschreiten, dann jeden Chunk read → parse → free.
  4. Paralleles Lesen: Dateien innerhalb eines Chunks werden gleichzeitig mit Promise.all gelesen, um I/O-Wartezeit zu reduzieren.
  5. Speicherfreigabe: Variablen nach Chunk-Verarbeitung zurücksetzen, damit GC Speicher freigeben kann.

Vorteile

  • Speicherkontrolle: Nicht alle Dateien auf einmal lesen; Obergrenze mit Byte-Budget festlegen
  • Frühe Filterung: Unnötige Dateien nur mit Stats ausschließen, I/O reduzieren
  • Fortschrittsverfolgung: Fortschritt pro Chunk melden, erleichtert UI-Aktualisierung oder Abbruchbehandlung
  • I/O-Effizienz: Festplatten-Wartezeit mit parallelem Lesen in Chunks optimieren

Vorsichtsmaßnahmen

Um in Phase 2 Strukturanalyse zu bestimmen “welche Dateien zu priorisieren” erfordert etwas Domänenwissen. Zum Beispiel kann das zuerst Lesen von package.json oder Konfigurationsdateien die Verarbeitungsstrategie für andere Dateien informieren. Außerdem benötigen chunkBudget und maxFileSize Anpassung je nach Umgebungs-Speicherlimits.

Anwendungen

  • Code-Indexer: Funktions-/Typdefinitionen im Repository extrahieren und indizieren
  • Statischer Analyzer: Lint-Regeln massenhaft auf große Codebasis anwenden
  • AST-basiertes Migrations-Tool: Automatisches Refactoring auf gesamte Codebasis anwenden
  • Massen-Linter: Zehntausende Dateien prüfen ohne Speicherausfall
  • Große-Repo-Aufnahme: Änderungen in großen Repositories in CI/CD analysieren