Merge pull request #13949 from atom/as-minimize-startup-sync-io

Minimize synchronous I/O during startup
This commit is contained in:
Antonio Scandurra
2017-03-10 15:00:30 +01:00
committed by GitHub
9 changed files with 133 additions and 151 deletions

View File

@@ -4,27 +4,24 @@ import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers'
import {Emitter, Disposable, CompositeDisposable} from 'event-kit'
import {HistoryManager, HistoryProject} from '../src/history-manager'
import StateStore from '../src/state-store'
describe("HistoryManager", () => {
let historyManager, commandRegistry, project, localStorage, stateStore
let historyManager, commandRegistry, project, stateStore
let commandDisposable, projectDisposable
beforeEach(() => {
beforeEach(async () => {
commandDisposable = jasmine.createSpyObj('Disposable', ['dispose'])
commandRegistry = jasmine.createSpyObj('CommandRegistry', ['add'])
commandRegistry.add.andReturn(commandDisposable)
localStorage = jasmine.createSpyObj('LocalStorage', ['getItem', 'setItem'])
localStorage.items = {
history: JSON.stringify({
projects: [
{ paths: ['/1', 'c:\\2'], lastOpened: new Date(2016, 9, 17, 17, 16, 23) },
{ paths: ['/test'], lastOpened: new Date(2016, 9, 17, 11, 12, 13) }
]
})
}
localStorage.getItem.andCallFake((key) => localStorage.items[key])
localStorage.setItem.andCallFake((key, value) => localStorage.items[key] = value)
stateStore = new StateStore('history-manager-test', 1)
await stateStore.save('history-manager', {
projects: [
{paths: ['/1', 'c:\\2'], lastOpened: new Date(2016, 9, 17, 17, 16, 23)},
{paths: ['/test'], lastOpened: new Date(2016, 9, 17, 11, 12, 13)}
]
})
projectDisposable = jasmine.createSpyObj('Disposable', ['dispose'])
project = jasmine.createSpyObj('Project', ['onDidChangePaths'])
@@ -33,7 +30,12 @@ describe("HistoryManager", () => {
return projectDisposable
})
historyManager = new HistoryManager({project, commands:commandRegistry, localStorage})
historyManager = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry})
await historyManager.loadState()
})
afterEach(async () => {
await stateStore.clear()
})
describe("constructor", () => {
@@ -65,33 +67,28 @@ describe("HistoryManager", () => {
})
describe("clearProjects", () => {
it("clears the list of projects", () => {
it("clears the list of projects", async () => {
expect(historyManager.getProjects().length).not.toBe(0)
historyManager.clearProjects()
await historyManager.clearProjects()
expect(historyManager.getProjects().length).toBe(0)
})
it("saves the state", () => {
expect(localStorage.setItem).not.toHaveBeenCalled()
historyManager.clearProjects()
expect(localStorage.setItem).toHaveBeenCalled()
expect(localStorage.setItem.calls[0].args[0]).toBe('history')
it("saves the state", async () => {
await historyManager.clearProjects()
const historyManager2 = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry})
await historyManager2.loadState()
expect(historyManager.getProjects().length).toBe(0)
})
it("fires the onDidChangeProjects event", () => {
expect(localStorage.setItem).not.toHaveBeenCalled()
historyManager.clearProjects()
expect(localStorage.setItem).toHaveBeenCalled()
expect(localStorage.setItem.calls[0].args[0]).toBe('history')
it("fires the onDidChangeProjects event", async () => {
const didChangeSpy = jasmine.createSpy()
historyManager.onDidChangeProjects(didChangeSpy)
await historyManager.clearProjects()
expect(historyManager.getProjects().length).toBe(0)
expect(didChangeSpy).toHaveBeenCalled()
})
})
it("loads state", () => {
expect(localStorage.getItem).toHaveBeenCalledWith('history')
})
it("listens to project.onDidChangePaths adding a new project", () => {
const start = new Date()
project.didChangePathsListener(['/a/new', '/path/or/two'])
@@ -112,61 +109,61 @@ describe("HistoryManager", () => {
})
describe("loadState", () => {
it("defaults to an empty array if no state", () => {
localStorage.items.history = null
historyManager.loadState()
it("defaults to an empty array if no state", async () => {
await stateStore.clear()
await historyManager.loadState()
expect(historyManager.getProjects()).toEqual([])
})
it("defaults to an empty array if no projects", () => {
localStorage.items.history = JSON.stringify('')
historyManager.loadState()
it("defaults to an empty array if no projects", async () => {
await stateStore.save('history-manager', {})
await historyManager.loadState()
expect(historyManager.getProjects()).toEqual([])
})
})
describe("addProject", () => {
it("adds a new project to the end", () => {
it("adds a new project to the end", async () => {
const date = new Date(2010, 10, 9, 8, 7, 6)
historyManager.addProject(['/a/b'], date)
await historyManager.addProject(['/a/b'], date)
const projects = historyManager.getProjects()
expect(projects.length).toBe(3)
expect(projects[2].paths).toEqual(['/a/b'])
expect(projects[2].lastOpened).toBe(date)
})
it("adds a new project to the start", () => {
it("adds a new project to the start", async () => {
const date = new Date()
historyManager.addProject(['/so/new'], date)
await historyManager.addProject(['/so/new'], date)
const projects = historyManager.getProjects()
expect(projects.length).toBe(3)
expect(projects[0].paths).toEqual(['/so/new'])
expect(projects[0].lastOpened).toBe(date)
})
it("updates an existing project and moves it to the start", () => {
it("updates an existing project and moves it to the start", async () => {
const date = new Date()
historyManager.addProject(['/test'], date)
await historyManager.addProject(['/test'], date)
const projects = historyManager.getProjects()
expect(projects.length).toBe(2)
expect(projects[0].paths).toEqual(['/test'])
expect(projects[0].lastOpened).toBe(date)
})
it("fires the onDidChangeProjects event when adding a project", () => {
it("fires the onDidChangeProjects event when adding a project", async () => {
const didChangeSpy = jasmine.createSpy()
const beforeCount = historyManager.getProjects().length
historyManager.onDidChangeProjects(didChangeSpy)
historyManager.addProject(['/test-new'], new Date())
await historyManager.addProject(['/test-new'], new Date())
expect(didChangeSpy).toHaveBeenCalled()
expect(historyManager.getProjects().length).toBe(beforeCount + 1)
})
it("fires the onDidChangeProjects event when updating a project", () => {
it("fires the onDidChangeProjects event when updating a project", async () => {
const didChangeSpy = jasmine.createSpy()
const beforeCount = historyManager.getProjects().length
historyManager.onDidChangeProjects(didChangeSpy)
historyManager.addProject(['/test'], new Date())
await historyManager.addProject(['/test'], new Date())
expect(didChangeSpy).toHaveBeenCalled()
expect(historyManager.getProjects().length).toBe(beforeCount)
})
@@ -186,14 +183,12 @@ describe("HistoryManager", () => {
})
describe("saveState" ,() => {
it("saves the state", () => {
historyManager.addProject(["/save/state"])
historyManager.saveState()
expect(localStorage.setItem).toHaveBeenCalled()
expect(localStorage.setItem.calls[0].args[0]).toBe('history')
expect(localStorage.items['history']).toContain('/save/state')
historyManager.loadState()
expect(historyManager.getProjects()[0].paths).toEqual(['/save/state'])
it("saves the state", async () => {
await historyManager.addProject(["/save/state"])
await historyManager.saveState()
const historyManager2 = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry})
await historyManager2.loadState()
expect(historyManager2.getProjects()[0].paths).toEqual(['/save/state'])
})
})
})

View File

@@ -1,5 +1,5 @@
_ = require 'underscore-plus'
{screen, ipcRenderer, remote, shell, webFrame} = require 'electron'
{ipcRenderer, remote, shell} = require 'electron'
ipcHelpers = require './ipc-helpers'
{Disposable} = require 'event-kit'
getWindowLoadSettings = require './get-window-load-settings'
@@ -80,6 +80,12 @@ class ApplicationDelegate
setWindowFullScreen: (fullScreen=false) ->
ipcHelpers.call('window-method', 'setFullScreen', fullScreen)
onDidEnterFullScreen: (callback) ->
ipcHelpers.on(ipcRenderer, 'did-enter-full-screen', callback)
onDidLeaveFullScreen: (callback) ->
ipcHelpers.on(ipcRenderer, 'did-leave-full-screen', callback)
openWindowDevTools: ->
# Defer DevTools interaction to the next tick, because using them during
# event handling causes some wrong input events to be triggered on
@@ -254,20 +260,6 @@ class ApplicationDelegate
openExternal: (url) ->
shell.openExternal(url)
disableZoom: ->
outerCallback = ->
webFrame.setZoomLevelLimits(1, 1)
outerCallback()
# Set the limits every time a display is added or removed, otherwise the
# configuration gets reset to the default, which allows zooming the
# webframe.
screen.on('display-added', outerCallback)
screen.on('display-removed', outerCallback)
new Disposable ->
screen.removeListener('display-added', outerCallback)
screen.removeListener('display-removed', outerCallback)
checkForUpdate: ->
ipcRenderer.send('command', 'application:check-for-update')

View File

@@ -215,8 +215,6 @@ class AtomEnvironment extends Model
@stylesElement = @styles.buildStylesElement()
@document.head.appendChild(@stylesElement)
@disposables.add(@applicationDelegate.disableZoom())
@keymaps.subscribeToFileReadFailure()
@keymaps.loadBundledKeymaps()
@@ -231,14 +229,12 @@ class AtomEnvironment extends Model
@observeAutoHideMenuBar()
@history = new HistoryManager({@project, @commands, localStorage})
@history = new HistoryManager({@project, @commands, @stateStore, localStorage: window.localStorage})
# Keep instances of HistoryManager in sync
@history.onDidChangeProjects (e) =>
@disposables.add @history.onDidChangeProjects (e) =>
@applicationDelegate.didChangeHistoryManager() unless e.reloaded
@disposables.add @applicationDelegate.onDidChangeHistoryManager(=> @history.loadState())
(new ReopenProjectMenuManager({@menu, @commands, @history, @config, open: (paths) => @open(pathsToOpen: paths)})).update()
attachSaveStateListeners: ->
saveState = _.debounce((=>
window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded
@@ -716,7 +712,14 @@ class AtomEnvironment extends Model
@openInitialEmptyEditorIfNecessary()
Promise.all([loadStatePromise, updateProcessEnvPromise])
loadHistoryPromise = @history.loadState().then =>
@reopenProjectMenuManager = new ReopenProjectMenuManager({
@menu, @commands, @history, @config,
open: (paths) => @open(pathsToOpen: paths)
})
@reopenProjectMenuManager.update()
Promise.all([loadStatePromise, loadHistoryPromise, updateProcessEnvPromise])
serialize: (options) ->
version: @constructor.version

View File

@@ -4,7 +4,7 @@ let windowLoadSettings = null
module.exports = () => {
if (!windowLoadSettings) {
windowLoadSettings = remote.getCurrentWindow().loadSettings
windowLoadSettings = JSON.parse(remote.getCurrentWindow().loadSettingsJSON)
}
return windowLoadSettings
}

View File

@@ -1,6 +1,6 @@
/** @babel */
import {Emitter} from 'event-kit'
import {Emitter, CompositeDisposable} from 'event-kit'
// Extended: History manager for remembering which projects have been opened.
//
@@ -8,12 +8,18 @@ import {Emitter} from 'event-kit'
//
// The project history is used to enable the 'Reopen Project' menu.
export class HistoryManager {
constructor ({project, commands, localStorage}) {
constructor ({stateStore, localStorage, project, commands}) {
this.stateStore = stateStore
this.localStorage = localStorage
commands.add('atom-workspace', {'application:clear-project-history': this.clearProjects.bind(this)})
this.emitter = new Emitter()
this.loadState()
project.onDidChangePaths((projectPaths) => this.addProject(projectPaths))
this.projects = []
this.disposables = new CompositeDisposable()
this.disposables.add(commands.add('atom-workspace', {'application:clear-project-history': this.clearProjects.bind(this)}))
this.disposables.add(project.onDidChangePaths((projectPaths) => this.addProject(projectPaths)))
}
destroy () {
this.disposables.dispose()
}
// Public: Obtain a list of previously opened projects.
@@ -27,9 +33,12 @@ export class HistoryManager {
//
// Note: This is not a privacy function - other traces will still exist,
// e.g. window state.
clearProjects () {
//
// Return a {Promise} that resolves when the history has been successfully
// cleared.
async clearProjects () {
this.projects = []
this.saveState()
await this.saveState()
this.didChangeProjects()
}
@@ -46,7 +55,7 @@ export class HistoryManager {
this.emitter.emit('did-change-projects', args || { reloaded: false })
}
addProject (paths, lastOpened) {
async addProject (paths, lastOpened) {
if (paths.length === 0) return
let project = this.getProject(paths)
@@ -57,11 +66,11 @@ export class HistoryManager {
project.lastOpened = lastOpened || new Date()
this.projects.sort((a, b) => b.lastOpened - a.lastOpened)
this.saveState()
await this.saveState()
this.didChangeProjects()
}
removeProject (paths) {
async removeProject (paths) {
if (paths.length === 0) return
let project = this.getProject(paths)
@@ -70,7 +79,7 @@ export class HistoryManager {
let index = this.projects.indexOf(project)
this.projects.splice(index, 1)
this.saveState()
await this.saveState()
this.didChangeProjects()
}
@@ -84,31 +93,23 @@ export class HistoryManager {
return null
}
loadState () {
const state = JSON.parse(this.localStorage.getItem('history'))
if (state && state.projects) {
this.projects = state.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened)))
this.didChangeProjects({ reloaded: true })
async loadState () {
let history = await this.stateStore.load('history-manager')
if (!history) {
history = JSON.parse(this.localStorage.getItem('history'))
}
if (history && history.projects) {
this.projects = history.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened)))
this.didChangeProjects({reloaded: true})
} else {
this.projects = []
}
}
saveState () {
const state = JSON.stringify({
projects: this.projects.map(p => ({
paths: p.paths, lastOpened: p.lastOpened
}))
})
this.localStorage.setItem('history', state)
}
async importProjectHistory () {
for (let project of await HistoryImporter.getAllProjects()) {
this.addProject(project.paths, project.lastOpened)
}
this.saveState()
this.didChangeProjects()
async saveState () {
const projects = this.projects.map(p => ({paths: p.paths, lastOpened: p.lastOpened}))
await this.stateStore.save('history-manager', {projects})
}
}
@@ -132,32 +133,3 @@ export class HistoryProject {
set lastOpened (lastOpened) { this._lastOpened = lastOpened }
get lastOpened () { return this._lastOpened }
}
class HistoryImporter {
static async getStateStoreCursor () {
const db = await atom.stateStore.dbPromise
const store = db.transaction(['states']).objectStore('states')
return store.openCursor()
}
static async getAllProjects (stateStore) {
const request = await HistoryImporter.getStateStoreCursor()
return new Promise((resolve, reject) => {
const rows = []
request.onerror = reject
request.onsuccess = event => {
const cursor = event.target.result
if (cursor) {
let project = cursor.value.value.project
let storedAt = cursor.value.storedAt
if (project && project.paths && storedAt) {
rows.push(new HistoryProject(project.paths, new Date(Date.parse(storedAt))))
}
cursor.continue()
} else {
resolve(rows)
}
}
})
}
}

View File

@@ -6,8 +6,8 @@ StorageFolder = require '../storage-folder'
Config = require '../config'
FileRecoveryService = require './file-recovery-service'
ipcHelpers = require '../ipc-helpers'
{BrowserWindow, Menu, app, dialog, ipcMain, shell} = require 'electron'
{CompositeDisposable} = require 'event-kit'
{BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require 'electron'
{CompositeDisposable, Disposable} = require 'event-kit'
fs = require 'fs-plus'
path = require 'path'
os = require 'os'
@@ -94,7 +94,7 @@ class AtomApplication
if process.platform is 'darwin' and @config.get('core.useCustomTitleBar')
@config.unset('core.useCustomTitleBar')
@config.set('core.titleBar', 'custom')
@config.onDidChange 'core.titleBar', @promptForRestart.bind(this)
process.nextTick => @autoUpdateManager.initialize()
@@ -397,6 +397,8 @@ class AtomApplication
@disposable.add ipcHelpers.on ipcMain, 'did-change-paths', =>
@saveState(false)
@disposable.add(@disableZoomOnDisplayChange())
setupDockMenu: ->
if process.platform is 'darwin'
dockMenu = Menu.buildFromTemplate [
@@ -815,3 +817,17 @@ class AtomApplication
args.push("--resource-path=#{@resourcePath}")
app.relaunch({args})
app.quit()
disableZoomOnDisplayChange: ->
outerCallback = =>
for window in @windows
window.disableZoom()
# Set the limits every time a display is added or removed, otherwise the
# configuration gets reset to the default, which allows zooming the
# webframe.
screen.on('display-added', outerCallback)
screen.on('display-removed', outerCallback)
new Disposable ->
screen.removeListener('display-added', outerCallback)
screen.removeListener('display-removed', outerCallback)

View File

@@ -83,12 +83,18 @@ class AtomWindow
@representedDirectoryPaths = loadSettings.initialPaths
@env = loadSettings.env if loadSettings.env?
@browserWindow.loadSettings = loadSettings
@browserWindow.loadSettingsJSON = JSON.stringify(loadSettings)
@browserWindow.on 'window:loaded', =>
@emit 'window:loaded'
@resolveLoadedPromise()
@browserWindow.on 'enter-full-screen', =>
@browserWindow.webContents.send('did-enter-full-screen')
@browserWindow.on 'leave-full-screen', =>
@browserWindow.webContents.send('did-leave-full-screen')
@browserWindow.loadURL url.format
protocol: 'file'
pathname: "#{@resourcePath}/static/index.html"
@@ -101,6 +107,7 @@ class AtomWindow
hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?)
@openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow()
@disableZoom()
@atomApplication.addWindow(this)
@@ -303,3 +310,6 @@ class AtomWindow
@atomApplication.saveState()
copy: -> @browserWindow.copy()
disableZoom: ->
@browserWindow.webContents.setZoomLevelLimits(1, 1)

View File

@@ -58,7 +58,7 @@ export default class ReopenProjectMenuManager {
// Windows users can right-click Atom taskbar and remove project from the jump list.
// We have to honor that or the group stops working. As we only get a partial list
// each time we remove them from history entirely.
applyWindowsJumpListRemovals () {
async applyWindowsJumpListRemovals () {
if (process.platform !== 'win32') return
if (this.app === undefined) {
this.app = require('remote').app
@@ -68,7 +68,7 @@ export default class ReopenProjectMenuManager {
if (removed.length === 0) return
for (let project of this.historyManager.getProjects()) {
if (removed.includes(ReopenProjectMenuManager.taskDescription(project.paths))) {
this.historyManager.removeProject(project.paths)
await this.historyManager.removeProject(project.paths)
}
}
}

View File

@@ -20,14 +20,8 @@ class WindowEventHandler
@subscriptions.add listen(@document, 'click', 'a', @handleLinkClick)
@subscriptions.add listen(@document, 'submit', 'form', @handleFormSubmit)
browserWindow = @applicationDelegate.getCurrentWindow()
browserWindow.on 'enter-full-screen', @handleEnterFullScreen
@subscriptions.add new Disposable =>
browserWindow.removeListener('enter-full-screen', @handleEnterFullScreen)
browserWindow.on 'leave-full-screen', @handleLeaveFullScreen
@subscriptions.add new Disposable =>
browserWindow.removeListener('leave-full-screen', @handleLeaveFullScreen)
@subscriptions.add(@applicationDelegate.onDidEnterFullScreen(@handleEnterFullScreen))
@subscriptions.add(@applicationDelegate.onDidLeaveFullScreen(@handleLeaveFullScreen))
@subscriptions.add @atomEnvironment.commands.add @window,
'window:toggle-full-screen': @handleWindowToggleFullScreen