Fix memory leaks

This commit is contained in:
Thomas Nordquist
2019-04-24 23:40:28 +02:00
parent 4c4e1543ec
commit 749df70d5c
11 changed files with 58 additions and 33 deletions

View File

@@ -78,6 +78,8 @@ export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppSta
} }
tree && tree.stopUpdating() tree && tree.stopUpdating()
tree && tree.destroy()
// Clear topic filter // Clear topic filter
dispatch({ dispatch({
topicFilter: '', topicFilter: '',
@@ -88,4 +90,5 @@ export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppSta
dispatch({ dispatch({
type: ActionTypes.CONNECTION_SET_DISCONNECTED, type: ActionTypes.CONNECTION_SET_DISCONNECTED,
}) })
dispatch(showTree(undefined))
} }

View File

@@ -53,20 +53,28 @@ const debouncedSelectTopic = debounce((topic: q.TreeNode<TopicViewModel>, dispat
} }
}, 70) }, 70)
export const resetStore = () => (dispatch: Dispatch<any>): AnyAction => { function destroyUnreferencedTree(state: AppState) {
const visibleTree = state.tree.get('tree')
const connectionTree = state.connection.tree
// Stop updates of old tree
if (visibleTree && visibleTree !== connectionTree) {
console.warn('destroy')
visibleTree.stopUpdating()
visibleTree.destroy()
}
}
export const resetStore = () => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
destroyUnreferencedTree(getState())
return dispatch({ return dispatch({
type: ActionTypes.TREE_RESET_STORE, type: ActionTypes.TREE_RESET_STORE,
}) })
} }
export const showTree = (tree: q.Tree<TopicViewModel> | undefined) => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => { export const showTree = (tree: q.Tree<TopicViewModel> | undefined) => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
const visibleTree = getState().tree.get('tree') destroyUnreferencedTree(getState())
const connectionTree = getState().connection.tree
// Stop updates of old tree
if (visibleTree !== connectionTree && visibleTree) {
visibleTree.stopUpdating()
}
return dispatch({ return dispatch({
tree, tree,

View File

@@ -35,7 +35,6 @@ interface Props {
} }
interface State { interface State {
node: q.TreeNode<TopicViewModel>
compareMessage?: q.Message compareMessage?: q.Message
valueRenderWidth: number valueRenderWidth: number
} }
@@ -49,8 +48,7 @@ class Sidebar extends React.Component<Props, State> {
constructor(props: any) { constructor(props: any) {
super(props) super(props)
console.error('Find and fix me #state') this.state = { valueRenderWidth: 300 }
this.state = { node: new q.Tree(), valueRenderWidth: 300 }
} }
private registerUpdateListener(node: q.TreeNode<TopicViewModel>) { private registerUpdateListener(node: q.TreeNode<TopicViewModel>) {
@@ -156,7 +154,6 @@ class Sidebar extends React.Component<Props, State> {
public componentWillReceiveProps(nextProps: Props) { public componentWillReceiveProps(nextProps: Props) {
this.props.node && this.removeUpdateListener(this.props.node) this.props.node && this.removeUpdateListener(this.props.node)
nextProps.node && this.registerUpdateListener(nextProps.node) nextProps.node && this.registerUpdateListener(nextProps.node)
this.props.node && this.setState({ node: this.props.node })
if (this.props.node !== nextProps.node) { if (this.props.node !== nextProps.node) {
this.setState({ compareMessage: undefined }) this.setState({ compareMessage: undefined })

View File

@@ -30,7 +30,7 @@ interface State {
lastUpdate: number lastUpdate: number
} }
class Tree extends React.PureComponent<Props, State> { class TreeComponent extends React.PureComponent<Props, State> {
private updateTimer?: any private updateTimer?: any
private perf: number = 0 private perf: number = 0
private renderTime = 0 private renderTime = 0
@@ -137,4 +137,4 @@ const mapDispatchToProps = (dispatch: any) => {
} }
} }
export default connect(mapStateToProps, mapDispatchToProps)(Tree) export default connect(mapStateToProps, mapDispatchToProps)(TreeComponent)

View File

@@ -70,7 +70,7 @@ interface State {
selected: boolean selected: boolean
} }
class TreeNode extends React.Component<Props, State> { class TreeNodeComponent extends React.Component<Props, State> {
private animationDirty: boolean = false private animationDirty: boolean = false
private cssAnimationWasSetAt?: number private cssAnimationWasSetAt?: number
@@ -97,8 +97,8 @@ class TreeNode extends React.Component<Props, State> {
treeNode.viewModel.change.subscribe(this.viewStateHasChanged) treeNode.viewModel.change.subscribe(this.viewStateHasChanged)
} }
private viewStateHasChanged = (msg: void, viewModel: TopicViewModel) => { private viewStateHasChanged = (msg: void) => {
this.setState({ selected: viewModel.isSelected() }) this.setState({ selected: this.props.treeNode.viewModel!.isSelected() })
} }
private removeSubscriber(treeNode: q.TreeNode<TopicViewModel>) { private removeSubscriber(treeNode: q.TreeNode<TopicViewModel>) {
@@ -243,4 +243,4 @@ class TreeNode extends React.Component<Props, State> {
} }
} }
export default withStyles(styles, { withTheme: true })(TreeNode) export default withStyles(styles, { withTheme: true })(TreeNodeComponent)

View File

@@ -2,7 +2,7 @@ import { EventDispatcher } from '../../../events'
export class TopicViewModel { export class TopicViewModel {
private selected: boolean private selected: boolean
public change = new EventDispatcher<void, TopicViewModel>(this) public change = new EventDispatcher<void, TopicViewModel>()
public constructor() { public constructor() {
this.selected = false this.selected = false

View File

@@ -7,7 +7,7 @@ export interface DataSourceState {
} }
export class DataSourceStateMachine { export class DataSourceStateMachine {
public onUpdate = new EventDispatcher<DataSourceState, DataSourceStateMachine>(this) public onUpdate = new EventDispatcher<DataSourceState, DataSourceStateMachine>()
private state: DataSourceState = { private state: DataSourceState = {
error: undefined, error: undefined,
connected: false, connected: false,

View File

@@ -16,7 +16,7 @@ export class Tree<ViewModel> extends TreeNode<ViewModel> {
public isTree = true public isTree = true
private cachedHash = `${Math.random()}` private cachedHash = `${Math.random()}`
private unmergedMessages: ChangeBuffer = new ChangeBuffer() private unmergedMessages: ChangeBuffer = new ChangeBuffer()
public didReceive = new EventDispatcher<void, Tree<ViewModel>>(this) public didReceive = new EventDispatcher<void, Tree<ViewModel>>()
constructor() { constructor() {
super(undefined, undefined) super(undefined, undefined)
@@ -27,6 +27,13 @@ export class Tree<ViewModel> extends TreeNode<ViewModel> {
this.didReceive.dispatch() this.didReceive.dispatch()
} }
public destroy() {
super.destroy()
this.updateSource && this.updateSource.unsubscribe(this.subscriptionEvent, this.handleNewData)
this.updateSource = undefined
this.didReceive.removeAllListeners()
}
public updateWithConnection(emitter: EventBusInterface, connectionId: string, nodeFilter?: (node: TreeNode<ViewModel>) => boolean) { public updateWithConnection(emitter: EventBusInterface, connectionId: string, nodeFilter?: (node: TreeNode<ViewModel>) => boolean) {
this.updateSource = emitter this.updateSource = emitter
this.connectionId = connectionId this.connectionId = connectionId

View File

@@ -12,9 +12,9 @@ export class TreeNode<ViewModel> {
public collapsed = false public collapsed = false
public messages: number = 0 public messages: number = 0
public lastUpdate: number = Date.now() public lastUpdate: number = Date.now()
public onMerge = new EventDispatcher<void, TreeNode<ViewModel>>(this) public onMerge = new EventDispatcher<void, TreeNode<ViewModel>>()
public onEdgesChange = new EventDispatcher<void, TreeNode<ViewModel>>(this) public onEdgesChange = new EventDispatcher<void, TreeNode<ViewModel>>()
public onMessage = new EventDispatcher<Message, TreeNode<ViewModel>>(this) public onMessage = new EventDispatcher<Message, TreeNode<ViewModel>>()
public isTree = false public isTree = false
private cachedPath?: string private cachedPath?: string
@@ -99,6 +99,21 @@ export class TreeNode<ViewModel> {
} }
} }
public destroy() {
for (const edge of this.edgeArray) {
edge.target.destroy()
}
this.edgeArray = []
this.edges = {}
this.cachedChildTopics = []
this.sourceEdge = undefined
this.onMerge.removeAllListeners()
this.onEdgesChange.removeAllListeners()
this.onMessage.removeAllListeners()
this.messageHistory = new RingBuffer<Message>(1, 1)
this.message = undefined
}
public unconnectedClone() { public unconnectedClone() {
const node = new TreeNode<ViewModel>() const node = new TreeNode<ViewModel>()
node.message = this.message node.message = this.message

View File

@@ -78,7 +78,7 @@ export class ConnectionManager {
} }
class UpdateNotifier { class UpdateNotifier {
public onCheckUpdateRequest = new EventDispatcher<void, UpdateNotifier>(this) public onCheckUpdateRequest = new EventDispatcher<void, UpdateNotifier>()
constructor() { constructor() {
backendEvents.subscribe(checkForUpdates, () => { backendEvents.subscribe(checkForUpdates, () => {
this.onCheckUpdateRequest.dispatch() this.onCheckUpdateRequest.dispatch()

View File

@@ -7,20 +7,15 @@ interface CallbackStore {
export class EventDispatcher<Message, Dispatcher> { export class EventDispatcher<Message, Dispatcher> {
private emitter = new EventEmitter() private emitter = new EventEmitter()
private dispatcher: Dispatcher
private callbacks: Array<CallbackStore> = [] private callbacks: Array<CallbackStore> = []
constructor(dispatcher: Dispatcher) {
this.dispatcher = dispatcher
}
public dispatch(msg: Message) { public dispatch(msg: Message) {
this.emitter.emit('event', msg) this.emitter.emit('event', msg)
} }
public subscribe(callback: (msg: Message, dispatcher: Dispatcher) => void) { public subscribe(callback: (msg: Message) => void) {
const wrappedCallback = (msg: Message) => { const wrappedCallback = (msg: Message) => {
callback(msg, this.dispatcher) callback(msg)
} }
this.emitter.on('event', wrappedCallback) this.emitter.on('event', wrappedCallback)
@@ -30,7 +25,7 @@ export class EventDispatcher<Message, Dispatcher> {
}) })
} }
public unsubscribe(callback: (msg: Message, dispatcher: Dispatcher) => void) { public unsubscribe(callback: (msg: Message) => void) {
const item = this.callbacks.find(store => store.callback === callback) const item = this.callbacks.find(store => store.callback === callback)
if (!item) { if (!item) {
return return