Tweak performance

This commit is contained in:
Thomas Nordquist
2019-01-04 00:28:31 +01:00
parent 87dafc9c89
commit cd540cade3
11 changed files with 405 additions and 271 deletions

View File

@@ -1,7 +1,7 @@
import * as React from 'react'
import * as q from '../../backend/src/Model'
import { Tree } from './components/Tree'
import { Tree } from './components/Tree/Tree'
import TitleBar from './components/TitleBar'
import Sidebar from './components/Sidebar/Sidebar'

View File

@@ -22,17 +22,17 @@ interface State {
}
class Sidebar extends React.Component<Props, State> {
private updateNode: (node?: q.TreeNode | undefined) => void
constructor(props: any) {
super(props)
this.state = {}
this.updateNode = (node) => {
private updateNode = (node: q.TreeNode) => {
if (!node) {
this.setState(this.state)
} else {
this.setState({ node })
}
}
constructor(props: any) {
super(props)
this.state = { node: new q.Tree() }
}
public static styles: StyleRulesCallback<string> = (theme: Theme) => {
@@ -52,9 +52,19 @@ class Sidebar extends React.Component<Props, State> {
}
public componentWillReceiveProps(nextProps: Props) {
this.props.node && this.props.node.removeListener('update', this.updateNode)
nextProps.node && nextProps.node.on('update', this.updateNode)
nextProps.node && this.updateNode(nextProps.node)
this.props.node && this.removeUpdateListener(this.props.node)
nextProps.node && this.registerUpdateListener(nextProps.node)
this.props.node && this.setState({ node: this.props.node })
}
private registerUpdateListener(node: q.TreeNode) {
node.on(q.TreeNodeUpdateEvents.merge, this.updateNode)
node.on(q.TreeNodeUpdateEvents.message, this.updateNode)
}
private removeUpdateListener(node: q.TreeNode) {
node.removeListener(q.TreeNodeUpdateEvents.merge, this.updateNode)
node.removeListener(q.TreeNodeUpdateEvents.message, this.updateNode)
}
private open(): boolean {
@@ -79,7 +89,7 @@ class Sidebar extends React.Component<Props, State> {
<Typography className={classes.heading}>Topic</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Topic node={this.state.node} />
<Topic node={this.props.node} didSelectNode={this.updateNode} />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel key="value" defaultExpanded={true}>
@@ -90,7 +100,7 @@ class Sidebar extends React.Component<Props, State> {
<ValueRenderer node={this.state.node} />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel key="stats" defaultExpanded={true}>
<ExpansionPanel defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />}>
<Typography className={classes.heading}>Stats</Typography>
</ExpansionPanelSummary>

View File

@@ -6,8 +6,9 @@ import Button from '@material-ui/core/Button'
interface Props {
classes: any
theme: Theme
node: q.TreeNode
node?: q.TreeNode
selected?: q.TreeNode
didSelectNode: (node: q.TreeNode) => void
}
class Topic extends React.Component<Props, {}> {
@@ -21,7 +22,11 @@ class Topic extends React.Component<Props, {}> {
public render() {
const { node } = this.props
let i = 0
if (!node) {
return null
}
let key = 0
const breadCrumps = node.branch()
.map(node => node.sourceEdge)
.filter(edge => Boolean(edge))
@@ -42,7 +47,7 @@ class Topic extends React.Component<Props, {}> {
}
const joinedBreadCrumps = breadCrumps.reduce((prev, current) =>
prev.concat([<span key={i += 1}>/</span>]).concat(current),
prev.concat([<span key={key += 1}>/</span>]).concat(current),
)
return <span style={{ lineHeight: '2.2em' }}>{joinedBreadCrumps}</span>

View File

@@ -1,11 +1,9 @@
import * as React from 'react'
import * as io from 'socket.io-client'
import * as q from '../../../backend/src/Model'
import * as q from '../../../../backend/src/Model'
import TreeNode from './TreeNode'
import List from '@material-ui/core/List'
const throttle = require('lodash.throttle')
class TreeState {
public tree: q.Tree
public msg: any
@@ -22,7 +20,7 @@ declare const performance: any
export class Tree extends React.Component<TreeNodeProps, TreeState> {
private socket: SocketIOClient.Socket
private renderDuration: number = 200
private renderDuration: number = 300
private updateTimer?: any
private lastUpdate: number = 0
private perf:number = 0
@@ -46,7 +44,7 @@ export class Tree extends React.Component<TreeNodeProps, TreeState> {
return
}
const updateInterval = Math.max(this.renderDuration * 5, 200)
const updateInterval = Math.max(this.renderDuration * 5, 300)
const timeUntilNextUpdate = updateInterval - (performance.now() - this.lastUpdate)
this.updateTimer = setTimeout(() => {
@@ -75,6 +73,8 @@ export class Tree extends React.Component<TreeNodeProps, TreeState> {
return <div>
<List>
<TreeNode
animateChages={true}
autoExpandLimit={3}
isRoot={true}
didSelectNode={this.props.didSelectNode}
treeNode={this.state.tree}

View File

@@ -0,0 +1,194 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { withTheme, Theme } from '@material-ui/core/styles'
const throttle = require('lodash.throttle')
import { isElementInViewport } from '../helper/isElementInViewport'
import TreeNodeTitle from './TreeNodeTitle'
import TreeNodeSubnodes from './TreeNodeSubnodes'
declare var performance: any
declare var window: any
export interface TreeNodeProps {
animateChages: boolean
isRoot?: boolean
treeNode: q.TreeNode
name?: string | undefined
collapsed?: boolean | undefined
performanceCallback?: ((ms: number) => void) | undefined
didSelectNode?: (node: q.TreeNode) => void
theme: Theme
autoExpandLimit: number
}
interface TreeNodeState {
title: string | undefined
collapsed: boolean
collapsedOverride: boolean | undefined
edgeCount: number
}
class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
private dirtySubnodes: boolean = true
private dirtyState: boolean = true
private dirtyEdges: boolean = true
private dirtyMessage: boolean = true
private cssAnimationWasSetAt?: number
private willUpdateTime: number = performance.now()
private titleRef = React.createRef<HTMLElement>()
private subnodesDidchange = () => {
this.dirtySubnodes = true
}
private messageDidChange = () => {
this.dirtyMessage = true
}
private edgesDidChange = () => {
this.dirtyMessage = true
}
constructor(props: TreeNodeProps) {
super(props)
const edgeCount = Object.keys(props.treeNode.edges).length
const collapsed = edgeCount > this.props.autoExpandLimit
this.state = { collapsed, edgeCount, collapsedOverride: props.collapsed, title: props.name }
}
public componentDidMount() {
this.props.treeNode.on(q.TreeNodeUpdateEvents.merge, this.subnodesDidchange)
this.props.treeNode.on(q.TreeNodeUpdateEvents.edges, this.edgesDidChange)
this.props.treeNode.on(q.TreeNodeUpdateEvents.message, this.messageDidChange)
}
public componentWillUnmount() {
this.props.treeNode.removeListener(q.TreeNodeUpdateEvents.merge, this.subnodesDidchange)
this.props.treeNode.removeListener(q.TreeNodeUpdateEvents.edges, this.edgesDidChange)
this.props.treeNode.removeListener(q.TreeNodeUpdateEvents.message, this.messageDidChange)
}
private getStyles() {
return {
collapsedSubnodes: {
color: this.props.theme.palette.text.secondary,
},
displayBlock: {
display: 'block',
},
}
}
public setState(newState: any) {
this.dirtyState = this.stateHasChanged(newState)
super.setState(newState)
}
private stateHasChanged(newState: any) {
return this.state.collapsed !== newState.collapsed
|| this.state.collapsedOverride !== newState.collapsedOverride
|| this.state.edgeCount !== newState.edgeCount
}
public shouldComponentUpdate() {
const shouldRenderToRemoveCssAnimation = this.cssAnimationWasSetAt !== undefined
return this.dirtyState
|| this.dirtyEdges
|| this.dirtyMessage
|| this.dirtySubnodes
|| shouldRenderToRemoveCssAnimation
}
public componentDidUpdate() {
if (this.props.performanceCallback) {
const renderTime = performance.now() - this.willUpdateTime
this.props.performanceCallback(renderTime)
}
}
public componentWillUpdate() {
if (this.props.performanceCallback) {
this.willUpdateTime = performance.now()
}
}
private toggle() {
this.setState({ collapsedOverride: !this.collapsed() })
}
private collapsed() {
if (this.state.collapsedOverride !== undefined) {
return this.state.collapsedOverride
}
return this.state.collapsed
}
public componentWillReceiveProps() {
const edgeCount = Object.keys(this.props.treeNode.edges).length
this.setState({ edgeCount, collapsed: edgeCount > this.props.autoExpandLimit })
}
public render() {
const { displayBlock } = this.getStyles()
const animationStyle = this.indicatingChangeAnimationStyle()
this.dirtyState = this.dirtyEdges = this.dirtyMessage = this.dirtySubnodes = false
return <div key={this.props.treeNode.hash()} style={ displayBlock }>
<div style={animationStyle} ref={this.titleRef}>
<TreeNodeTitle
edgeCount={this.state.edgeCount}
collapsed={this.collapsed()}
treeNode={this.props.treeNode}
name={this.props.name}
didSelectNode={this.props.didSelectNode}
toggleCollapsed={() => this.toggle()}
/>
</div>
{ this.clear() }
<div style = { displayBlock }>
{this.renderNodes()}
</div>
</div>
}
private clear() {
return <div style={{ clear: 'both' }} />
}
private renderNodes() {
return <TreeNodeSubnodes
animateChanges={this.props.animateChages}
collapsed={this.collapsed()}
autoExpandLimit={this.props.autoExpandLimit}
didSelectNode={this.props.didSelectNode}
toggleCollapsed={() => this.toggle()}
treeNode={this.props.treeNode}
/>
}
private indicatingChangeAnimationStyle() {
if (this.props.isRoot) {
return {}
}
if (this.cssAnimationWasSetAt && (performance.now() - this.cssAnimationWasSetAt) > 500) {
this.cssAnimationWasSetAt = undefined
return {}
}
const isInViewPort = this.titleRef.current && isElementInViewport(this.titleRef.current)
const isDirty = this.dirtyMessage || this.dirtyEdges || this.collapsed()
if (this.props.animateChages && isDirty && isInViewPort) {
if (!this.cssAnimationWasSetAt) {
this.cssAnimationWasSetAt = performance.now()
}
return { animation: 'example 0.5s' }
}
}
}
export default withTheme()(TreeNode)

View File

@@ -0,0 +1,51 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { withTheme, Theme } from '@material-ui/core/styles'
import { List, ListItem, Collapse } from '@material-ui/core'
import TreeNode from './TreeNode'
export interface Props {
animateChanges: boolean
treeNode: q.TreeNode
autoExpandLimit: number
collapsed?: boolean | undefined
didSelectNode?: (node: q.TreeNode) => void
toggleCollapsed: () => void
theme: Theme
}
class TreeNodeSubnodes extends React.Component<Props, {}> {
public render() {
const edges = Object.values(this.props.treeNode.edges)
const listItemStyle = {
padding: '3px 8px 0px 8px',
}
const listStyle = {
padding: '3px 8px 0px 8px',
}
if (edges.length > 0 && !this.props.collapsed) {
const listItems = edges
.map(edge => edge.target)
.map(node => (
<ListItem key={node.hash()} style={listItemStyle} button>
<TreeNode
animateChages={this.props.animateChanges}
treeNode={node}
didSelectNode={this.props.didSelectNode}
autoExpandLimit={this.props.autoExpandLimit}
/>
</ListItem>
))
return <Collapse in={!this.props.collapsed} timeout="auto" unmountOnExit>
<List style={listStyle}>{listItems}</List>
</Collapse>
}
return null
}
}
export default withTheme()(TreeNodeSubnodes)

View File

@@ -0,0 +1,95 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { Typography } from '@material-ui/core'
import { withTheme, Theme } from '@material-ui/core/styles'
export interface TreeNodeProps {
treeNode: q.TreeNode
name?: string | undefined
collapsed?: boolean | undefined
toggleCollapsed: () => void
didSelectNode?: (node: q.TreeNode) => void
edgeCount: number
theme: Theme
}
class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
private getStyles() {
const { theme } = this.props
return {
collapsedSubnodes: {
color: theme.palette.text.secondary,
},
container: {
display: 'block',
},
}
}
public render() {
const style: React.CSSProperties = {
lineHeight: '1em',
whiteSpace: 'nowrap',
}
return <Typography style={style}>
{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
</Typography>
}
private renderSourceEdge() {
const style: React.CSSProperties = {
fontWeight: 'bold',
overflow: 'hidden',
display: 'inline-block',
}
const name = this.props.name || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
return <span style={style} onClick={() => {
this.toggle()
this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)
}}>{name}</span>
}
private renderValue() {
const style: React.CSSProperties = {
width: '15em',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
padding: '0',
paddingLeft: '5px',
display: 'inline-block',
}
return this.props.treeNode.message
? <span
style={style}
onMouseOver={() => this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)}
> = {this.props.treeNode.message.value.toString()}</span>
: null
}
private toggle() {
this.props.toggleCollapsed()
}
private renderExpander() {
if (this.props.edgeCount === 0) {
return null
}
return this.props.collapsed
? <span onClick={() => this.toggle()}></span>
: <span onClick={() => this.toggle()}></span>
}
private renderCollapsedSubnodes() {
if (this.props.edgeCount === 0 || !this.props.collapsed) {
return null
}
const messages = this.props.treeNode.leafes().map(leaf => leaf.messages).reduce((a, b) => a + b)
return <span style={this.getStyles().collapsedSubnodes}>({this.props.treeNode.leafes().length} nodes, {messages} messages)</span>
}
}
export default withTheme()(TreeNodeTitle)

View File

@@ -1,242 +0,0 @@
import * as React from 'react'
import * as q from '../../../backend/src/Model'
import { List, ListItem, Collapse, Typography } from '@material-ui/core'
import { withTheme, Theme } from '@material-ui/core/styles'
const throttle = require('lodash.throttle')
import Slide from '@material-ui/core/Slide'
import { isElementInViewport } from './helper/isElementInViewport'
const collapseLimit = 3
declare var performance: any
export interface TreeNodeProps {
isRoot?: boolean
treeNode: q.TreeNode
name?: string | undefined
collapsed?: boolean | undefined
performanceCallback?: ((ms: number) => void) | undefined
didSelectNode?: (node: q.TreeNode) => void
theme: Theme
}
interface TreeNodeState {
title: string | undefined
collapsed: boolean
collapsedOverride: boolean | undefined
edgeCount: number
}
class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
private dirty: boolean = true
private willUpdateTime: number = performance.now()
private titleRef = React.createRef<HTMLElement>()
private markAsDirty = () => {
this.dirty = true
if (!this.props.isRoot) {
this.indicateUpdate()
}
}
private indicateUpdate = throttle(() => {
const title: any = this.titleRef.current
if (title && isElementInViewport(title)) {
title.style.animation = 'example 0.5s'
setTimeout(() => {
title.style.animation = ''
}, 500)
}
}, 500)
constructor(props: TreeNodeProps) {
super(props)
const edgeCount = Object.keys(props.treeNode.edges).length
const collapsed = edgeCount > collapseLimit
this.state = { collapsed, edgeCount, collapsedOverride: props.collapsed, title: props.name }
}
public componentDidMount() {
this.props.treeNode.on('update', this.markAsDirty)
}
public componentWillUnmount() {
this.props.treeNode.removeListener('update', this.markAsDirty)
}
private getStyles() {
const { theme } = this.props
return {
collapsedSubnodes: {
color: theme.palette.text.secondary,
},
container: {
display: 'block',
},
}
}
public setState(state: any) {
this.dirty = this.state.collapsed !== state.collapsed
|| this.state.collapsedOverride !== state.collapsedOverride
|| this.state.edgeCount !== state.edgeCount
super.setState(state)
}
public shouldComponentUpdate() {
return this.dirty
}
public componentDidUpdate() {
if (this.props.performanceCallback) {
const renderTime = performance.now() - this.willUpdateTime
this.props.performanceCallback(renderTime)
}
}
public componentWillUpdate() {
if (this.props.performanceCallback) {
this.willUpdateTime = performance.now()
}
}
private collapsed() {
if (this.state.collapsedOverride !== undefined) {
return this.state.collapsedOverride
}
return this.state.collapsed
}
private renderNodes() {
const edges = Object.values(this.props.treeNode.edges)
const listItemStyle = {
padding: '3px 8px 0px 8px',
}
const listStyle = {
padding: '3px 8px 0px 16px',
}
if (edges.length > 0) {
const listItems = edges
.map(edge => edge.target)
.map(node => (
<ListItem key={node.hash()} style={listItemStyle} button>
<TreeNode
theme={this.props.theme}
didSelectNode={this.props.didSelectNode}
treeNode={node}
/>
</ListItem>
))
return <Collapse in={!this.collapsed()} timeout="auto" unmountOnExit>
<List style={listStyle}>{listItems}</List>
</Collapse>
}
}
private renderSourceEdge() {
const style: React.CSSProperties = {
fontWeight: 'bold',
overflow: 'hidden',
display: 'inline-block',
}
const name = this.state.title || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
return <span style={style} onClick={() => {
this.toggle()
this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)
}>{name}</span>
}
public componentWillReceiveProps() {
const edgeCount = Object.keys(this.props.treeNode.edges).length
this.setState({ edgeCount, collapsed: edgeCount > collapseLimit })
}
private renderValue() {
const style: React.CSSProperties = {
width: '15em',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
padding: '0',
paddingLeft: '5px',
display: 'inline-block',
}
return this.props.treeNode.message
? <span
style={style}
onMouseOver={() => this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)}
> = {this.props.treeNode.message.value.toString()}</span>
: null
}
private clear() {
return <div style={{ clear: 'both' }} />
}
private renderTitleLine() {
const style: React.CSSProperties = {
lineHeight: '1em',
whiteSpace: 'nowrap',
width: '15em',
}
return <div
ref={this.titleRef}
>
<Typography style={style}>
{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
</Typography>
</div>
}
public render() {
this.dirty = false
const nodeStyle: React.CSSProperties = {
display: 'block',
}
return <div key={this.props.treeNode.hash()} style={ this.getStyles().container }>
{ this.renderTitleLine() }
<div style = { nodeStyle }>
{ this.clear() }
<div style = { this.subnodesStyle() }>
{ this.collapsed() ? null : this.renderNodes() }
</div>
</div>
</div>
}
private toggle() {
this.setState({ collapsedOverride: !this.collapsed() })
}
private renderExpander() {
if (this.state.edgeCount === 0) {
return null
}
return this.collapsed()
? <span onClick={() => this.toggle()}></span>
: <span onClick={() => this.toggle()}></span>
}
private renderCollapsedSubnodes() {
if (this.state.edgeCount === 0 || !this.collapsed()) {
return null
}
const messages = this.props.treeNode.leafes().map(leaf => leaf.messages).reduce((a, b) => a + b)
return <span style={this.getStyles().collapsedSubnodes}>({this.props.treeNode.leafes().length} nodes, {messages} messages)</span>
}
private subnodesStyle(): React.CSSProperties {
return {
display: 'block',
}
}
}
export default withTheme()(TreeNode)

View File

@@ -1,6 +1,12 @@
import { Edge, Message } from './'
import { EventEmitter } from 'events'
export enum TreeNodeUpdateEvents {
edges = 'edges',
message = 'message',
merge = 'merge',
}
export class TreeNode extends EventEmitter {
public sourceEdge?: Edge
public message?: Message
@@ -19,6 +25,10 @@ export class TreeNode extends EventEmitter {
this.setMessage(message)
}
private propagateUpdate(event: TreeNodeUpdateEvents) {
this.emit(event)
}
public setMessage(value: any) {
this.message = value
this.messages += 1
@@ -43,10 +53,13 @@ export class TreeNode extends EventEmitter {
return this.sourceEdge ? this.sourceEdge.source || undefined : undefined
}
public addEdge(edge: Edge) {
public addEdge(edge: Edge, emitUpdate: boolean = false) {
this.edges[edge.name] = edge
edge.source = this
this.emit('update')
if (emitUpdate) {
this.propagateUpdate(TreeNodeUpdateEvents.edges)
}
}
public branch(): TreeNode[] {
@@ -61,10 +74,11 @@ export class TreeNode extends EventEmitter {
public updateWithNode(node: TreeNode) {
if (node.message) {
this.setMessage(node.message)
this.propagateUpdate(TreeNodeUpdateEvents.message)
}
this.mergeEdges(node)
this.emit('update')
this.propagateUpdate(TreeNodeUpdateEvents.merge)
}
public leafes(): TreeNode[] {
@@ -79,13 +93,20 @@ export class TreeNode extends EventEmitter {
private mergeEdges(node: TreeNode) {
const edgeKeys = Object.keys(node.edges)
let edgesDidUpdate = false
for (const edgeKey of edgeKeys) {
const matchingEdge = this.edges[edgeKey]
if (matchingEdge) {
matchingEdge.target.updateWithNode(node.edges[edgeKey].target)
} else {
this.addEdge(node.edges[edgeKey])
this.addEdge(node.edges[edgeKey], false)
edgesDidUpdate = true
}
}
if (edgesDidUpdate) {
this.propagateUpdate(TreeNodeUpdateEvents.edges)
}
}
}

View File

@@ -1,5 +1,5 @@
export { Edge } from './Edge'
export { TreeNode } from './TreeNode'
export { TreeNode, TreeNodeUpdateEvents } from './TreeNode'
export { Message } from './Message'
export { TreeNodeFactory } from './TreeNodeFactory'
export { Tree } from './Tree'

View File

@@ -4,7 +4,7 @@ const http = require('http')
import { TopicProperties, Tree, TreeNodeFactory } from './Model'
import { MqttSource, DataSource } from './DataSource'
const options = { url: 'mqtt://test.mosquitto.org' }
const options = { url: 'mqtt://nodered' }
const dataSource = new MqttSource()
const a: any[] = []