How To Speed Up Your Packages


#1

I’ve done some testing in an attempt to loading new windows faster. The major issues are caused by packages. When writing packages, here are some tips.

Do not require modules before activation.

Node.js promotes the pattern of putting all your “imports” at the top of the file. Do not do this on your main module. Remember that your mainModule is require()'d whenever a window loads, but all of your code need not be as well. You should know that require() does not scale well.

You can do the following pattern.

Builder = null

module.exports =
  activate: (state) ->
    Builder ?= require './models/builder'
    @builder = new Builder()

  deactivate: ->
    @builder.destroy()
    @builder = null

?= will check if the variable is null, and if so, will assign the value. If it’s not null, it’ll skip assigning the value.

Set activationEvents in your package.json

If your package doesn’t need to be activated before user input (which is every single package that isn’t part of the UI) then you should have an activationEvent set. These are either keybindings defined a ./keymap/*.cson file, or atom defined events.

{
  "name": "build",
  "main": "./lib/main",
  ...
  "activationEvents": [
    "build:run",
    "build:stop",
    "build:config"
  ],
  ...
}

Atom Defined Events

Finding events:

  • Open up the Dev Tools (Ctrl+Alt+I)
  • Click the console tab
  • Enter: atom.workspaceView.events()

List of Atom Events (with atom --safe)

atom.workspaceView.events()

application:about: "Application: About"
application:bring-all-windows-to-front: "Application: Bring All Windows To Front"
application:hide: "Application: Hide"
application:hide-other-applications: "Application: Hide Other Applications"
application:install-update: "Application: Install Update"
application:minimize: "Application: Minimize"
application:new-file: "Application: New File"
application:new-window: "Application: New Window"
application:open: "Application: Open"
application:open-dev: "Application: Open Dev"
application:open-file: "Application: Open File"
application:open-folder: "Application: Open Folder"
application:open-license: "Application: Open License"
application:open-safe: "Application: Open Safe"
application:open-your-config: "Application: Open Your Config"
application:open-your-init-script: "Application: Open Your Init Script"
application:open-your-keymap: "Application: Open Your Keymap"
application:open-your-snippets: "Application: Open Your Snippets"
application:open-your-stylesheet: "Application: Open Your Stylesheet"
application:quit: "Application: Quit"
application:run-all-specs: "Application: Run All Specs"
application:run-benchmarks: "Application: Run Benchmarks"
application:show-settings: "Application: Show Settings"
application:unhide-all-applications: "Application: Unhide All Applications"
application:zoom: "Application: Zoom"
autocomplete:toggle: "Autocomplete: Toggle"
autoflow:reflow-selection: "Autoflow: Reflow Selection"
bookmarks:view-all: "Bookmarks: View All"
click: null
command-palette:toggle: "Command Palette: Toggle"
contextmenu: null
core:cancel: null
core:close: "Core: Close"
core:copy: null
core:focus-next: "Core: Focus Next"
core:focus-previous: "Core: Focus Previous"
core:paste: null
core:redo: null
core:save: "Core: Save"
core:save-as: "Core: Save As"
core:select-all: null
core:undo: null
cursor:moved: null
deprecation-cop:view: "Deprecation Cop: View"
dragover: null
drop: null
editor:attached: null
find-and-replace:find-next: "Find And Replace: Find Next"
find-and-replace:find-previous: "Find And Replace: Find Previous"
find-and-replace:replace-all: "Find And Replace: Replace All"
find-and-replace:replace-next: "Find And Replace: Replace Next"
find-and-replace:select-all: "Find And Replace: Select All"
find-and-replace:select-next: "Find And Replace: Select Next"
find-and-replace:show: "Find And Replace: Show"
find-and-replace:show-replace: "Find And Replace: Show Replace"
find-and-replace:toggle: "Find And Replace: Toggle"
find-and-replace:use-selection-as-find-pattern: "Find And Replace: Use Selection As Find Pattern"
focus: null
focusout: null
fuzzy-finder:toggle-buffer-finder: "Fuzzy Finder: Toggle Buffer Finder"
fuzzy-finder:toggle-file-finder: "Fuzzy Finder: Toggle File Finder"
fuzzy-finder:toggle-git-status-finder: "Fuzzy Finder: Toggle Git Status Finder"
go-to-line:toggle: "Go To Line: Toggle"
grammar-selector:show: "Grammar Selector: Show"
key-binding-resolver:toggle: "Key Binding Resolver: Toggle"
keydown: null
link:open: "Link: Open"
markdown-preview:copy-html: "Markdown Preview: Copy Html"
markdown-preview:preview-file: null
markdown-preview:toggle: "Markdown Preview: Toggle"
markdown-preview:toggle-break-on-single-newline: "Markdown Preview: Toggle Break On Single Newline"
open-on-github:blame: "Open On GitHub: Blame"
open-on-github:branch-compare: "Open On GitHub: Branch Compare"
open-on-github:copy-url: "Open On GitHub: Copy Url"
open-on-github:file: "Open On GitHub: File"
open-on-github:history: "Open On GitHub: History"
package-generator:generate-package: "Package Generator: Generate Package"
package-generator:generate-syntax-theme: "Package Generator: Generate Syntax Theme"
pane-container:active-pane-item-changed: null
pane:active-item-title-changed: null
pane:before-item-destroyed: null
pane:removed: null
pane:reopen-closed-item: "Pane: Reopen Closed Item"
project-find:show: "Project Find: Show"
project-find:show-in-current-directory: "Project Find: Show In Current Directory"
project-find:toggle: "Project Find: Toggle"
release-notes:show: "Release Notes: Show"
settings-view:change-themes: "Settings View: Change Themes"
settings-view:install-packages: "Settings View: Install Packages"
settings-view:install-themes: "Settings View: Install Themes"
settings-view:open: "Settings View: Open"
settings-view:show-keybindings: "Settings View: Show Keybindings"
settings-view:uninstall-packages: "Settings View: Uninstall Packages"
settings-view:uninstall-themes: "Settings View: Uninstall Themes"
show: null
styleguide:show: "Styleguide: Show"
symbols-view:go-to-declaration: "Symbols View: Go To Declaration"
symbols-view:toggle-file-symbols: "Symbols View: Toggle File Symbols"
symbols-view:toggle-project-symbols: "Symbols View: Toggle Project Symbols"
timecop:view: "Timecop: View"
tree-view:add-file: "Tree View: Add File"
tree-view:add-folder: "Tree View: Add Folder"
tree-view:duplicate: "Tree View: Duplicate"
tree-view:remove: "Tree View: Remove"
tree-view:reveal-active-file: "Tree View: Reveal Active File"
tree-view:show: "Tree View: Show"
tree-view:toggle: "Tree View: Toggle"
tree-view:toggle-focus: "Tree View: Toggle Focus"
tree-view:toggle-side: "Tree View: Toggle Side"
update-package-dependencies:update: "Update Package Dependencies: Update"
welcome:show-welcome-buffer: "Welcome: Show Welcome Buffer"
whitespace:convert-tabs-to-spaces: "Whitespace: Convert Tabs To Spaces"
whitespace:remove-trailing-whitespace: "Whitespace: Remove Trailing Whitespace"
window:decrease-font-size: "Window: Decrease Font Size"
window:focus-next-pane: "Window: Focus Next Pane"
window:focus-pane-above: "Window: Focus Pane Above"
window:focus-pane-below: "Window: Focus Pane Below"
window:focus-pane-on-left: "Window: Focus Pane On Left"
window:focus-pane-on-right: "Window: Focus Pane On Right"
window:focus-previous-pane: "Window: Focus Previous Pane"
window:increase-font-size: "Window: Increase Font Size"
window:install-shell-commands: "Window: Install Shell Commands"
window:log-deprecation-warnings: "Window: Log Deprecation Warnings"
window:reset-font-size: "Window: Reset Font Size"
window:run-package-specs: "Window: Run Package Specs"
window:save-all: "Window: Save All"
window:toggle-auto-indent: "Window: Toggle Auto Indent"
window:toggle-invisibles: "Window: Toggle Invisibles"
window:update-available: null

Tips for obtaining metrics

  • Use timecop
  • The first time you load/activate a package, all .coffee files will be compiled and put into the coffeescript cache. So open a new window, then open a second window and open timecop on that second window.

FAQ: Best Practices
Library packages?
Package lifecycle tension: serialization vs. lazy package activation
Is there an event for a view resizing?
Archived FAQs (see FAQ category for all current FAQs)
Module initializing more than once
Switching to Atom from Sublime Text
#2

I think this particular behavior has changed. See my comment here about a different package lifecycle issue.


#3

Removed the module cache import hack as v138 shares imported modules (of the same version I think).

I’ll look into this.


#4

Removed the module cache import hack as v138 shares imported modules (of the same version I think).

@Zren So does that mean that

lib = null
module.exports =
	activate: ->
		lib ?= require 'lib'

is no longer necessary to optimise packages?

Is it better to just require only when actually needed?

command: ->
	lib = require 'lib'
	# Command from lib

Or does Atom now handle this all more efficiently, to the point where it isn’t any less optimal to just put all the requires at the top?


#5

Atom has done a lot of optimization around compile and module caching. I haven’t looked at numbers recently, unfortunately. To my knowledge, these changes were targeting startup perf. But

One thing to keep in mind is that even though it might be much faster to load because things don’t have to compile, if you’re the first package to load that module then you still take the perf hit for loading it. And if you don’t need to load it because it is something that might not be used … then why not load it later if it becomes necessary? So, it is still potentially better if you load only things when needed. But you have to balance that against the complexity of things and supportability, etc.


#6

@leedohm So do you think take the latter approach but don’t bother with the former ?= method?


#7

Here’s what I use specifically in my tabs-to-spaces package:

https://github.com/lee-dohm/tabs-to-spaces/blob/master/lib/index.coffee#L52-L54

And I specifically null out the module reference in the deactivate method:

https://github.com/lee-dohm/tabs-to-spaces/blob/master/lib/index.coffee#L29-L35


#8

Nice… I see you have multiple commands here though (any of which rely on ./tabs-to-spaces). I’m currently working on a modular-keymaps package (it’s ready to publish actually) but here I only need fs.readdir and only on activation. Anyway here is my code at the moment…

#readdir = null
module.exports =
	#config:
#-------------------------------------------------------------------------------
	subs: null
	activate: ->
		{CompositeDisposable} = require 'atom'
		@subs = new CompositeDisposable

		##{name} = require './package.json'
		{readdir} = require 'fs' #?=

		path = "#{atom.config.configDirPath}/keymap" #process.env.ATOM_HOME
		keymaps = "#{path}s" # folder

		# Load keymaps
		readdir keymaps, (err, files) ->
			if err then throw err
			files.forEach (keymap) ->
				if keymap.endsWith '.cson' #try
					#console.log "#{name}: Loaded #{keymap} keymap." if @debug
					atom.keymaps.loadKeymap "#{keymaps}/#{keymap}"

		# Disable package keymaps by default.
		# FIXME Disables on every load rather than only once on install.
		#key = 'core.packagesWithKeymapsDisabled'
		#array = atom.config.get key
		#if not array or name not in array
		#	atom.packages.getLoadedPackage(name).deactivateKeymaps()
		#	atom.config.pushAtKeyPath key, name

#-------------------------------------------------------------------------------

		@subs.add atom.commands.add 'atom-workspace', #body
			'modular-keymaps:open': => @open [ keymaps, "#{path}.cson" ]
			##{name}:open #application:open-your-keymap

		# Automatically reload modified keymaps
		@subs.add atom.workspace.observeTextEditors (editor) ->
			keymap = editor.getPath()
			editor.onDidSave ->
				if keymap.startsWith(keymaps) and keymap.endsWith '.cson'
					#console.log "#{name}: Reloaded #{keymap} keymap." if @debug
					atom.keymaps.reloadKeymap keymap

#-------------------------------------------------------------------------------
	open: (keymaps) ->
		#exec 'atom -n "$ATOM_HOME"/keymap{s,.cson}'
		atom.open ({ pathsToOpen: keymaps, newWindow: true })
		# TODO atom.project.removePath atom.configDirPath

#-------------------------------------------------------------------------------
	deactivate: ->
		@subs.dispose()
		#readdir = null

Does this look optimal to you? I plan on re/writing my existing packages with a similar pattern (including several I haven’t published yet…)


#9

Since fs is a built-in Node library that is heavily used by Atom, it isn’t necessary to do this optimization at all for it.


#10

@leedohm Could the same be said [heavily used by Atom] about child_process?


#11

I think that is a safe assumption.


#12

I think it doesn’t work like that, since the module’s path is still in the require cache (and updates don’t clear the cache, remember my PR on that subject). The only exception is the package’s main file which is forcefully reloaded - hence the numerous errors when the main file use a new version of an API that haven’t been updated because it comes from another file that was not reloaded.

What I do now in the minimap is to put the following code at the top of my main file:

/*
  The following hack clears the require cache of all the paths to the minimap when this file is loaded. It should prevents errors of partial reloading after an update.
 */
import path from 'path'
if (!atom.inSpecMode()) {
  Object.keys(require.cache).filter((p) => {
    return p !== __filename && p.indexOf(path.resolve(__dirname, '..') + path.sep) > -1
  }).forEach((p) => {
    delete require.cache[p]
  })
}

#13

Thanks for this … I should have been more clear about “theoretically” :laughing:


#14

May be out of topic, but I would like to share my experience, should it be useful to somebody.
My package atom-math was quite slow to activate and load (~300ms on my working machine - Arch Linux with Intel i7 @2.5GHz). This was quite weird, since the package actually does nothing when activated beside adding a few commands.

I used to require mathjs, one of the modules I’m using, at the beginning of the main file, in such fashion:

...
Parser = allowUnsafeEval -> allowUnsafeNewFunction -> require('mathjs').parser()

module.exports = AtomMath =

  activate: (state) ->
    ...

What I did instead, was import the required module when it’s used, like this:

@parser ?= allowUnsafeEval -> allowUnsafeNewFunction -> require('mathjs').parser()

Now, the package takes ~4ms for both loading and activation. This was all due to the delayed module loading.

So well, yes, this worked pretty good for me! Thanks!

(you can find the package here, a new version will be released soon)