mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-10-12 04:54:27 +00:00
perf(parser): cache syntax errors to skip walk on cache hit
extractErrors is a pure function of the parsed tree (it ignores content), so re-walking the whole tree on every cache hit wasted work on the hottest path (file_read+include_ast, ast_query, edit validation). Store []SyntaxError in CachedTree and return it directly on hits. Adds TestParseCachedErrorsMatch asserting cache-hit errors equal the cache-miss parse.
This commit is contained in:
@@ -48,10 +48,13 @@ type Registry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CachedTree stores a parsed tree with its metadata.
|
// CachedTree stores a parsed tree with its metadata.
|
||||||
// Content is not stored to reduce memory usage.
|
// Content is not stored to reduce memory usage. Syntax errors are a pure
|
||||||
|
// function of the tree (extractErrors ignores content), so they are cached
|
||||||
|
// alongside it to avoid re-walking the whole tree on every cache hit.
|
||||||
type CachedTree struct {
|
type CachedTree struct {
|
||||||
Tree *sitter.Tree
|
Tree *sitter.Tree
|
||||||
Language protocol.Language
|
Language protocol.Language
|
||||||
|
Errors []SyntaxError
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseResult contains the result of parsing a file.
|
// ParseResult contains the result of parsing a file.
|
||||||
@@ -202,11 +205,10 @@ func (r *Registry) Parse(ctx context.Context, filename string, content []byte) (
|
|||||||
hash := fmt.Sprintf("%s:%016x", string(lang), xxhash.Sum64(content))
|
hash := fmt.Sprintf("%s:%016x", string(lang), xxhash.Sum64(content))
|
||||||
if cached, ok := r.cache.Get(hash); ok && cached.Language == lang {
|
if cached, ok := r.cache.Get(hash); ok && cached.Language == lang {
|
||||||
r.cacheHits.Add(1)
|
r.cacheHits.Add(1)
|
||||||
errors := extractErrors(cached.Tree.RootNode(), content)
|
|
||||||
return &ParseResult{
|
return &ParseResult{
|
||||||
Tree: cached.Tree,
|
Tree: cached.Tree,
|
||||||
Language: lang,
|
Language: lang,
|
||||||
Errors: errors,
|
Errors: cached.Errors,
|
||||||
Content: content,
|
Content: content,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -242,10 +244,12 @@ func (r *Registry) Parse(ctx context.Context, filename string, content []byte) (
|
|||||||
// Extract syntax errors
|
// Extract syntax errors
|
||||||
errors := extractErrors(tree.RootNode(), content)
|
errors := extractErrors(tree.RootNode(), content)
|
||||||
|
|
||||||
// Cache result (LRU cache handles eviction automatically)
|
// Cache result (LRU cache handles eviction automatically).
|
||||||
|
// Errors are cached so cache hits skip a full tree walk.
|
||||||
r.cache.Add(hash, &CachedTree{
|
r.cache.Add(hash, &CachedTree{
|
||||||
Tree: tree,
|
Tree: tree,
|
||||||
Language: lang,
|
Language: lang,
|
||||||
|
Errors: errors,
|
||||||
})
|
})
|
||||||
|
|
||||||
return &ParseResult{
|
return &ParseResult{
|
||||||
|
|||||||
@@ -172,6 +172,43 @@ func TestParseWithSyntaxErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestParseCachedErrorsMatch verifies that the syntax errors returned on a
|
||||||
|
// cache hit are identical to those computed on the initial parse. Errors are
|
||||||
|
// cached alongside the tree (they are a pure function of the tree), so the hot
|
||||||
|
// cache-hit path must not lose or alter them.
|
||||||
|
func TestParseCachedErrorsMatch(t *testing.T) {
|
||||||
|
r := NewRegistry()
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
content := []byte("package main\n\nfunc main( {}\n") // missing closing paren
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
first, err := r.Parse(ctx, "test.go", content)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("first parse failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(first.Errors) == 0 {
|
||||||
|
t.Fatal("expected syntax errors on first parse")
|
||||||
|
}
|
||||||
|
|
||||||
|
second, err := r.Parse(ctx, "test.go", content)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cached parse failed: %v", err)
|
||||||
|
}
|
||||||
|
if second.Tree != first.Tree {
|
||||||
|
t.Fatal("expected cache hit (same tree pointer)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(second.Errors) != len(first.Errors) {
|
||||||
|
t.Fatalf("cached errors count = %d, want %d", len(second.Errors), len(first.Errors))
|
||||||
|
}
|
||||||
|
for i := range first.Errors {
|
||||||
|
if second.Errors[i] != first.Errors[i] {
|
||||||
|
t.Errorf("cached error[%d] = %+v, want %+v", i, second.Errors[i], first.Errors[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestIsBinary(t *testing.T) {
|
func TestIsBinary(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user