Skip to content

Instantly share code, notes, and snippets.

@maxbrunsfeld
Last active September 11, 2017 22:04
Show Gist options
  • Save maxbrunsfeld/d3671eb644e54d251b43c0093a7fa466 to your computer and use it in GitHub Desktop.
Save maxbrunsfeld/d3671eb644e54d251b43c0093a7fa466 to your computer and use it in GitHub Desktop.
Avoiding memory leaks when using event listeners in Atom

Preventing Memory Leaks

Cleaning up event listeners

When subscribing to events using methods like atom.workspace.onDidAddTextEditor, it is important remove the event listeners when you no longer need your callback to be called (e.g. when your package is deactivated). To do this, store the returned Disposable, and call .dispose on it:

module.exports = {
  activate () {
    this.editorSubscription = atom.workspace.onDidAddTextEditor(editor => {
      console.log('Added a text editor!', editor)
    })
  },

  deactivate () {
    this.editorSubscription.dispose()
  }
}

Groups of event listeners

You will often need to use multiple event listeners in your package. You can easily remove multiple listeners at a time using the CompositeDisposable class:

const {CompositeDisposable} = require('atom')

module.exports = {
  activate () {
    this.subscriptions = new CompositeDisposable()

    this.subscriptions.add(atom.workspace.onDidAddPane(pane => {
      console.log('Added a pane!', pane)
    }))

    this.subscriptions.add(atom.workspace.onDidDestroyPane(pane => {
      console.log('Destroyed a pane!', pane)
    }))
  },

  deactivate () {
    this.subscriptions.dispose()
  }
}

Event listeners on objects with finite lifetimes

The event listeners in the previous two examples were registered on atom.workspace, an object that exists for the life of the application. Oftentimes though, you'll need to register event listeners on objects like TextEditors, which are created and destroyed frequently. This can give rise to another type of memory leak: Disposables themselves can retain objects like TextEditors unnecessarily:

const {CompositeDisposable} = require('atom')

module.exports = {
  activate () {
    this.subscriptions = new CompositeDisposable()

    this.subscriptions.add(atom.workspace.onDidAddTextEditor(editor => {

      // Memory leak - this `Disposable` retains `editor` until the package is
      // deactivated, even though `editor` may be destroyed long before then!
      const editorSubscription = editor.onDidStopChanging(() => {
        console.log('Editor changed', editor)
      })

      this.subscriptions.add(editorSubscription)
    }))
  },

  deactivate () {
    this.subscriptions.dispose()
  }
}

To avoid this type of leak, you need to release Disposables that reference the editor when the editor is destroyed. You can do this using the editor's onDidDestroy method:

const {CompositeDisposable} = require('atom')

module.exports = {
  activate () {
    this.subscriptions = new CompositeDisposable()

    this.subscriptions.add(atom.workspace.onDidAddTextEditor(editor => {
      const editorSubscriptions = new CompositeDisposable()

      editorSubscriptions.add(editor.onDidStopChanging(() => {
        console.log('Editor changed', editor)
      }))

      editorSubscriptions.add(editor.onDidDestroy(() => {
        editorSubscriptions.dispose()
        this.subscriptions.remove(editorSubscriptions)
      }))

      this.subscriptions.add(editorSubscriptions)
    }))
  },

  deactivate () {
    this.subscriptions.dispose()
  }
}

All objects in Atom that have event APIs and finite lifetimes should have an onDidDestroy method that works the same way. Examples:

@icetee
Copy link

icetee commented Sep 11, 2017

Thanks, great help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment