From 97feb3321c74b5fdc7aac35eca2bb852800d9120 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 14:06:26 -0800 Subject: [PATCH 1/2] Improve refresh queue Use requestAnimationFrame in addition to a queue to refresh every animation frame but only when a refresh is needed. Fixes #280 Fixes #290 --- src/xterm.js | 98 +++++++++++++++++++++++++--------------------------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 1d220652..79ca633b 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -137,14 +137,8 @@ function Terminal(options) { */ this.y = 0; - /** - * Used to debounce the refresh function - */ - this.isRefreshing = false; - - /** - * Whether there is a full terminal refresh queued - */ + /** A queue of the rows to be refreshed */ + this.refreshRowsQueue = []; this.cursorState = 0; this.cursorHidden = false; @@ -407,7 +401,7 @@ Terminal.prototype.blur = function() { */ Terminal.bindBlur = function (term) { on(term.textarea, 'blur', function (ev) { - term.refresh(term.y, term.y); + term.queueRefresh(term.y, term.y); if (term.sendFocus) { term.send('\x1b[O'); } @@ -585,8 +579,13 @@ Terminal.prototype.open = function(parent) { this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasureElement); - // Draw the screen. - this.refresh(0, this.rows - 1); + // Setup loop that draws to screen + this.queueRefresh(0, this.rows - 1); + function refreshLoop() { + self.refresh(); + window.requestAnimationFrame(refreshLoop); + } + window.requestAnimationFrame(refreshLoop); // Initialize global actions that // need to be taken on the document. @@ -997,6 +996,16 @@ Terminal.flags = { INVISIBLE: 16 } +/** + * Queues a refresh between two rows (inclusive), to be done on next animation + * frame. + * @param {number} start The start row. + * @param {number} end The end row. + */ +Terminal.prototype.queueRefresh = function(start, end) { + this.refreshRowsQueue.push({ start: start, end: end }); +} + /** * Refreshes (re-renders) terminal content within two rows (inclusive) * @@ -1019,44 +1028,33 @@ Terminal.flags = { * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) * @param {boolean} queue Whether the refresh should ran right now or be queued */ -Terminal.prototype.refresh = function(start, end, queue) { - var self = this; - - // queue defaults to true - queue = (typeof queue == 'undefined') ? true : queue; - - /** - * The refresh queue allows refresh to execute only approximately 30 times a second. For - * commands that pass a significant amount of output to the write function, this prevents the - * terminal from maxing out the CPU and making the UI unresponsive. While commands can still - * run beyond what they do on the terminal, it is far better with a debounce in place as - * every single terminal manipulation does not need to be constructed in the DOM. - * - * A side-effect of this is that it makes ^C to interrupt a process seem more responsive. - */ - if (queue) { - // If refresh should be queued, order the refresh and return. - if (this._refreshIsQueued) { - // If a refresh has already been queued, just order a full refresh next - this._fullRefreshNext = true; - } else { - setTimeout(function () { - self.refresh(start, end, false); - }, 34) - this._refreshIsQueued = true; - } +Terminal.prototype.refresh = function() { + if (this.refreshRowsQueue.length === 0) { + // Don't refresh if there were no row changes return; } - // If refresh should be run right now (not be queued), release the lock - this._refreshIsQueued = false; - - // If multiple refreshes were requested, make a full refresh. - if (this._fullRefreshNext) { + var start; + var end; + if (this.refreshRowsQueue.length > 4) { + // Just do a full refresh when 5+ refreshes are queued start = 0; end = this.rows - 1; - this._fullRefreshNext = false // reset lock + } else { + // Get start and end rows that need refreshing + start = this.refreshRowsQueue[0].start; + end = this.refreshRowsQueue[0].end; + for (var i = 1; i < this.refreshRowsQueue.length; i++) { + if (this.refreshRowsQueue[i].start < start) { + start = this.refreshRowsQueue[i].start; + } + if (this.refreshRowsQueue[i].end > end) { + end = this.refreshRowsQueue[i].end; + } + } } + this.refreshRowsQueue = []; + var self = this; var x, y, i, line, out, ch, ch_width, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement; @@ -1224,7 +1222,7 @@ Terminal.prototype.refresh = function(start, end, queue) { Terminal.prototype.showCursor = function() { if (!this.cursorState) { this.cursorState = 1; - this.refresh(this.y, this.y); + this.queueRefresh(this.y, this.y); } }; @@ -1311,7 +1309,7 @@ Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) { this.emit('scroll', this.ydisp); } - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); }; /** @@ -2379,7 +2377,7 @@ Terminal.prototype.write = function(data) { } this.updateRange(this.y); - this.refresh(this.refreshStart, this.refreshEnd); + this.queueRefresh(this.refreshStart, this.refreshEnd); }; /** @@ -2945,7 +2943,7 @@ Terminal.prototype.resize = function(x, y) { this.scrollTop = 0; this.scrollBottom = y - 1; - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.normal = null; @@ -3074,7 +3072,7 @@ Terminal.prototype.clear = function() { for (var i = 1; i < this.rows; i++) { this.lines.push(this.blankLine()); } - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.emit('scroll', this.ydisp); }; @@ -3205,7 +3203,7 @@ Terminal.prototype.reset = function() { var customKeydownHandler = this.customKeydownHandler; Terminal.call(this, this.options); this.customKeydownHandler = customKeydownHandler; - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.viewport.syncScrollArea(); }; @@ -4324,7 +4322,7 @@ Terminal.prototype.resetMode = function(params) { // this.x = this.savedX; // this.y = this.savedY; // } - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.viewport.syncScrollArea(); this.showCursor(); } From 7234bfb6eb637fe3ed388db5b0a78949de7f06d7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 14:58:17 -0800 Subject: [PATCH 2/2] Move row evaluation into refreshLoop This allows the refresh public API to still function --- src/xterm.js | 66 +++++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 79ca633b..cd304186 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -581,11 +581,7 @@ Terminal.prototype.open = function(parent) { // Setup loop that draws to screen this.queueRefresh(0, this.rows - 1); - function refreshLoop() { - self.refresh(); - window.requestAnimationFrame(refreshLoop); - } - window.requestAnimationFrame(refreshLoop); + this.refreshLoop(); // Initialize global actions that // need to be taken on the document. @@ -1006,6 +1002,38 @@ Terminal.prototype.queueRefresh = function(start, end) { this.refreshRowsQueue.push({ start: start, end: end }); } +/** + * Performs the refresh loop callback, calling refresh only if a refresh is + * necessary before queueing up the next one. + */ +Terminal.prototype.refreshLoop = function() { + // Don't refresh if there were no row changes + if (this.refreshRowsQueue.length > 0) { + var start; + var end; + if (this.refreshRowsQueue.length > 4) { + // Just do a full refresh when 5+ refreshes are queued + start = 0; + end = this.rows - 1; + } else { + // Get start and end rows that need refreshing + start = this.refreshRowsQueue[0].start; + end = this.refreshRowsQueue[0].end; + for (var i = 1; i < this.refreshRowsQueue.length; i++) { + if (this.refreshRowsQueue[i].start < start) { + start = this.refreshRowsQueue[i].start; + } + if (this.refreshRowsQueue[i].end > end) { + end = this.refreshRowsQueue[i].end; + } + } + } + this.refreshRowsQueue = []; + this.refresh(start, end); + } + window.requestAnimationFrame(this.refreshLoop.bind(this)); +} + /** * Refreshes (re-renders) terminal content within two rows (inclusive) * @@ -1026,34 +1054,8 @@ Terminal.prototype.queueRefresh = function(start, end) { * * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) - * @param {boolean} queue Whether the refresh should ran right now or be queued */ -Terminal.prototype.refresh = function() { - if (this.refreshRowsQueue.length === 0) { - // Don't refresh if there were no row changes - return; - } - - var start; - var end; - if (this.refreshRowsQueue.length > 4) { - // Just do a full refresh when 5+ refreshes are queued - start = 0; - end = this.rows - 1; - } else { - // Get start and end rows that need refreshing - start = this.refreshRowsQueue[0].start; - end = this.refreshRowsQueue[0].end; - for (var i = 1; i < this.refreshRowsQueue.length; i++) { - if (this.refreshRowsQueue[i].start < start) { - start = this.refreshRowsQueue[i].start; - } - if (this.refreshRowsQueue[i].end > end) { - end = this.refreshRowsQueue[i].end; - } - } - } - this.refreshRowsQueue = []; +Terminal.prototype.refresh = function(start, end) { var self = this; var x, y, i, line, out, ch, ch_width, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement;