Skip to content

Commit

Permalink
Fix #43 - Search page UI responsiveness sluggish when many spans in r…
Browse files Browse the repository at this point in the history
…esults (#46)

* update eslint-plugin-react, avoid eslintrc warnings

* .eslintrc env for browser, jest and jasmine

* Fix #43 - Search page responsiveness issues

Fix an issue with the search page that shows up when there are many
spans (cumulatively) in the search results. Issue at hand is the
mapStateToProps function calls .toJS() on some of the immutableJS state
values, transforms the resulting values, then passes the transformed
values to reselect. The .toJS() invocation prevents the calculations
from being memoized by reselect because it always results in new objects
or arrays.

The change is to wrap the .toJS() call and subsequent transformation in
a function, the result of which is cached based on the state value. The
caching resolves the issue. Only the last result needs to be cached
because these are top-level state values (vs transforming items in an
array) and therefore a state value will never be revisited.

* Fix yarn lock file, e.g. use npm registry

* Fix typo in comment
  • Loading branch information
tiffon authored Aug 1, 2017
1 parent c30737e commit 6979e20
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 22 deletions.
17 changes: 11 additions & 6 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
{
"env": {
"browser": true,
"jest": true,
"jasmine": true
},
"extends": [
"react-app",
"airbnb",
"prettier",
"prettier/flowtype",
"react-app",
"airbnb",
"prettier",
"prettier/flowtype",
"prettier/react"
],
"rules": {
/* general */
"comma-dangle": 0,
"arrow-parens": 1,
"arrow-parens": [1, "as-needed"],
"no-restricted-syntax": 0,
"no-plusplus": 0,

Expand Down Expand Up @@ -37,4 +42,4 @@
]
}]
}
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"eslint-plugin-flowtype": "^2.21.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^4.0.0",
"eslint-plugin-react": "^6.4.1",
"eslint-plugin-react": "7.1.0",
"husky": "^0.13.3",
"lint-staged": "^3.4.0",
"react-scripts": "^0.9.5",
Expand Down
39 changes: 25 additions & 14 deletions src/components/SearchTracePage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
MOST_RECENT,
} from '../../selectors/search';
import { getPercentageOfDuration } from '../../utils/date';
import getLastXformCacher from '../../utils/get-last-xform-cacher';

/**
* Contains the dropdown to sort and filter trace search results
Expand Down Expand Up @@ -196,6 +197,7 @@ export default class SearchTracePage extends Component {

SearchTracePage.propTypes = {
isHomepage: PropTypes.bool,
// eslint-disable-next-line react/forbid-prop-types
traceResults: PropTypes.array,
numberOfTraceResults: PropTypes.number,
maxTraceDuration: PropTypes.number,
Expand All @@ -218,20 +220,32 @@ SearchTracePage.propTypes = {
errorMessage: PropTypes.string,
};

const stateTraceXformer = getLastXformCacher(stateTrace => {
const { traces: traceMap, loading, error: traceError } = stateTrace.toJS();
const traces = Object.keys(traceMap).map(traceID => traceMap[traceID]);
return { tracesSrc: { traces }, loading, traceError };
});

const stateServicesXformer = getLastXformCacher(stateServices => {
const {
services: serviceList,
operationsForService: opsBySvc,
error: serviceError,
} = stateServices.toJS();
const services = serviceList.map(name => ({
name,
operations: opsBySvc[name] || [],
}));
return { services, serviceError };
});

function mapStateToProps(state) {
const query = state.routing.locationBeforeTransitions.query;
const isHomepage = !Object.keys(query).length;
const { traces: traceMap, loading, error: traceError } = state.trace.toJS();
const {
services,
operationsForService,
error: serviceError,
} = state.services.toJS();
const traceResults = Object.keys(traceMap).map(traceID => traceMap[traceID]);
const { tracesSrc, loading, traceError } = stateTraceXformer(state.trace);
const { traces, maxDuration } = transformTraceResultsSelector(tracesSrc);
const { services, serviceError } = stateServicesXformer(state.services);
const sortBy = traceResultsFiltersFormSelector(state, 'sortBy');
const { traces, maxDuration } = transformTraceResultsSelector({
traces: traceResults,
});
const traceResultsSorted = getSortedTraceResults(traces, sortBy);
const errorMessage = serviceError || traceError
? `${serviceError || ''} ${traceError || ''}`
Expand All @@ -244,10 +258,7 @@ function mapStateToProps(state) {
numberOfTraceResults: traceResultsSorted.length,
maxTraceDuration: maxDuration,
urlQueryParams: query,
services: services.map(serviceName => ({
name: serviceName,
operations: operationsForService[serviceName] || [],
})),
services,
loading,
errorMessage,
};
Expand Down
48 changes: 48 additions & 0 deletions src/utils/get-last-xform-cacher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

/**
* Given a transformer function, returns a function that invokes the
* transformer on the argument and caches the transformation before returning
* it. If the source value changes, the transformation is recalculated, cached
* and returned.
*
* @param {function} xformer The transformer function, the most recent result
* of which is cached.
* @return {function} A wrapper around the transformer function which caches
* the last transformation, returning the cached value if
* the source value is the same.
*/
export default function getLastXformCacher(xformer) {
let lastArgs = null;
let lastXformed = null;

return function getOrCache(...args) {
const sameArgs = lastArgs &&
lastArgs.length === args.length &&
lastArgs.every((lastArg, i) => lastArg === args[i]);
if (sameArgs) {
return lastXformed;
}
lastArgs = args;
lastXformed = xformer(...args);
return lastXformed;
};
}
89 changes: 89 additions & 0 deletions src/utils/get-last-xform-cacher.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import getLastXformCacher from './get-last-xform-cacher';

const xformImpl = value => value + value;
let xformer;
let cacher;

beforeEach(() => {
xformer = jest.fn(xformImpl);
cacher = getLastXformCacher(xformer);
});

it('returns a function', () => {
expect(cacher).toEqual(jasmine.any(Function));
});

it('handles the first invocation where nothing is cached', () => {
expect(() => cacher('a')).not.toThrow();
});

it('the returned function returns the same results as the transformer function', () => {
expect(cacher('a')).toBe(xformImpl('a'));
expect(cacher('a')).toBe(xformImpl('a'));
expect(cacher(1)).toBe(xformImpl(1));
expect(cacher('a')).not.toBe(cacher('b'));
});

it('the returned function returns a cached value for subsequent invocation with the same argument', () => {
expect(xformer.mock.calls.length).toBe(0);
const value = cacher('a');
expect(xformer.mock.calls.length).toBe(1);
expect(cacher('a')).toBe(value);
expect(xformer.mock.calls.length).toBe(1);
});

it('the returned function ignores the cached value when invoked with different arguments', () => {
expect(xformer.mock.calls.length).toBe(0);
const firstValue = cacher('a');
expect(xformer.mock.calls.length).toBe(1);
expect(cacher('a')).toBe(firstValue);
expect(xformer.mock.calls.length).toBe(1);
const secondValue = cacher('b');
expect(xformer.mock.calls.length).toBe(2);
expect(cacher('b')).toBe(secondValue);
expect(xformer.mock.calls.length).toBe(2);
});

it('the functionality works with multiple arguments', () => {
const multiXformer = jest.fn((a, b) => [a + a, b + b]);
const multiCacher = getLastXformCacher(multiXformer);

expect(multiXformer.mock.calls.length).toBe(0);
const firstValue = multiCacher('a', 'b');

expect(multiXformer.mock.calls.length).toBe(1);
expect(firstValue).toEqual(['aa', 'bb']);
expect(multiCacher('a', 'b')).toBe(firstValue);
expect(multiXformer.mock.calls.length).toBe(1);

const secondValue = multiCacher('A', 'B');
expect(multiXformer.mock.calls.length).toBe(2);
expect(secondValue).toEqual(['AA', 'BB']);
expect(multiCacher('A', 'B')).toBe(secondValue);
expect(multiXformer.mock.calls.length).toBe(2);

const thirdValue = multiCacher('A', 'B', 'extra-arg');
expect(multiXformer.mock.calls.length).toBe(3);
expect(thirdValue).not.toBe(secondValue);
expect(thirdValue).toEqual(secondValue);
});
21 changes: 20 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1872,6 +1872,13 @@ [email protected]:
esutils "^2.0.2"
isarray "^1.0.0"

doctrine@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63"
dependencies:
esutils "^2.0.2"
isarray "^1.0.0"

dom-converter@~0.1:
version "0.1.4"
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz#a45ef5727b890c9bffe6d7c876e7b19cb0e17f3b"
Expand Down Expand Up @@ -2222,13 +2229,21 @@ [email protected], eslint-plugin-jsx-a11y@^4.0.0:
jsx-ast-utils "^1.0.0"
object-assign "^4.0.1"

[email protected], eslint-plugin-react@^6.4.1:
[email protected]:
version "6.4.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-6.4.1.tgz#7d1aade747db15892f71eee1fea4addf97bcfa2b"
dependencies:
doctrine "^1.2.2"
jsx-ast-utils "^1.3.1"

[email protected]:
version "7.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.1.0.tgz#27770acf39f5fd49cd0af4083ce58104eb390d4c"
dependencies:
doctrine "^2.0.0"
has "^1.0.1"
jsx-ast-utils "^1.4.1"

[email protected], eslint@^3.16.1:
version "3.16.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.16.1.tgz#9bc31fc7341692cf772e80607508f67d711c5609"
Expand Down Expand Up @@ -3712,6 +3727,10 @@ jsx-ast-utils@^1.0.0, jsx-ast-utils@^1.3.1:
dependencies:
object-assign "^4.1.0"

jsx-ast-utils@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1"

kind-of@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.1.0.tgz#475d698a5e49ff5e53d14e3e732429dc8bf4cf47"
Expand Down

0 comments on commit 6979e20

Please sign in to comment.