immutable data structures in tables 5/n: create table plugin

Summary:
Migrating tables' row collection to Immutable.js List:
1. Migrate createTablePlugin to ManagedTable_immutable

-----
Current implementation of tables forces to copy arrays on new data arrival which causes O(N^2) complexity ->
tables can't handle a lot of new rows in short period of time -> tables freeze and become unresponsive for a few seconds.
Immutable data structures will bring us to O(N) complexity

Reviewed By: jknoxville

Differential Revision: D16416867

fbshipit-source-id: 20890aa851cd2e34e33fd2ed69c5d6048af14cbb
This commit is contained in:
Timur Valiev
2019-07-23 07:57:00 -07:00
committed by Facebook Github Bot
parent d3658f7d31
commit 294d158869
2 changed files with 70 additions and 62 deletions

View File

@@ -6,15 +6,30 @@
*/ */
import {createTablePlugin} from '../createTablePlugin.js'; import {createTablePlugin} from '../createTablePlugin.js';
import type {TableRows_immutable} from 'flipper';
//import type {PersistedState, RowData} from '../createTablePlugin.js';
import {FlipperPlugin} from '../plugin.js'; import {FlipperPlugin} from '../plugin.js';
import {List, Map as ImmutableMap} from 'immutable';
const PROPS = { const PROPS = {
method: 'method', method: 'method',
resetMethod: 'resetMethod', resetMethod: 'resetMethod',
columns: {}, columns: {},
columnSizes: {}, columnSizes: {},
renderSidebar: () => {}, renderSidebar: (r: {id: string}) => {},
buildRow: () => {}, buildRow: (r: {id: string}) => {},
};
type PersistedState<T> = {|
rows: TableRows_immutable,
datas: ImmutableMap<string, T>,
|};
type RowData = {
id: string,
}; };
test('createTablePlugin returns FlipperPlugin', () => { test('createTablePlugin returns FlipperPlugin', () => {
@@ -24,29 +39,27 @@ test('createTablePlugin returns FlipperPlugin', () => {
test('persistedStateReducer is resetting data', () => { test('persistedStateReducer is resetting data', () => {
const resetMethod = 'resetMethod'; const resetMethod = 'resetMethod';
const tablePlugin = createTablePlugin({...PROPS, resetMethod}); const tablePlugin = createTablePlugin<RowData>({...PROPS, resetMethod});
// $FlowFixMe persistedStateReducer exists for createTablePlugin const ps: PersistedState<RowData> = {
const {rows, datas} = tablePlugin.persistedStateReducer( datas: ImmutableMap({'1': {id: '1'}}),
{ rows: List([
datas: {'1': {id: '1'}}, {
rows: [ key: '1',
{ columns: {
key: '1', id: {
columns: { value: '1',
id: {
value: '1',
},
}, },
}, },
], },
}, ]),
resetMethod, };
{},
);
expect(datas).toEqual({}); // $FlowFixMe persistedStateReducer exists for createTablePlugin
expect(rows).toEqual([]); const {rows, datas} = tablePlugin.persistedStateReducer(ps, resetMethod, {});
expect(datas.toJSON()).toEqual({});
expect(rows.size).toBe(0);
}); });
test('persistedStateReducer is adding data', () => { test('persistedStateReducer is adding data', () => {
@@ -56,14 +69,11 @@ test('persistedStateReducer is adding data', () => {
// $FlowFixMe persistedStateReducer exists for createTablePlugin // $FlowFixMe persistedStateReducer exists for createTablePlugin
const {rows, datas} = tablePlugin.persistedStateReducer( const {rows, datas} = tablePlugin.persistedStateReducer(
{ tablePlugin.defaultPersistedState,
datas: {},
rows: [],
},
method, method,
{id}, {id},
); );
expect(rows.length).toBe(1); expect(rows.size).toBe(1);
expect(Object.keys(datas)).toEqual([id]); expect([...datas.keys()]).toEqual([id]);
}); });

View File

@@ -7,7 +7,7 @@
import type { import type {
TableHighlightedRows, TableHighlightedRows,
TableRows, TableRows_immutable,
TableColumnSizes, TableColumnSizes,
TableColumns, TableColumns,
} from 'flipper'; } from 'flipper';
@@ -16,13 +16,15 @@ import FlexColumn from './ui/components/FlexColumn';
import Button from './ui/components/Button'; import Button from './ui/components/Button';
import DetailSidebar from './chrome/DetailSidebar'; import DetailSidebar from './chrome/DetailSidebar';
import {FlipperPlugin} from './plugin'; import {FlipperPlugin} from './plugin';
import SearchableTable from './ui/components/searchable/SearchableTable'; import SearchableTable_immutable from './ui/components/searchable/SearchableTable_immutable';
import textContent from './utils/textContent.js'; import textContent from './utils/textContent.js';
import createPaste from './fb-stubs/createPaste.js'; import createPaste from './fb-stubs/createPaste.js';
import {List, Map as ImmutableMap} from 'immutable';
type ID = string; type ID = string;
type RowData = { export type RowData = {
id: ID, id: ID,
}; };
@@ -35,9 +37,9 @@ type Props<T> = {|
buildRow: (row: T) => any, buildRow: (row: T) => any,
|}; |};
type PersistedState<T> = {| export type PersistedState<T> = {|
rows: TableRows, rows: TableRows_immutable,
datas: {[key: ID]: T}, datas: ImmutableMap<ID, T>,
|}; |};
type State = {| type State = {|
@@ -58,41 +60,36 @@ type State = {|
* the client in an unknown state. * the client in an unknown state.
*/ */
export function createTablePlugin<T: RowData>(props: Props<T>) { export function createTablePlugin<T: RowData>(props: Props<T>) {
return class extends FlipperPlugin<State, *, PersistedState<T>> { return class extends FlipperPlugin<State, *, PersistedState<*>> {
static keyboardActions = ['clear', 'createPaste']; static keyboardActions = ['clear', 'createPaste'];
static defaultPersistedState: PersistedState<T> = { static defaultPersistedState = {
rows: [], rows: List(),
datas: {}, datas: ImmutableMap<ID, T>(),
}; };
static persistedStateReducer = ( static persistedStateReducer = (
persistedState: PersistedState<T>, persistedState: PersistedState<*>,
method: string, method: string,
payload: T | Array<T>, payload: T | Array<T>,
): $Shape<PersistedState<RowData>> => { ): $Shape<PersistedState<T>> => {
if (method === props.method) { if (method === props.method) {
const newRows = []; return List(Array.isArray(payload) ? payload : [payload]).reduce(
const newData = {}; (ps: PersistedState<*>, data: T) => {
const datas = Array.isArray(payload) ? payload : [payload]; if (data.id == null) {
console.warn('The data sent did not contain an ID.', data);
for (const data of datas.reverse()) { }
if (data.id == null) { return {
console.warn('The data sent did not contain an ID.', data); datas: ps.datas.set(data.id, data),
} rows: ps.rows.push(props.buildRow(data)),
if (persistedState.datas[data.id] == null) { };
newData[data.id] = data; },
newRows.push(props.buildRow(data)); persistedState,
} );
}
return {
datas: {...persistedState.datas, ...newData},
rows: [...persistedState.rows, ...newRows],
};
} else if (method === props.resetMethod) { } else if (method === props.resetMethod) {
return { return {
rows: [], rows: List(),
datas: {}, datas: ImmutableMap(),
}; };
} else { } else {
return {}; return {};
@@ -113,8 +110,8 @@ export function createTablePlugin<T: RowData>(props: Props<T>) {
clear = () => { clear = () => {
this.props.setPersistedState({ this.props.setPersistedState({
rows: [], rows: List(),
datas: {}, datas: ImmutableMap(),
}); });
this.setState({ this.setState({
selectedIds: [], selectedIds: [],
@@ -154,7 +151,8 @@ export function createTablePlugin<T: RowData>(props: Props<T>) {
const selectedId = selectedIds.length !== 1 ? null : selectedIds[0]; const selectedId = selectedIds.length !== 1 ? null : selectedIds[0];
if (selectedId != null) { if (selectedId != null) {
return renderSidebar(datas[selectedId]); const data = datas.get(selectedId);
return data != null ? renderSidebar(data) : null;
} else { } else {
return null; return null;
} }
@@ -166,7 +164,7 @@ export function createTablePlugin<T: RowData>(props: Props<T>) {
return ( return (
<FlexColumn grow={true}> <FlexColumn grow={true}>
<SearchableTable <SearchableTable_immutable
key={this.constructor.id} key={this.constructor.id}
rowLineHeight={28} rowLineHeight={28}
floating={false} floating={false}