Skip to content

TextMate grammar registries not shared, ~350 MB memory waste over time #2396

@gadfly3173

Description

@gadfly3173

High memory usage after prolonged use — TextMate grammar data accumulates to ~350 MB

Generated by AI and I reviewed by myself

Summary

After running SourceGit (AOT build) for ~6 days with 4–5 repositories open, the process RSS grows to ~1.15 GB. Analysis of /proc/<pid>/smaps and anonymous memory segment dumps reveals that TextMate syntax highlighting grammar data alone accounts for ~345 MB (30%) of total RSS, making it the single largest memory consumer.

Environment

Item Value
Build AOT (self-contained)
Runtime ~6 days
OS Linux 6.18 (deepin)
GPU Intel HD Graphics 630, Mesa 24.3.0
.NET SDK 10.0.300
Open repos 4–5

Observed memory breakdown

Category                      RSS        Share
──────────────────────────────────────────────
TextMate grammar segments     345 MB     30%
.NET LOH segments             172 MB     15%
.NET SOH segments             138 MB     12%
Other GC segments             204 MB     18%
GPU / Skia cache               61 MB      5%
Native heap                    55 MB      5%
Other                         175 MB     15%
──────────────────────────────────────────────
Total                        1150 MB    100%

Memory mapping analysis

90% of RSS is anonymous memory (heap allocations), not file-backed mappings. Among the 307 anonymous rw-p segments, ~105 segments in the 5–7 MB range each contain a full set of TextMate grammar definitions — compiled regular expressions, scope names (e.g. keyword.preprocessor.error.cs, meta.function-call.parameters.r), and rule structures (beginCaptures, endCaptures, patterns). Each such segment holds roughly 460–480 distinct TextMate scope keywords.

This points to redundant, non-shared TextMate grammar registries being created independently for each editor/viewer instance.

Suspected root cause

TextMateHelper.CreateForEditor() creates a new RegistryOptionsWrapper (and therefore a new RegistryOptions) for every call:

// src/Models/TextMateHelper.cs
public static TextMate.Installation CreateForEditor(TextEditor editor)
{
    return editor.InstallTextMate(
        Application.Current?.ActualThemeVariant == ThemeVariant.Dark
            ? new RegistryOptionsWrapper(ThemeName.DarkPlus)   // new instance every time
            : new RegistryOptionsWrapper(ThemeName.LightPlus));
}

RegistryOptions internally caches loaded grammars (compiled regex objects, scope maps, etc.), but each instance maintains its own independent cache. This method is called from at least 7 different views — TextDiffView, Blame, CommandLogContentPresenter, MergeConflictEditor, AIAssistant, SelfUpdate, and RevisionFileContentViewer — each creating a separate registry.

Although _textMate.Dispose() is called when views are destroyed, the grammar objects previously cached in each RegistryOptions instance may survive on the GC heap until collected. When multiple views' lifetimes overlap (e.g. switching between diffs of different file types), several independent copies of the same grammar data coexist in managed memory simultaneously.

Over hours/days of usage viewing diffs across many file types, this accumulates to an estimated 200–350 MB of retained grammar data.

Suggested fix

Share a single RegistryOptionsWrapper per theme (dark/light) across all editors, so grammar definitions are loaded and cached only once:

private static RegistryOptionsWrapper s_darkRegistry;
private static RegistryOptionsWrapper s_lightRegistry;

public static TextMate.Installation CreateForEditor(TextEditor editor)
{
    var isDark = Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
    var registry = isDark == true
        ? (s_darkRegistry ??= new RegistryOptionsWrapper(ThemeName.DarkPlus))
        : (s_lightRegistry ??= new RegistryOptionsWrapper(ThemeName.LightPlus));
    return editor.InstallTextMate(registry);
}

Note: per-view state like LastScope should be moved out of the shared registry to avoid cross-editor interference.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions