From 54c427927006563ba4d7c5db28a6603dc30040bd Mon Sep 17 00:00:00 2001 From: DC Date: Sat, 26 Mar 2016 22:46:55 -0700 Subject: [PATCH] Speed up init() by >= 2x Lazy load the WebTorrent, Chromecast, and Airplay modules --- renderer/index.css | 28 ------ renderer/index.html | 1 - renderer/index.js | 172 +++++++++++++++++++++------------ renderer/lib/cast.js | 2 +- renderer/state.js | 1 + renderer/views/torrent-list.js | 4 +- 6 files changed, 113 insertions(+), 95 deletions(-) diff --git a/renderer/index.css b/renderer/index.css index 0a74d27d..b27a7c93 100644 --- a/renderer/index.css +++ b/renderer/index.css @@ -49,34 +49,6 @@ table { background-color: rgb(40, 40, 40); } -.loading { - display: flex; - flex-direction: column; - justify-content: center; - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; -} - -.loading .icon { - font-size: 42px; - display: block; - text-align: center; - animation: spin-ccw 2s infinite linear; -} - -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -@keyframes spin-ccw { - from { transform: rotate(360deg); } - to { transform: rotate(0deg); } -} - @keyframes fadein { from { opacity: 0; } to { opacity: 1; } diff --git a/renderer/index.html b/renderer/index.html index 93b1f586..4d04d3cb 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -6,7 +6,6 @@ -
sync
diff --git a/renderer/index.js b/renderer/index.js index 1dd3a755..8accb85e 100644 --- a/renderer/index.js +++ b/renderer/index.js @@ -12,19 +12,22 @@ var musicmetadata = require('musicmetadata') var networkAddress = require('network-address') var path = require('path') var remote = require('remote') -var WebTorrent = require('webtorrent') var createElement = require('virtual-dom/create-element') var diff = require('virtual-dom/diff') var patch = require('virtual-dom/patch') var App = require('./views/app') -var Cast = require('./lib/cast') var errors = require('./lib/errors') var config = require('../config') var TorrentPlayer = require('./lib/torrent-player') var torrentPoster = require('./lib/torrent-poster') +// These two dependencies are the slowest-loading, so we lazy load them +// This cuts time from icon click to rendered window from ~550ms to ~150ms on my laptop +var WebTorrent = null +var Cast = null + // Electron apps have two processes: a main process (node) runs first and starts // a renderer process (essentially a Chrome window). We're in the renderer process, // and this IPC channel receives from and sends messages to the main process @@ -53,20 +56,11 @@ loadState(init) function init () { state.location.go({ url: 'home' }) - // Connect to the WebTorrent and BitTorrent networks - // WebTorrent.app is a hybrid client, as explained here: https://webtorrent.io/faq - state.client = new WebTorrent() - state.client.on('warning', onWarning) - state.client.on('error', function (err) { - // TODO: WebTorrent should have semantic errors - if (err.message.startsWith('There is already a swarm')) { - onError(new Error('Couldn\'t add duplicate torrent')) - } else { - onError(err) - } - }) - resumeTorrents() /* restart everything we were torrenting last time the app ran */ - setInterval(updateTorrentProgress, 1000) + // Lazily load the WebTorrent, Chromecast, and Airplay modules + window.setTimeout(function () { + lazyLoadClient() + lazyLoadCast() + }, 100) // The UI is built with virtual-dom, a minimalist library extracted from React // The concepts--one way data flow, a pure function that renders state to a @@ -79,23 +73,13 @@ function init () { }) document.body.appendChild(vdomLoop.target) - // Calling update() updates the UI given the current state - // Do this at least once a second to show latest state for each torrent - // (eg % downloaded) and to keep the cursor in sync when playing a video - setInterval(function () { - update() - updateClientProgress() - }, 1000) - + // Save state on exit window.addEventListener('beforeunload', saveState) - // listen for messages from the main process + // Listen for messages from the main process setupIpc() // OS integrations: - // ...Chromecast and Airplay - Cast.init(update) - // ...drag and drop a torrent or video file to play or seed dragDrop('body', (files) => dispatch('onOpen', files)) @@ -130,11 +114,56 @@ function init () { }) // Done! Ideally we want to get here <100ms after the user clicks the app - document.querySelector('.loading').remove() /* TODO: no spinner once fast enough */ playInterfaceSound('STARTUP') console.timeEnd('init') } +// Lazily loads the WebTorrent module and creates the WebTorrent client +function lazyLoadClient () { + if (!WebTorrent) initWebtorrent() + return state.client +} + +// Lazily loads Chromecast and Airplay support +function lazyLoadCast () { + if (!Cast) { + Cast = require('./lib/cast') + Cast.init(update) // Search the local network for Chromecast and Airplays + } + return Cast +} + +// Load the WebTorrent module, connect to both the WebTorrent and BitTorrent +// networks, resume torrents, start monitoring torrent progress +function initWebtorrent () { + WebTorrent = require('webtorrent') + + // Connect to the WebTorrent and BitTorrent networks + // WebTorrent.app is a hybrid client, as explained here: https://webtorrent.io/faq + state.client = new WebTorrent() + state.client.on('warning', onWarning) + state.client.on('error', function (err) { + // TODO: WebTorrent should have semantic errors + if (err.message.startsWith('There is already a swarm')) { + onError(new Error('Couldn\'t add duplicate torrent')) + } else { + onError(err) + } + }) + + // Restart everything we were torrenting last time the app ran + resumeTorrents() + + // Calling update() updates the UI given the current state + // Do this at least once a second to give every file in every torrentSummary + // a progress bar and to keep the cursor in sync when playing a video + setInterval(function () { + if (!updateTorrentProgress()) { + update() // If we didn't just update(), do so now, for the video cursor + } + }, 1000) +} + // This is the (mostly) pure function from state -> UI. Returns a virtual DOM // tree. Any events, such as button clicks, will turn into calls to dispatch() function render (state) { @@ -188,7 +217,6 @@ function dispatch (action, ...args) { state.location.go({ url: 'player', onbeforeload: function (cb) { - // TODO: handle audio. video only for now. openPlayer(args[0] /* torrentSummary */, args[1] /* index */, cb) }, onbeforeunload: closePlayer @@ -210,13 +238,13 @@ function dispatch (action, ...args) { toggleSelectTorrent(args[0] /* infoHash */) } if (action === 'openChromecast') { - Cast.openChromecast() + lazyLoadCast().openChromecast() } if (action === 'openAirplay') { - Cast.openAirplay() + lazyLoadCast().openAirplay() } if (action === 'stopCasting') { - Cast.stopCasting() + lazyLoadCast().stopCasting() } if (action === 'setDimensions') { setDimensions(args[0] /* dimensions */) @@ -272,7 +300,7 @@ function playPause (isPaused) { return // Nothing to do } // Either isPaused is undefined, or it's the opposite of the current state. Toggle. - if (Cast.isCasting()) { + if (lazyLoadCast().isCasting()) { Cast.playPause() } state.playing.isPaused = !state.playing.isPaused @@ -280,7 +308,7 @@ function playPause (isPaused) { } function jumpToTime (time) { - if (Cast.isCasting()) { + if (lazyLoadCast().isCasting()) { Cast.seek(time) } else { state.playing.jumpToTime = time @@ -296,7 +324,7 @@ function changeVolume (delta) { function setVolume (volume) { // check if its in [0.0 - 1.0] range volume = Math.max(0, Math.min(1, volume)) - if (Cast.isCasting()) { + if (lazyLoadCast().isCasting()) { Cast.setVolume(volume) } else { state.playing.setVolume = volume @@ -362,18 +390,6 @@ function saveState () { }) } -function updateClientProgress () { - var progress = state.client.progress - var activeTorrentsExist = state.client.torrents.some(function (torrent) { - return torrent.progress !== 1 - }) - // Hide progress bar when client has no torrents, or progress is 100% - if (!activeTorrentsExist || progress === 1) { - progress = -1 - } - state.dock.progress = progress -} - function onOpen (files) { if (!Array.isArray(files)) files = [ files ] @@ -417,7 +433,9 @@ function getTorrentSummary (infoHash) { // Get an active torrent from state.client.torrents // Returns undefined if we are not currently torrenting that infoHash function getTorrent (infoHash) { - return state.client.torrents.find((x) => x.infoHash === infoHash) + var pending = state.pendingTorrents[infoHash] + if (pending) return pending + return lazyLoadClient().torrents.find((x) => x.infoHash === infoHash) } // Adds a torrent to the list, starts downloading/seeding. TorrentID can be a @@ -454,16 +472,22 @@ function addTorrentToList (torrent) { // Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object function startTorrentingSummary (torrentSummary) { var s = torrentSummary - if (s.torrentPath) return startTorrentingID(s.torrentPath, s.path) - else if (s.magnetURI) return startTorrentingID(s.magnetURI, s.path) - else return startTorrentingID(s.infoHash, s.path) + if (s.torrentPath) { + var ret = startTorrentingID(s.torrentPath, s.path) + if (s.infoHash) state.pendingTorrents[s.infoHash] = ret + return ret + } else if (s.magnetURI) { + return startTorrentingID(s.magnetURI, s.path) + } else { + return startTorrentingID(s.infoHash, s.path) + } } // Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object // See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent- function startTorrentingID (torrentID, path) { - console.log('Starting torrent ' + torrentID) - var torrent = state.client.add(torrentID, { + console.log('starting torrent ' + torrentID) + var torrent = lazyLoadClient().add(torrentID, { path: path || state.saved.downloadPath // Use downloads folder }) addTorrentEvents(torrent) @@ -479,13 +503,17 @@ function stopTorrenting (infoHash) { // Creates a torrent for a local file and starts seeding it function seed (files) { if (files.length === 0) return - var torrent = state.client.seed(files) + var torrent = lazyLoadClient().seed(files) addTorrentToList(torrent) addTorrentEvents(torrent) } function addTorrentEvents (torrent) { - torrent.on('infoHash', update) + torrent.on('infoHash', function () { + var infoHash = torrent.infoHash + if (state.pendingTorrents[infoHash]) delete state.pendingTorrents[infoHash] + update() + }) torrent.on('ready', torrentReady) torrent.on('done', torrentDone) @@ -531,10 +559,25 @@ function addTorrentEvents (torrent) { } function updateTorrentProgress () { + var changed = false + + // First, track overall progress + var progress = lazyLoadClient().progress + var activeTorrentsExist = lazyLoadClient().torrents.some(function (torrent) { + return torrent.progress !== 1 + }) + // Hide progress bar when client has no torrents, or progress is 100% + if (!activeTorrentsExist || progress === 1) { + progress = -1 + } + // Show progress bar under the WebTorrent taskbar icon, on OSX + if (state.dock.progress !== progress) changed = true + state.dock.progress = progress + + // Track progress for every file in each torrentSummary // TODO: ideally this would be tracked by WebTorrent, which could do it // more efficiently than looping over torrent.bitfield - var changed = false - state.client.torrents.forEach(function (torrent) { + lazyLoadClient().torrents.forEach(function (torrent) { var torrentSummary = getTorrentSummary(torrent.infoHash) if (!torrentSummary || !torrent.ready) return torrent.files.forEach(function (file, index) { @@ -554,6 +597,7 @@ function updateTorrentProgress () { }) if (changed) update() + return changed } function generateTorrentPoster (torrent, torrentSummary) { @@ -597,8 +641,8 @@ function saveTorrentFile (torrentSummary, torrent) { // Otherwise, save the .torrent file, under the app config folder fs.mkdir(config.CONFIG_TORRENT_PATH, function (_) { fs.writeFile(torrentPath, torrent.torrentFile, function (err) { - if (err) return console.log('Error saving torrent file %s: %o', torrentPath, err) - console.log('Saved torrent file %s', torrentPath) + if (err) return console.log('error saving torrent file %s: %o', torrentPath, err) + console.log('saved torrent file %s', torrentPath) torrentSummary.torrentPath = torrentPath saveState() }) @@ -639,7 +683,7 @@ function startServerFromReadyTorrent (torrent, index, cb) { // if it's audio, parse out the metadata (artist, title, etc) musicmetadata(file.createReadStream(), function (err, info) { if (err) return - console.log('Got audio metadata for %s: %v', file.name, info) + console.log('got audio metadata for %s: %v', file.name, info) state.playing.audioInfo = info update() }) @@ -690,7 +734,7 @@ function stopServer () { // Opens the video player function openPlayer (torrentSummary, index, cb) { - var torrent = state.client.get(torrentSummary.infoHash) + var torrent = lazyLoadClient().get(torrentSummary.infoHash) if (!torrent || !torrent.done) playInterfaceSound('PLAY') torrentSummary.playStatus = 'requested' update() @@ -743,7 +787,7 @@ function closePlayer (cb) { } function openFile (torrentSummary, index) { - var torrent = state.client.get(torrentSummary.infoHash) + var torrent = lazyLoadClient().get(torrentSummary.infoHash) if (!torrent) return var filePath = path.join(torrent.path, torrent.files[index].path) @@ -751,7 +795,7 @@ function openFile (torrentSummary, index) { } function openFolder (torrentSummary) { - var torrent = state.client.get(torrentSummary.infoHash) + var torrent = lazyLoadClient().get(torrentSummary.infoHash) if (!torrent) return var folderPath = path.join(torrent.path, torrent.name) diff --git a/renderer/lib/cast.js b/renderer/lib/cast.js index debc01ef..237289ca 100644 --- a/renderer/lib/cast.js +++ b/renderer/lib/cast.js @@ -57,7 +57,7 @@ function addAirplayEvents () {} function pollCastStatus (state) { if (state.playing.location === 'chromecast') { state.devices.chromecast.status(function (err, status) { - if (err) return console.log('Error getting %s status: %o', state.playing.location, err) + if (err) return console.log('error getting %s status: %o', state.playing.location, err) state.playing.isPaused = status.playerState === 'PAUSED' state.playing.currentTime = status.currentTime state.playing.volume = status.volume.muted ? 0 : status.volume.level diff --git a/renderer/state.js b/renderer/state.js index c626acfb..42c66475 100644 --- a/renderer/state.js +++ b/renderer/state.js @@ -30,6 +30,7 @@ module.exports = { isPaused: true, mouseStationarySince: 0 /* Unix time in ms */ }, + pendingTorrents: {}, /* infohash to WebTorrent handle */ devices: { /* playback devices like Chromecast and AppleTV */ airplay: null, /* airplay client. finds and manages AppleTVs */ chromecast: null /* chromecast client. finds and manages Chromecasts */ diff --git a/renderer/views/torrent-list.js b/renderer/views/torrent-list.js index 4d613149..5c087739 100644 --- a/renderer/views/torrent-list.js +++ b/renderer/views/torrent-list.js @@ -24,7 +24,9 @@ function TorrentList (state, dispatch) { function renderTorrent (torrentSummary) { // Get ephemeral data (like progress %) directly from the WebTorrent handle var infoHash = torrentSummary.infoHash - var torrent = state.client.torrents.find((x) => x.infoHash === infoHash) + var torrent = state.client + ? state.client.torrents.find((x) => x.infoHash === infoHash) + : null var isSelected = state.selectedInfoHash === infoHash // Background image: show some nice visuals, like a frame from the movie, if possible