Skip to content

perf: XML document parsed redundantly on every feature request — add per-document parse cache #14

@harshanacz

Description

@harshanacz

Problem

Every language feature handler (completion, hover, definition, rename, references, symbols, folding, formatting) and the diagnostics handler each independently call parseXMLDocument() on the same document text. There is no shared parse result cache.

This means a single user interaction can trigger multiple redundant parses of the same unchanged document:

user types a character
  → didChange → DiagnosticsHandler → parseXMLDocument()   (parse #1)
  → user hovers → requestHandlers  → parseXMLDocument()   (parse #2)
  → user triggers completion       → parseXMLDocument()   (parse #3)

Affected locations

diagnosticsHandler.ts line 53

const xmlDoc = this.service.parseXMLDocument(document.uri, text);

requestHandlers.ts — called independently in every handler:

// completion (line 41)
const xmlDoc = service.parseXMLDocument(document.uri, document.getText());

// hover (line 64)
const xmlDoc = service.parseXMLDocument(document.uri, document.getText());

// documentSymbol (line 74)
const xmlDoc = service.parseXMLDocument(document.uri, document.getText());

// foldingRanges, rename, definition, references, formatting — same pattern

Impact

  • Redundant CPU work on every keystroke
  • Adds latency to hover, completion, and other features especially for large XML files
  • Gets worse as more feature handlers are added

Proposed Solution

Introduce a lightweight parse cache keyed by document URI and document version. The TextDocument object from vscode-languageserver-textdocument exposes a version number that increments on every content change — this makes it a natural cache invalidation key.

interface ParseCacheEntry {
  version: number;
  xmlDoc: XMLDocument;
}

const parseCache = new Map<string, ParseCacheEntry>();

function getParsedDoc(document: TextDocument): XMLDocument {
  const cached = parseCache.get(document.uri);
  if (cached?.version === document.version) return cached.xmlDoc; // reuse ✅

  const xmlDoc = service.parseXMLDocument(document.uri, document.getText());
  parseCache.set(document.uri, { version: document.version, xmlDoc });
  return xmlDoc;
}
  • Same document, same version → return cached AST instantly, no re-parse
  • Document edited → version increments → re-parse once, update cache
  • Document closed → cache entry should be removed to avoid memory leaks

Where the cache should live

The cache should be shared between diagnosticsHandler.ts and requestHandlers.ts so that a parse triggered by validation is reused by the next hover or completion request on the same document version. It can be passed as a dependency during registerRequestHandlers() and DiagnosticsHandler construction, or extracted into a small XmlDocumentCache utility class.

Cache eviction on document close

Wire up documents.onDidClose in server.ts to remove the entry:

documents.onDidClose((e) => {
  parseCache.delete(e.document.uri);
});

Acceptance Criteria

  • parseXMLDocument() is called at most once per document version across all handlers
  • Cache is invalidated when document content changes (version changes)
  • Cache entry is removed when document is closed
  • Both DiagnosticsHandler and requestHandlers.ts use the shared cache

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions