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

@@ -0,0 +1,90 @@
import * as React from 'react'
import * as io from 'socket.io-client'
import * as q from '../../../../backend/src/Model'
import TreeNode from './TreeNode'
import List from '@material-ui/core/List'
class TreeState {
public tree: q.Tree
public msg: any
constructor(tree: q.Tree, msg: any) {
this.tree = tree
this.msg = msg
}
}
export interface TreeNodeProps {
didSelectNode?: (node: q.TreeNode) => void
}
declare const performance: any
export class Tree extends React.Component<TreeNodeProps, TreeState> {
private socket: SocketIOClient.Socket
private renderDuration: number = 300
private updateTimer?: any
private lastUpdate: number = 0
private perf:number = 0
constructor(props: any) {
super(props)
const tree = new q.Tree()
this.state = new TreeState(tree, {})
this.socket = io('http://localhost:3000')
}
public time(): number {
const time = performance.now() - this.perf
this.perf = performance.now()
return time
}
public throttledStateUpdate(state: any) {
if (this.updateTimer) {
return
}
const updateInterval = Math.max(this.renderDuration * 5, 300)
const timeUntilNextUpdate = updateInterval - (performance.now() - this.lastUpdate)
this.updateTimer = setTimeout(() => {
this.lastUpdate = performance.now()
this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined
this.setState(state)
}, Math.max(0, timeUntilNextUpdate))
}
public componentDidMount() {
this.socket.on('message', (msg: any) => {
const edges = msg.topic.split('/')
const node = q.TreeNodeFactory.fromEdgesAndValue(edges, Buffer.from(msg.payload, 'base64').toString())
this.state.tree.updateWithNode(node.firstNode())
this.throttledStateUpdate({ msg, tree: this.state.tree })
})
}
public componentWillUnmount() {
this.socket.removeAllListeners()
}
public render() {
return <div>
<List>
<TreeNode
animateChages={true}
autoExpandLimit={3}
isRoot={true}
didSelectNode={this.props.didSelectNode}
treeNode={this.state.tree}
name="/" collapsed={false}
key="rootNode"
performanceCallback={(ms: number) => {
this.renderDuration = ms
}}
/>
</List>
</div>
}
}

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)