Skip to content

Commit f84f6ee

Browse files
committed
Merge pull request #198 from parallaxinc/dom-flash
Major performance improvements in console and TX/RX flash
2 parents dfd7030 + 67716fc commit f84f6ee

File tree

6 files changed

+218
-35
lines changed

6 files changed

+218
-35
lines changed

plugins/editor/index.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,20 @@ const TransmitPane = require('./transmit-pane');
2828

2929
const makeToasts = require('../../src/lib/toasts');
3030

31+
const Scroller = require('./scroller');
32+
3133
function editor(app, opts, done){
3234

3335
var codeEditor;
3436
var outputConsole;
3537
var transmission;
3638
var transmitPane;
39+
var scroller = new Scroller();
3740

3841
function refreshConsole(){
39-
const { text } = consoleStore.getState();
40-
41-
if(outputConsole){
42-
outputConsole.innerHTML = text;
43-
outputConsole.scrollTop = outputConsole.scrollHeight;
44-
}
42+
const { lines } = consoleStore.getState();
43+
scroller.setLines(lines);
44+
scroller.requestRefresh();
4545
}
4646

4747
function highlighter(position, length) {
@@ -130,6 +130,10 @@ function editor(app, opts, done){
130130
outputConsole.style.overflow = 'auto';
131131
outputConsole.style.whiteSpace = 'pre-wrap';
132132
el.appendChild(outputConsole);
133+
134+
scroller.setConsole(outputConsole);
135+
136+
outputConsole.addEventListener('scroll', scroller.scroll, false);
133137
}
134138
if(!transmission) {
135139
transmission = document.createElement('div');

plugins/editor/indicators.js

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,65 @@
11
'use strict';
22

3+
const _ = require('lodash');
34
const React = require('react');
45

56
const styles = require('./styles');
7+
const transmissionStore = require('../../src/stores/transmission');
68

79
class Indicators extends React.Component {
810

9-
render() {
10-
const { flashRx, flashTx } = this.props;
11+
constructor(){
12+
this._updateIndicators = this._updateIndicators.bind(this);
13+
}
1114

12-
let indicatorRx = [styles.indicator];
13-
let indicatorTx = [styles.indicator];
14-
if(flashRx) {
15-
indicatorRx.push(styles.rx);
15+
_updateIndicators(){
16+
const { flashRx, flashTx } = transmissionStore.getState();
17+
if(flashRx){
18+
this.rx.style.backgroundColor = styles.rx.backgroundColor;
19+
} else {
20+
this.rx.style.backgroundColor = styles.indicator.backgroundColor;
1621
}
17-
if(flashTx) {
18-
indicatorTx.push(styles.tx);
22+
if(flashTx){
23+
this.tx.style.backgroundColor = styles.tx.backgroundColor;
24+
} else {
25+
this.tx.style.backgroundColor = styles.indicator.backgroundColor;
1926
}
27+
}
28+
29+
componentDidMount() {
30+
var parent = React.findDOMNode(this);
31+
var tx = this.tx = document.createElement('span');
32+
var rx = this.rx = document.createElement('span');
33+
this.txLabel = document.createTextNode('TX');
34+
this.rxLabel = document.createTextNode(' RX');
35+
parent.appendChild(this.txLabel);
36+
parent.appendChild(tx);
37+
parent.appendChild(this.rxLabel);
38+
parent.appendChild(rx);
39+
_.forEach(styles.indicator, function(style, name){
40+
tx.style[name] = style;
41+
rx.style[name] = style;
42+
});
43+
44+
transmissionStore.listen(this._updateIndicators);
45+
}
2046

47+
componentWillUnmount() {
48+
var parent = React.findDOMNode(this);
49+
parent.removeChild(this.tx);
50+
parent.removeChild(this.rx);
51+
this.tx = null;
52+
this.rx = null;
53+
transmissionStore.unlisten(this._updateIndicators);
54+
}
55+
56+
shouldComponentUpdate() {
57+
return false;
58+
}
59+
60+
render() {
2161
return (
22-
<span style={styles.rxtx}>
23-
TX<span styles={indicatorTx} />RX<span styles={indicatorRx} />
24-
</span>
62+
<span style={styles.rxtx}></span>
2563
);
2664
}
2765
}

plugins/editor/scroller.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
'use strict';
2+
3+
var _ = require('lodash');
4+
5+
function generateContent(lines, start, end, minLength) {
6+
return _(lines)
7+
.slice(start, end)
8+
.thru(function(array){
9+
if(array.length < minLength){
10+
// pad whitespace at top of array
11+
return _(new Array(minLength - array.length))
12+
.fill('\u2009')
13+
.concat(array)
14+
.value();
15+
}else{
16+
return array;
17+
}
18+
})
19+
.map(function(line){
20+
if(line.length === 0){
21+
// insert a blank space to prevent pre omitting a trailing newline,
22+
// even though pre/pre-nowrap/pre-line are specified.
23+
return '\u2009';
24+
}
25+
return line;
26+
})
27+
.join('\n');
28+
}
29+
30+
function Scroller() {
31+
this.lines = [];
32+
this.minVisible = 30;
33+
this.startPosition = 0;
34+
this.animateRequest = null;
35+
this.sticky = true;
36+
this.jumpToBottom = true;
37+
this.dirty = false;
38+
this.console = null;
39+
40+
//pre-bind functions and throttle expansion
41+
this.refresh = this._renderVisible.bind(this);
42+
this.scroll = this._onScroll.bind(this);
43+
this.expand = _.throttle(this._expand.bind(this), 150, {
44+
leading: true,
45+
trailing: true
46+
});
47+
}
48+
49+
Scroller.prototype.setLines = function(newLines) {
50+
var len = newLines.length;
51+
this.lines = newLines;
52+
if(this.sticky){
53+
this.startPosition = Math.max(0, len - this.minVisible);
54+
}else if(len === 1 && newLines[0].length === 0){
55+
// ^^ `lines` is reset to an array with one empty line. ugh.
56+
57+
// handle the reset case when lines is replaced with an empty array
58+
// we don't have a direct event that can call this
59+
this.reset();
60+
}else if(len < this.startPosition){
61+
// handle buffer rollover, where number of lines will go from 2048 to ~1900
62+
this.startPosition = Math.max(0, len - this.minVisible);
63+
}
64+
this.dirty = true;
65+
};
66+
67+
Scroller.prototype.reset = function(){
68+
this.startPosition = Math.max(0, this.lines.length - this.minVisible);
69+
this.jumpToBottom = true;
70+
this.sticky = true;
71+
this.dirty = true;
72+
};
73+
74+
Scroller.prototype.requestRefresh = function(){
75+
if(this.console){
76+
this.animateRequest = requestAnimationFrame(this.refresh);
77+
}
78+
};
79+
80+
Scroller.prototype._renderVisible = function(){
81+
this.animateRequest = null;
82+
if(this.dirty && this.console){
83+
var top = this.console.scrollTop;
84+
if(this.sticky){
85+
this.startPosition = Math.max(0, this.lines.length - this.minVisible);
86+
}
87+
this.console.innerHTML = generateContent(this.lines, this.startPosition, this.lines.length, this.minVisible);
88+
if(this.jumpToBottom){
89+
this.console.scrollTop = 2000;
90+
this.jumpToBottom = false;
91+
}else if(!this.sticky && this.startPosition > 0 && top === 0){
92+
//cover the situation where the window was fully scrolled faster than expand could keep up and locked to the top
93+
requestAnimationFrame(this.expand);
94+
}
95+
this.dirty = false;
96+
}
97+
};
98+
99+
Scroller.prototype._expand = function(){
100+
this.startPosition = Math.max(0, this.startPosition - this.minVisible);
101+
this.sticky = false;
102+
if(this.console){
103+
var scrollHeight = this.console.scrollHeight;
104+
var scrollTop = this.console.scrollTop;
105+
106+
// do an inline scroll to avoid potential scroll interleaving
107+
this.console.innerHTML = generateContent(this.lines, this.startPosition, this.lines.length, this.minVisible);
108+
var newScrollHeight = this.console.scrollHeight;
109+
this.console.scrollTop = scrollTop + newScrollHeight - scrollHeight;
110+
111+
this.dirty = false;
112+
}
113+
};
114+
115+
Scroller.prototype._onScroll = function(){
116+
if(this.jumpToBottom){
117+
// do nothing, prepare to jump
118+
return;
119+
}
120+
var height = this.console.offsetHeight;
121+
var scrollHeight = this.console.scrollHeight;
122+
var scrollTop = this.console.scrollTop;
123+
if(this.sticky){
124+
if(scrollTop + height < scrollHeight - 30){
125+
this.sticky = false;
126+
}
127+
}else{
128+
if(scrollTop < 15 && this.startPosition > 0){
129+
this.expand();
130+
}else if(scrollTop + height > scrollHeight - 30){
131+
this.jumpToBottom = true;
132+
this.sticky = true;
133+
this.dirty = true;
134+
}
135+
}
136+
137+
if(this.dirty && !this.animateRequest){
138+
this.animateRequest = requestAnimationFrame(this.refresh);
139+
}
140+
};
141+
142+
Scroller.prototype.setConsole = function(console){
143+
this.console = console;
144+
};
145+
146+
module.exports = Scroller;

plugins/editor/styles.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ const styles = {
2020
},
2121
indicator: {
2222
backgroundColor: grey,
23-
height: 10,
24-
width: 10,
23+
height: '10px',
24+
width: '10px',
2525
borderRadius: '100%',
2626
margin: '0px 10px',
2727
display: 'inline-block'

plugins/editor/transmission-bar.js

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,23 @@
11
'use strict';
22

33
const React = require('react');
4-
const { createContainer } = require('sovereign');
54

65
const Indicators = require('./indicators');
76
const styles = require('./styles');
8-
const transmissionStore = require('../../src/stores/transmission');
97

108
class TransmissionBar extends React.Component {
119

12-
render() {
13-
const { flashRx, flashTx } = this.props;
10+
shouldComponentUpdate() {
11+
return false;
12+
}
1413

14+
render() {
1515
return (
1616
<div style={styles.bar}>
17-
<Indicators flashRx={flashRx} flashTx={flashTx} />
17+
<Indicators />
1818
</div>
1919
);
2020
}
2121
}
2222

23-
module.exports = createContainer(TransmissionBar, {
24-
getStores(){
25-
return {
26-
deviceStore: transmissionStore
27-
};
28-
},
29-
30-
getPropsFromStores() {
31-
return transmissionStore.getState();
32-
}
33-
});
23+
module.exports = TransmissionBar;

plugins/editor/transmit-pane.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ class TransmitPane extends React.Component {
3838
transmitInput(data);
3939
}
4040

41+
shouldComponentUpdate(nextProps){
42+
const { connected, text } = this.props;
43+
return (connected !== nextProps.connected || text !== nextProps.text);
44+
}
45+
4146
render() {
4247
const { connected, text } = this.props;
4348
return (

0 commit comments

Comments
 (0)