perf: 60ms improvement: Replace fs-extra with mkdirp/rimraf/cp-file

In Electron apps, the cost of large modules is very real.

fs-extra is very convenient, but removing it caused 50 fewer unique
files to be required(), resultin in 60ms faster startup!

Before: 557 unique requires (1330-1340ms)
After: 507 unique requires (1270-1280ms)
This commit is contained in:
Feross Aboukhadijeh
2016-09-30 19:51:35 -07:00
parent d4cfc32c8d
commit 02f5dbb63f
8 changed files with 66 additions and 58 deletions

View File

@@ -18,6 +18,7 @@
"bitfield": "^1.0.2", "bitfield": "^1.0.2",
"capture-frame": "^1.0.0", "capture-frame": "^1.0.0",
"chromecasts": "^1.8.0", "chromecasts": "^1.8.0",
"cp-file": "^3.2.0",
"create-torrent": "^3.24.5", "create-torrent": "^3.24.5",
"debounce": "^1.0.0", "debounce": "^1.0.0",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
@@ -25,11 +26,11 @@
"drag-drop": "^2.12.1", "drag-drop": "^2.12.1",
"electron": "1.4.2", "electron": "1.4.2",
"es6-error": "^3.0.1", "es6-error": "^3.0.1",
"fs-extra": "^0.30.0",
"iso-639-1": "^1.2.1", "iso-639-1": "^1.2.1",
"languagedetect": "^1.1.1", "languagedetect": "^1.1.1",
"location-history": "^1.0.0", "location-history": "^1.0.0",
"material-ui": "^0.15.4", "material-ui": "^0.15.4",
"mkdirp": "^0.5.1",
"musicmetadata": "^2.0.2", "musicmetadata": "^2.0.2",
"network-address": "^1.1.0", "network-address": "^1.1.0",
"parse-torrent": "^5.7.3", "parse-torrent": "^5.7.3",
@@ -37,6 +38,7 @@
"react": "^15.2.1", "react": "^15.2.1",
"react-dom": "^15.2.1", "react-dom": "^15.2.1",
"react-tap-event-plugin": "^1.0.0", "react-tap-event-plugin": "^1.0.0",
"rimraf": "^2.5.2",
"run-parallel": "^1.1.6", "run-parallel": "^1.1.6",
"semver": "^5.1.0", "semver": "^5.1.0",
"simple-concat": "^1.0.0", "simple-concat": "^1.0.0",
@@ -56,13 +58,11 @@
"electron-winstaller": "^2.3.0", "electron-winstaller": "^2.3.0",
"gh-release": "^2.0.3", "gh-release": "^2.0.3",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"nobin-debian-installer": "^0.0.10", "nobin-debian-installer": "^0.0.10",
"nodemon": "^1.10.2", "nodemon": "^1.10.2",
"open": "0.0.5", "open": "0.0.5",
"plist": "^2.0.1", "plist": "^2.0.1",
"pngjs": "^3.0.0", "pngjs": "^3.0.0",
"rimraf": "^2.5.2",
"run-series": "^1.1.4", "run-series": "^1.1.4",
"spectron": "^3.3.0", "spectron": "^3.3.0",
"standard": "*", "standard": "*",

View File

@@ -273,7 +273,7 @@ function commandToArgs (command) {
} }
function installLinux () { function installLinux () {
const fs = require('fs-extra') const fs = require('fs')
const os = require('os') const os = require('os')
const path = require('path') const path = require('path')
@@ -326,6 +326,8 @@ function installLinux () {
function writeIconFile (err, iconFile) { function writeIconFile (err, iconFile) {
if (err) return log.error(err.message) if (err) return log.error(err.message)
const mkdirp = require('mkdirp')
const iconFilePath = path.join( const iconFilePath = path.join(
os.homedir(), os.homedir(),
'.local', '.local',
@@ -333,9 +335,11 @@ function installLinux () {
'icons', 'icons',
'webtorrent-desktop.png' 'webtorrent-desktop.png'
) )
fs.mkdirp(path.dirname(iconFilePath)) mkdirp(path.dirname(iconFilePath), (err) => {
fs.writeFile(iconFilePath, iconFile, function (err) {
if (err) return log.error(err.message) if (err) return log.error(err.message)
fs.writeFile(iconFilePath, iconFile, (err) => {
if (err) log.error(err.message)
})
}) })
} }
} }
@@ -343,7 +347,7 @@ function installLinux () {
function uninstallLinux () { function uninstallLinux () {
const os = require('os') const os = require('os')
const path = require('path') const path = require('path')
const fs = require('fs-extra') const rimraf = require('rimraf')
const desktopFilePath = path.join( const desktopFilePath = path.join(
os.homedir(), os.homedir(),
@@ -352,7 +356,7 @@ function uninstallLinux () {
'applications', 'applications',
'webtorrent-desktop.desktop' 'webtorrent-desktop.desktop'
) )
fs.removeSync(desktopFilePath) rimraf(desktopFilePath)
const iconFilePath = path.join( const iconFilePath = path.join(
os.homedir(), os.homedir(),
@@ -361,5 +365,5 @@ function uninstallLinux () {
'icons', 'icons',
'webtorrent-desktop.png' 'webtorrent-desktop.png'
) )
fs.removeSync(iconFilePath) rimraf(iconFilePath)
} }

View File

@@ -1,5 +1,5 @@
const electron = require('electron') const electron = require('electron')
const fs = require('fs-extra') const fs = require('fs')
const path = require('path') const path = require('path')
const parallel = require('run-parallel') const parallel = require('run-parallel')

View File

@@ -48,7 +48,7 @@ function run (state) {
} }
function migrate_0_7_0 (saved) { function migrate_0_7_0 (saved) {
const fs = require('fs-extra') const cpFile = require('cp-file')
const path = require('path') const path = require('path')
saved.torrents.forEach(function (ts) { saved.torrents.forEach(function (ts) {
@@ -70,7 +70,7 @@ function migrate_0_7_0 (saved) {
dst = path.join(config.TORRENT_PATH, infoHash + '.torrent') dst = path.join(config.TORRENT_PATH, infoHash + '.torrent')
// Synchronous FS calls aren't ideal, but probably OK in a migration // Synchronous FS calls aren't ideal, but probably OK in a migration
// that only runs once // that only runs once
if (src !== dst) fs.copySync(src, dst) if (src !== dst) cpFile.sync(src, dst)
delete ts.torrentPath delete ts.torrentPath
ts.torrentFileName = infoHash + '.torrent' ts.torrentFileName = infoHash + '.torrent'
@@ -85,7 +85,7 @@ function migrate_0_7_0 (saved) {
dst = path.join(config.POSTER_PATH, infoHash + extension) dst = path.join(config.POSTER_PATH, infoHash + extension)
// Synchronous FS calls aren't ideal, but probably OK in a migration // Synchronous FS calls aren't ideal, but probably OK in a migration
// that only runs once // that only runs once
if (src !== dst) fs.copySync(src, dst) if (src !== dst) cpFile.sync(src, dst)
delete ts.posterURL delete ts.posterURL
ts.posterFileName = infoHash + extension ts.posterFileName = infoHash + extension
@@ -139,7 +139,7 @@ function migrate_0_12_0 (saved) {
if (!fileOrFolder) return if (!fileOrFolder) return
try { try {
fs.statSync(fileOrFolder) fs.statSync(fileOrFolder)
} catch (e) { } catch (err) {
// Default torrent with "missing path" error. Clear path. // Default torrent with "missing path" error. Clear path.
delete torrentSummary.path delete torrentSummary.path
} }

View File

@@ -109,7 +109,8 @@ function getDefaultPlayState () {
/* If the saved state file doesn't exist yet, here's what we use instead */ /* If the saved state file doesn't exist yet, here's what we use instead */
function setupStateSaved (cb) { function setupStateSaved (cb) {
const fs = require('fs-extra') const cpFile = require('cp-file')
const fs = require('fs')
const parseTorrent = require('parse-torrent') const parseTorrent = require('parse-torrent')
const parallel = require('run-parallel') const parallel = require('run-parallel')
@@ -130,18 +131,16 @@ function setupStateSaved (cb) {
config.DEFAULT_TORRENTS.map(function (t, i) { config.DEFAULT_TORRENTS.map(function (t, i) {
const infoHash = saved.torrents[i].infoHash const infoHash = saved.torrents[i].infoHash
tasks.push(function (cb) { tasks.push(function (cb) {
fs.copy( cpFile(
path.join(config.STATIC_PATH, t.posterFileName), path.join(config.STATIC_PATH, t.posterFileName),
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName)), path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName))
cb ).then(cb).catch(cb)
)
}) })
tasks.push(function (cb) { tasks.push(function (cb) {
fs.copy( cpFile(
path.join(config.STATIC_PATH, t.torrentFileName), path.join(config.STATIC_PATH, t.torrentFileName),
path.join(config.TORRENT_PATH, infoHash + '.torrent'), path.join(config.TORRENT_PATH, infoHash + '.torrent')
cb ).then(cb).catch(cb)
)
}) })
}) })
@@ -151,6 +150,7 @@ function setupStateSaved (cb) {
}) })
function createTorrentObject (t) { function createTorrentObject (t) {
// TODO: Doing several fs.readFileSync calls during first startup is not ideal
const torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName)) const torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
const parsedTorrent = parseTorrent(torrent) const parsedTorrent = parseTorrent(torrent)

View File

@@ -6,7 +6,8 @@ const crypto = require('crypto')
const deepEqual = require('deep-equal') const deepEqual = require('deep-equal')
const defaultAnnounceList = require('create-torrent').announceList const defaultAnnounceList = require('create-torrent').announceList
const electron = require('electron') const electron = require('electron')
const fs = require('fs-extra') const fs = require('fs')
const mkdirp = require('mkdirp')
const musicmetadata = require('musicmetadata') const musicmetadata = require('musicmetadata')
const networkAddress = require('network-address') const networkAddress = require('network-address')
const path = require('path') const path = require('path')
@@ -203,19 +204,22 @@ function getTorrentFileInfo (file) {
} }
} }
// Every time we resolve a magnet URI, save the torrent file so that we never // Every time we resolve a magnet URI, save the torrent file so that we can use
// have to download it again. Never ask the DHT the same question twice. // it on next startup. Starting with the full torrent metadata will be faster
// than re-fetching it from peers using ut_metadata.
function saveTorrentFile (torrentKey) { function saveTorrentFile (torrentKey) {
const torrent = getTorrent(torrentKey) const torrent = getTorrent(torrentKey)
checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) { const torrentPath = path.join(config.TORRENT_PATH, torrent.infoHash + '.torrent')
fs.access(torrentPath, fs.constants.R_OK, function (err) {
const fileName = torrent.infoHash + '.torrent' const fileName = torrent.infoHash + '.torrent'
if (exists) { if (!err) {
// We've already saved the file // We've already saved the file
return ipc.send('wt-file-saved', torrentKey, fileName) return ipc.send('wt-file-saved', torrentKey, fileName)
} }
// Otherwise, save the .torrent file, under the app config folder // Otherwise, save the .torrent file, under the app config folder
fs.mkdir(config.TORRENT_PATH, function (_) { mkdirp(config.TORRENT_PATH, function (_) {
fs.writeFile(torrentPath, torrent.torrentFile, function (err) { fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err) if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
console.log('saved torrent file %s', torrentPath) console.log('saved torrent file %s', torrentPath)
@@ -225,15 +229,6 @@ function saveTorrentFile (torrentKey) {
}) })
} }
// Checks whether we've already resolved a given infohash to a torrent file
// Calls back with (torrentPath, exists). Logs, does not call back on error
function checkIfTorrentFileExists (infoHash, cb) {
const torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
fs.exists(torrentPath, function (exists) {
cb(torrentPath, exists)
})
}
// Save a JPG that represents a torrent. // Save a JPG that represents a torrent.
// Auto chooses either a frame from a video file, an image, etc // Auto chooses either a frame from a video file, an image, etc
function generateTorrentPoster (torrentKey) { function generateTorrentPoster (torrentKey) {
@@ -241,7 +236,7 @@ function generateTorrentPoster (torrentKey) {
torrentPoster(torrent, function (err, buf, extension) { torrentPoster(torrent, function (err, buf, extension) {
if (err) return console.log('error generating poster: %o', err) if (err) return console.log('error generating poster: %o', err)
// save it for next time // save it for next time
fs.mkdirp(config.POSTER_PATH, function (err) { mkdirp(config.POSTER_PATH, function (err) {
if (err) return console.log('error creating poster dir: %o', err) if (err) return console.log('error creating poster dir: %o', err)
const posterFileName = torrent.infoHash + extension const posterFileName = torrent.infoHash + extension
const posterFilePath = path.join(config.POSTER_PATH, posterFileName) const posterFilePath = path.join(config.POSTER_PATH, posterFileName)

View File

@@ -1,8 +1,11 @@
const path = require('path')
const Application = require('spectron').Application const Application = require('spectron').Application
const fs = require('fs-extra') const fs = require('fs')
const mkdirp = require('mkdirp')
const parseTorrent = require('parse-torrent') const parseTorrent = require('parse-torrent')
const path = require('path')
const PNG = require('pngjs').PNG const PNG = require('pngjs').PNG
const rimraf = require('rimraf')
const config = require('./config') const config = require('./config')
module.exports = { module.exports = {
@@ -75,8 +78,13 @@ function endTest (app, t, err) {
function screenshotCreateOrCompare (app, t, name) { function screenshotCreateOrCompare (app, t, name) {
const ssDir = path.join(__dirname, 'screenshots', process.platform) const ssDir = path.join(__dirname, 'screenshots', process.platform)
const ssPath = path.join(ssDir, name + '.png') const ssPath = path.join(ssDir, name + '.png')
fs.ensureFileSync(ssPath) let ssBuf
const ssBuf = fs.readFileSync(ssPath)
try {
ssBuf = fs.readFileSync(ssPath)
} catch (err) {
ssBuf = Buffer.alloc(0)
}
return wait().then(function () { return wait().then(function () {
return app.browserWindow.capturePage() return app.browserWindow.capturePage()
}).then(function (buffer) { }).then(function (buffer) {
@@ -136,14 +144,14 @@ function compareIgnoringTransparency (bufActual, bufExpected) {
// Resets the test directory, containing config.json, torrents, downloads, etc // Resets the test directory, containing config.json, torrents, downloads, etc
function resetTestDataDir () { function resetTestDataDir () {
fs.removeSync(config.TEST_DIR) rimraf.sync(config.TEST_DIR)
// Create TEST_DIR as well as /Downloads and /Desktop // Create TEST_DIR as well as /Downloads and /Desktop
fs.mkdirpSync(config.TEST_DIR_DOWNLOAD) mkdirp.sync(config.TEST_DIR_DOWNLOAD)
fs.mkdirpSync(config.TEST_DIR_DESKTOP) mkdirp.sync(config.TEST_DIR_DESKTOP)
} }
function deleteTestDataDir () { function deleteTestDataDir () {
fs.removeSync(config.TEST_DIR) rimraf.sync(config.TEST_DIR)
} }
// Checks a given folder under Downloads. // Checks a given folder under Downloads.
@@ -159,11 +167,11 @@ function compareDownloadFolder (t, dirname, filenames) {
const expectedSorted = filenames.slice().sort() const expectedSorted = filenames.slice().sort()
const actualSorted = actualFilenames.slice().sort() const actualSorted = actualFilenames.slice().sort()
t.deepEqual(actualSorted, expectedSorted, 'download folder contents: ' + dirname) t.deepEqual(actualSorted, expectedSorted, 'download folder contents: ' + dirname)
} catch (e) { } catch (err) {
if (e.code === 'ENOENT') { if (err.code === 'ENOENT') {
t.equal(filenames, null, 'download folder missing: ' + dirname) t.equal(filenames, null, 'download folder missing: ' + dirname)
} else { } else {
console.error(e) console.error(err)
t.fail('unexpected error getting download folder: ' + dirname) t.fail('unexpected error getting download folder: ' + dirname)
} }
} }
@@ -200,14 +208,14 @@ function extractImportantFields (parsedTorrent) {
} }
function copy (pathFrom, pathTo) { function copy (pathFrom, pathTo) {
const cpFile = require('cp-file')
try { try {
fs.copySync(pathFrom, pathTo) cpFile.sync(pathFrom, pathTo)
} catch (e) { } catch (err) {
// There is a bug in either node or `fs-extra` on windows
// Windows lets us create files and folders under C:\Windows\Temp, // Windows lets us create files and folders under C:\Windows\Temp,
// but when you try to `copySync` into one of those folders, you get EPERM // but when you try to `copySync` into one of those folders, you get EPERM
// Ignore for now... // Ignore for now...
if (process.platform !== 'win32' || e.code !== 'EPERM') throw e if (process.platform !== 'win32' || err.code !== 'EPERM') throw err
console.log('ignoring windows copy EPERM error', e) console.log('ignoring windows copy EPERM error', err)
} }
} }

View File

@@ -1,11 +1,12 @@
const rimraf = require('rimraf')
const test = require('tape') const test = require('tape')
const fs = require('fs-extra')
const setup = require('./setup')
const config = require('./config') const config = require('./config')
const setup = require('./setup')
test('torrent-list: show download path missing', function (t) { test('torrent-list: show download path missing', function (t) {
setup.resetTestDataDir() setup.resetTestDataDir()
fs.removeSync(config.TEST_DIR_DOWNLOAD) rimraf.sync(config.TEST_DIR_DOWNLOAD)
t.timeoutAfter(20e3) t.timeoutAfter(20e3)
const app = setup.createApp() const app = setup.createApp()