Merge pull request #33 from Luminarys/master

Improve reducing and rendering performance
master
Drew DeVault 2018-02-26 08:25:21 -05:00 committed by GitHub
commit d15c787469
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 155 additions and 123 deletions

View File

@ -12,21 +12,17 @@ function hack(old, _new) {
export default function resourceReducer(type) { export default function resourceReducer(type) {
return (state = {}, action) => { return (state = {}, action) => {
let ns = {...state};
switch (action.type) { switch (action.type) {
case UPDATE_RESOURCES: case UPDATE_RESOURCES:
return { action.resources
...state, .filter(r => r.type ===type)
...action.resources .map(r => ns[r.id] = hack(state[r.id], { ...state[r.id], ...r }));
.filter(r => r.type === type) return ns;
.reduce((s, r) => ({
...s,
[r.id]: hack(state[r.id], { ...state[r.id], ...r })
}), {})
};
case RESOURCES_REMOVED: case RESOURCES_REMOVED:
return Object.values(state) action.ids
.filter(r => action.ids.indexOf(r.id) === -1) .map(id => delete ns[id]);
.reduce((s, r) => ({ ...s, [r.id]: r }), {}); return ns;
case SOCKET_UPDATE: case SOCKET_UPDATE:
const _state = action.state; const _state = action.state;
return _state === SOCKET_STATE.CONNECTING ? {} : state; return _state === SOCKET_STATE.CONNECTING ? {} : state;

View File

@ -8,12 +8,12 @@ export default function subscribe(state = [], action) {
return [ ...state, ...ids.map(id => ({ serial, id })) ]; return [ ...state, ...ids.map(id => ({ serial, id })) ];
} }
case UNSUBSCRIBE: { case UNSUBSCRIBE: {
const { ids } = action; const ids = new Set(action.ids);
return state.filter(sub => ids.indexOf(sub.id) === -1); return state.filter(sub => !ids.has(sub.id));
} }
case RESOURCES_REMOVED: { case RESOURCES_REMOVED: {
const { ids } = action; const ids = new Set(action.ids);
return state.filter(sub => ids.indexOf(sub.id) === -1); return state.filter(sub => !ids.has(sub.id));
} }
} }
return state; return state;

View File

@ -35,55 +35,69 @@ function basename(path) {
return parts[parts.length - 1]; return parts[parts.length - 1];
} }
function File({ dispatch, file }) { class File extends Component {
const { uri } = store.getState().socket; shouldComponentUpdate(nextProps, _) {
const { download_token } = store.getState().server; return nextProps.file !== this.props.file;
return ( }
<div className="file">
<Progress render() {
value={file.progress * 100} const { dispatch, file } = this.props;
color={file.progress != 1.0 ? "success" : "primary"} const { uri } = store.getState().socket;
> const { download_token } = store.getState().server;
{file.progress === 1.0 ? return (
"done" : `${(file.progress * 100).toFixed(0)}%`} <div className="file">
</Progress> <Progress
<div className="path" title={file.path}> value={file.progress * 100}
{file.progress === 1.0 ? color={file.progress != 1.0 ? "success" : "primary"}
<a href={dlURI(uri, download_token, file.id)} target="_new">
{basename(file.path)}
</a> : basename(file.path)}
</div>
<div>
<Input
type="select"
id="priority"
value={file.priority}
onChange={e => dispatch(updateResource({
id: file.id,
priority: parseInt(e.target.value)
}))}
> >
<option value="0">Skip</option> {file.progress === 1.0 ?
<option value="1">Lowest</option> "done" : `${(file.progress * 100).toFixed(0)}%`}
<option value="2">Low</option> </Progress>
<option value="3">Normal</option> <div className="path" title={file.path}>
<option value="4">High</option> {file.progress === 1.0 ?
<option value="5">Highest</option> <a href={dlURI(uri, download_token, file.id)} target="_new">
</Input> {basename(file.path)}
</a> : basename(file.path)}
</div>
<div>
<Input
type="select"
id="priority"
value={file.priority}
onChange={e => dispatch(updateResource({
id: file.id,
priority: parseInt(e.target.value)
}))}
>
<option value="0">Skip</option>
<option value="1">Lowest</option>
<option value="2">Low</option>
<option value="3">Normal</option>
<option value="4">High</option>
<option value="5">Highest</option>
</Input>
</div>
</div> </div>
</div> );
); }
} }
function Peer({ peer }) { class Peer extends Component {
return ( shouldComponentUpdate(nextProps, _) {
<div className="peer"> return nextProps.peer !== this.props.peer;
<div style={{flexGrow: 5}}>{peer.ip}</div> }
<div style={{flexBasis: "18%"}}>{formatBitrate(peer.rate_up)} up</div>
<div style={{flexBasis: "18%"}}>{formatBitrate(peer.rate_down)} down</div> render() {
<div style={{flexBasis: "18%"}}>has {`${(peer.availability * 100).toFixed(0)}%`}</div> const { peer } = this.props;
</div> return (
); <div className="peer">
<div style={{flexGrow: 5}}>{peer.ip}</div>
<div style={{flexBasis: "18%"}}>{formatBitrate(peer.rate_up)} up</div>
<div style={{flexBasis: "18%"}}>{formatBitrate(peer.rate_down)} down</div>
<div style={{flexBasis: "18%"}}>has {`${(peer.availability * 100).toFixed(0)}%`}</div>
</div>
);
}
} }
// TODO: move to separate component // TODO: move to separate component
@ -114,6 +128,14 @@ class Torrent extends Component {
}; };
} }
shouldComponentUpdate(nextProps, nextState) {
return nextProps.torrent !== this.props.torrent
|| nextState.peersShown !== this.state.peersShown
|| nextState.trackersShown !== this.state.trackersShown
|| nextState.filesShown !== this.state.filesShown
|| nextState.infoShown !== this.state.infoShown;
}
toggleTorrentState(torrent) { toggleTorrentState(torrent) {
if (torrent.status === "paused") { if (torrent.status === "paused") {
ws_send("RESUME_TORRENT", { id: torrent.id }); ws_send("RESUME_TORRENT", { id: torrent.id });
@ -222,9 +244,11 @@ class Torrent extends Component {
<Card style={{marginBottom: "1rem"}}> <Card style={{marginBottom: "1rem"}}>
<CardBlock style={{padding: "0"}}> <CardBlock style={{padding: "0"}}>
<div className="files flex-table" style={{marginBottom: "0"}}> <div className="files flex-table" style={{marginBottom: "0"}}>
{files.slice().sort((a, b) => {this.state.filesShown
a.path.localeCompare(b.path)).map(file => ? files.slice()
<File dispatch={dispatch} file={file} />)} .sort((a, b) => a.path.localeCompare(b.path))
.map(file => <File dispatch={dispatch} file={file}/>)
: null}
</div> </div>
</CardBlock> </CardBlock>
</Card> </Card>
@ -264,7 +288,7 @@ class Torrent extends Component {
<Card style={{marginBottom: "1rem"}}> <Card style={{marginBottom: "1rem"}}>
<CardBlock style={{padding: "0"}}> <CardBlock style={{padding: "0"}}>
<div className="peers flex-table" style={{marginBottom: "0"}}> <div className="peers flex-table" style={{marginBottom: "0"}}>
{peers.map(peer => <Peer peer={peer} />)} {this.state.peersShown ? peers.map(peer => <Peer peer={peer} />) : null}
</div> </div>
{peers.length === 0 && {peers.length === 0 &&
<div style={{padding: "0.5rem 0 0 0.5rem"}}> <div style={{padding: "0.5rem 0 0 0.5rem"}}>
@ -361,15 +385,21 @@ class TorrentDetails extends Component {
selection, selection,
dispatch dispatch
} = this.props; } = this.props;
const _files = Object.values(files).reduce((s, f) => ({ const index_by_tid = (res) => {
...s, [f.torrent_id]: [...(s[f.torrent_id] || []), f] let _indexed = {};
}), {}); Object.values(res).map(r => {
const _trackers = Object.values(trackers).reduce((s, t) => ({ if (!(r.torrent_id in _indexed)) {
...s, [t.torrent_id]: [...(s[t.torrent_id] || []), t] _indexed[r.torrent_id] = [];
}), {}); }
const _peers = Object.values(peers).reduce((s, p) => ({ _indexed[r.torrent_id].push(r);
...s, [p.torrent_id]: [...(s[p.torrent_id] || []), p] });
}), {}); return _indexed;
};
const _files = index_by_tid(files);
const _trackers = index_by_tid(trackers);
const _peers = index_by_tid(peers);
return ( return (
<div> <div>
{selection.length > 1 ? this.renderHeader.bind(this)() : null} {selection.length > 1 ? this.renderHeader.bind(this)() : null}

View File

@ -12,55 +12,7 @@ const name_style = {
whiteSpace: 'nowrap' whiteSpace: 'nowrap'
}; };
class TorrentTable extends Component { class _Torrent extends Component {
render() {
const { selection, torrents, dispatch } = this.props;
return (
<table className="table torrents">
<thead>
<tr>
<th style={{width: "1px"}}>
<input
type="checkbox"
checked={selection.length === Object.values(torrents).length}
onChange={e => {
if (selection.length > 0) {
dispatch(selectTorrent([], EXCLUSIVE));
} else {
dispatch(selectTorrent(Object.keys(torrents), EXCLUSIVE));
}
}}
/>
</th>
<th style={name_style}>name</th>
<th style={{ minWidth: '75px' }}>up</th>
<th style={{ minWidth: '75px' }}>down</th>
<th style={{width: "18rem"}}>
<span class="ratio">
<span>ratio</span>
<span></span>
<span></span>
</span>
</th>
<th>progress</th>
</tr>
</thead>
<tbody>
{Object.values(torrents).slice().sort((a, b) => a.name.localeCompare(b.name)).map(t =>
<Torrent
dispatch={dispatch}
torrent={t}
selection={selection}
key={t.id}
/>
)}
</tbody>
</table>
);
}
}
class Torrent extends Component {
shouldComponentUpdate(nextProps, _) { shouldComponentUpdate(nextProps, _) {
const { selection, torrent } = this.props; const { selection, torrent } = this.props;
const nt = nextProps.torrent; const nt = nextProps.torrent;
@ -111,6 +63,60 @@ class Torrent extends Component {
} }
} }
const Torrent = connect((state, props) => {
return {
torrent: state.torrents[props.id],
selection: state.selection,
};
})(_Torrent);
class TorrentTable extends Component {
render() {
const { selection, torrents, dispatch } = this.props;
return (
<table className="table torrents">
<thead>
<tr>
<th style={{width: "1px"}}>
<input
type="checkbox"
checked={selection.length === Object.values(torrents).length}
onChange={e => {
if (selection.length > 0) {
dispatch(selectTorrent([], EXCLUSIVE));
} else {
dispatch(selectTorrent(Object.keys(torrents), EXCLUSIVE));
}
}}
/>
</th>
<th style={name_style}>name</th>
<th style={{ minWidth: '75px' }}>up</th>
<th style={{ minWidth: '75px' }}>down</th>
<th style={{width: "18rem"}}>
<span class="ratio">
<span>ratio</span>
<span></span>
<span></span>
</span>
</th>
<th>progress</th>
</tr>
</thead>
<tbody>
{Object.values(torrents).slice().sort((a, b) => a.name.localeCompare(b.name)).map(t =>
<Torrent
dispatch={dispatch}
id={t.id}
key={t.id}
/>
)}
</tbody>
</table>
);
}
}
export default connect(state => ({ export default connect(state => ({
torrents: state.torrents, torrents: state.torrents,
selection: state.selection, selection: state.selection,