Skip to content

Commit

Permalink
Fix jaegertracing#43 - UI responsiveness of search page when many res…
Browse files Browse the repository at this point in the history
…ults have many spans
  • Loading branch information
tiffon committed Jul 31, 2017
1 parent 34ea513 commit 14c8ca5
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 14 deletions.
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 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);
});

0 comments on commit 14c8ca5

Please sign in to comment.