Skip to content

Commit

Permalink
fix: Memory Leaks
Browse files Browse the repository at this point in the history
  • Loading branch information
dtkav committed Sep 21, 2024
1 parent 2b2c49c commit 89589bb
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 37 deletions.
67 changes: 56 additions & 11 deletions src/LiveViews.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { Extension } from "@codemirror/state";
import { Compartment } from "@codemirror/state";
import {
StateEffect,
StateField,
EditorState,
Compartment,
} from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import {
App,
Expand All @@ -20,10 +25,7 @@ import { SharedFolder, SharedFolders } from "./SharedFolder";
import { curryLog } from "./debug";
import { promiseWithTimeout } from "./promiseUtils";
import { Banner } from "./ui/Banner";
import {
LiveEdit,
connectionManagerFacet,
} from "./y-codemirror.next/LiveEditPlugin";
import { LiveEdit } from "./y-codemirror.next/LiveEditPlugin";
import {
yRemoteSelections,
yRemoteSelectionsTheme,
Expand Down Expand Up @@ -265,9 +267,19 @@ export class LiveView implements S3View {
return stale;
}

_workaroundCM6StateFieldInitialization() {
const editorView = (this.view.editor as any).cm as EditorView;
const field = editorView.state.field(ConnectionManagerStateField, false);
if (field === undefined) {
this._parent.reconfigure(editorView);
}
}

attach(): Promise<LiveView> {
// can be called multiple times, whereas release is only ever called once
this.setConnectionDot();
this._workaroundCM6StateFieldInitialization();

return new Promise((resolve) => {
return this.document
.whenReady()
Expand Down Expand Up @@ -307,9 +319,20 @@ export class LiveView implements S3View {
this.document.disconnect();
}

_workaroundCM6MemoryLeak() {
// CM6 memory leak
// CM6 will hold references to state fields in config.dynamicSlots
// for us this is a big problem because LiveViewManager has references
// to basically everything.
const editor = this.view.editor;
const editorView = (editor as any).cm as EditorView;
(editorView.state as any).config.dynamicSlots.length = 0;
}

destroy() {
this.release();
this.clearViewActions();
this._workaroundCM6MemoryLeak();
this._parent = null as any;
this.view = null as any;
this.document = null as any;
Expand All @@ -320,8 +343,7 @@ export class LiveViewManager {
workspace: Workspace;
views: S3View[];
private _activePromise?: Promise<boolean> | null;
private _stale: string;
private _compartment: Compartment;
_compartment: Compartment;
private loginManager: LoginManager;
private offListeners: (() => void)[] = [];
private folderListeners: Map<SharedFolder, () => void> = new Map();
Expand All @@ -346,12 +368,11 @@ export class LiveViewManager {
this.sharedFolders = sharedFolders;
this.views = [];
this.extensions = [];
this._compartment = new Compartment();
this._activePromise = null;
this._stale = "";
this.loginManager = loginManager;
this.networkStatus = networkStatus;
this.refreshQueue = [];
this._compartment = new Compartment();

this.log = curryLog("[LiveViews]", "log");
this.warn = curryLog("[LiveViews]", "warn");
Expand Down Expand Up @@ -411,6 +432,16 @@ export class LiveViewManager {
);
}

reconfigure(editorView: EditorView) {
editorView.dispatch({
effects: this._compartment.reconfigure([
ConnectionManagerStateField.init(() => {
return this;
}),
]),
});
}

onMeta(tfile: TFile, cb: (data: string, cache: CachedMetadata) => void) {
this.metadataListeners.set(tfile, cb);
}
Expand Down Expand Up @@ -721,7 +752,11 @@ export class LiveViewManager {
this.wipe();
if (this.views.length > 0) {
this.extensions.push([
this._compartment.of(connectionManagerFacet.of(this)),
this._compartment.of(
ConnectionManagerStateField.init(() => {
return this;
}),
),
LiveEdit,
yRemoteSelectionsTheme,
yRemoteSelections,
Expand All @@ -744,9 +779,19 @@ export class LiveViewManager {
this.views = [];
this.wipe();
this.sharedFolders = null as any;
this._compartment = null as any;
this.refreshQueue = null as any;
this.networkStatus = null as any;
this._activePromise = null as any;
}
}

export const ConnectionManagerStateField = StateField.define<
LiveViewManager | undefined
>({
create(state: EditorState) {
return undefined;
},
update(currentManager, transaction) {
return currentManager;
},
});
24 changes: 13 additions & 11 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";

import { type Extension, StateField, EditorState } from "@codemirror/state";
import { TFolder, Notice, MarkdownView, Vault, FileManager } from "obsidian";
import { Platform } from "obsidian";
import { SharedFolder } from "./SharedFolder";
Expand Down Expand Up @@ -66,7 +67,6 @@ export default class Live extends Plugin {
_offFlagUpdates!: Unsubscriber;
relayManager!: RelayManager;
settingsTab!: LiveSettingsTab;
_extensions!: [];
log!: (message: string, ...args: unknown[]) => void;
warn!: (message: string, ...args: unknown[]) => void;
private _liveViews!: LiveViewManager;
Expand Down Expand Up @@ -499,18 +499,20 @@ export default class Live extends Plugin {
// We want to unload the visual components but not the data
this.settingsFileLocked = true;

this._offFlagUpdates();
if (this._offSaveSettings) {
this._offSaveSettings();
}
this._offFlagUpdates?.();
this._offFlagUpdates = null as any;

this._offSaveSettings?.();
this._offSaveSettings = null as any;

this.timeProvider.destroy();
this.timeProvider?.destroy();

this.folderNavDecorations?.destroy();

this.app.workspace.detachLeavesOfType(VIEW_TYPE_DIFFERENCES);

this.backgroundSync.destroy();
this.backgroundSync?.destroy();
this.backgroundSync = null as any;

this._liveViews?.destroy();
this._liveViews = null as any;
Expand All @@ -530,11 +532,11 @@ export default class Live extends Plugin {
this.sharedFolders.destroy();
this.sharedFolders = null as any;

this.settingsTab.destroy();

this.loginManager.destroy();
this.settingsTab?.destroy();
this.settingsTab = null as any;

this.app.workspace.updateOptions();
this.loginManager?.destroy();
this.loginManager = null as any;

FeatureFlagManager.destroy();
PostOffice.destroy();
Expand Down
21 changes: 17 additions & 4 deletions src/markdownView/InvalidLinkExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
} from "@codemirror/view";
import { WidgetType } from "@codemirror/view";
import { connectionManagerFacet } from "src/y-codemirror.next/LiveEditPlugin";
import { type S3View, LiveViewManager } from "../LiveViews";
import {
type S3View,
LiveViewManager,
ConnectionManagerStateField,
} from "../LiveViews";
import { curryLog } from "src/debug";
import type { CachedMetadata, MetadataCache } from "obsidian";

Expand Down Expand Up @@ -40,15 +44,16 @@ export class InvalidLinkPluginValue {
metadata: Map<number, CacheLink>;
editor: EditorView;
view?: S3View;
connectionManager: LiveViewManager | null;
connectionManager?: LiveViewManager;
decorationAnchors: number[];
decorations: DecorationSet;
log: (message: string) => void = (message: string) => {};
offMetadataUpdates = () => {};

constructor(editor: EditorView) {
this.editor = editor;
this.connectionManager = this.editor.state.facet(connectionManagerFacet);
this.connectionManager = this.editor.state.field(
ConnectionManagerStateField,
);
this.decorations = Decoration.none;
this.decorationAnchors = [];
this.metadata = new Map();
Expand Down Expand Up @@ -278,6 +283,14 @@ export class InvalidLinkPluginValue {
}
return this.decorations;
}

destroy() {
this.connectionManager = null as any;
this.view = undefined;
this.editor = null as any;
this.metadata.clear();
this.metadata = null as any;
}
}

export const InvalidLinkPlugin = ViewPlugin.fromClass(InvalidLinkPluginValue, {
Expand Down
20 changes: 13 additions & 7 deletions src/y-codemirror.next/LiveEditPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@
// License
// [The MIT License](./LICENSE) © Kevin Jahns

import { Facet, Annotation } from "@codemirror/state";
import { Facet, Annotation, StateField } from "@codemirror/state";
import type { ChangeSpec } from "@codemirror/state";
import { EditorView, ViewUpdate, ViewPlugin } from "@codemirror/view";
import type { PluginValue } from "@codemirror/view";
import { type S3View, LiveViewManager, isLive } from "../LiveViews";
import {
type S3View,
LiveViewManager,
isLive,
ConnectionManagerStateField,
} from "../LiveViews";
import { YText, YTextEvent, Transaction } from "yjs/dist/src/internals";
import { curryLog } from "src/debug";
import { FeatureFlagManager, withFlag } from "src/flagManager";
import { flag } from "src/flags";

export const connectionManagerFacet: Facet<LiveViewManager, LiveViewManager> =
Facet.define({
combine(inputs) {
Expand All @@ -24,16 +28,18 @@ export const ySyncAnnotation = Annotation.define();
export class LiveCMPluginValue implements PluginValue {
editor: EditorView;
view?: S3View;
connectionManager: LiveViewManager;
connectionManager?: LiveViewManager;
initialSet = false;
_observer?: (event: YTextEvent, tr: Transaction) => void;
_ytext?: YText;
log: (message: string) => void = (message: string) => {};

constructor(editor: EditorView) {
this.editor = editor;
this.connectionManager = this.editor.state.facet(connectionManagerFacet);
this.view = this.connectionManager.findView(editor);
this.connectionManager = this.editor.state.field(
ConnectionManagerStateField,
);
this.view = this.connectionManager?.findView(editor);
if (!this.view) {
return;
}
Expand Down Expand Up @@ -140,7 +146,7 @@ export class LiveCMPluginValue implements PluginValue {
return;
}
const editor: EditorView = update.view;
this.view = this.connectionManager.findView(editor);
this.view = this.connectionManager?.findView(editor);
const ytext = this.view?.document?.ytext;
if (!ytext) {
return;
Expand Down
20 changes: 16 additions & 4 deletions src/y-codemirror.next/RemoteSelections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ import {

import type { PluginValue, DecorationSet } from "@codemirror/view";

import { type S3View, LiveViewManager, LiveView } from "../LiveViews";
import {
type S3View,
LiveViewManager,
LiveView,
ConnectionManagerStateField,
} from "../LiveViews";

import * as Y from "yjs";
import { connectionManagerFacet } from "./LiveEditPlugin";
Expand Down Expand Up @@ -155,7 +160,7 @@ type AwarenessChangeHandler = (

export class YRemoteSelectionsPluginValue implements PluginValue {
editor: EditorView;
connectionManager: LiveViewManager;
connectionManager?: LiveViewManager;
view?: S3View;
decorations: DecorationSet;
_awareness?: Awareness;
Expand All @@ -164,8 +169,11 @@ export class YRemoteSelectionsPluginValue implements PluginValue {
constructor(editor: EditorView) {
this.editor = editor;
this.decorations = RangeSet.of([]);
this.connectionManager = this.editor.state.facet(connectionManagerFacet);
const view = this.connectionManager.findView(editor);
this.connectionManager = this.editor.state.field(
ConnectionManagerStateField,
);

const view = this.connectionManager?.findView(editor);
if (view && view instanceof LiveView) {
this.view = view;
const provider = this.view.document?._provider;
Expand All @@ -189,7 +197,11 @@ export class YRemoteSelectionsPluginValue implements PluginValue {
destroy() {
if (this._listener) {
this._awareness?.off("change", this._listener);
this._listener = undefined;
}
this.connectionManager = null as any;
this.view = null as any;
this.editor = null as any;
}

update(update: ViewUpdate) {
Expand Down

0 comments on commit 89589bb

Please sign in to comment.