[Resolver] Selector performance (#72380)

* Memoize various selectors
* Improve performance of the selectors that calculate the `aria-flowto` attribute.
* more tests.
This commit is contained in:
Robert Austin 2020-07-20 09:38:30 -04:00 committed by GitHub
parent f331cc8b64
commit 6cf796a4fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 427 additions and 280 deletions

View file

@ -482,7 +482,6 @@ export interface LegacyEndpointEvent {
type: string;
version: string;
};
process?: object;
rule?: object;
user?: object;
event?: {

View file

@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable no-shadow */
import { uniquePidForProcess, uniqueParentPidForProcess, orderByTime } from '../process_event';
import { IndexedProcessTree } from '../../types';
import { ResolverEvent } from '../../../../common/endpoint/types';
@ -24,16 +26,15 @@ export function factory(
const uniqueProcessPid = uniquePidForProcess(process);
idToValue.set(uniqueProcessPid, process);
const uniqueParentPid = uniqueParentPidForProcess(process);
// if its defined and not ''
if (uniqueParentPid) {
let siblings = idToChildren.get(uniqueParentPid);
if (!siblings) {
siblings = [];
idToChildren.set(uniqueParentPid, siblings);
}
siblings.push(process);
// NB: If the value was null or undefined, use `undefined`
const uniqueParentPid: string | undefined = uniqueParentPidForProcess(process) ?? undefined;
let childrenWithTheSameParent = idToChildren.get(uniqueParentPid);
if (!childrenWithTheSameParent) {
childrenWithTheSameParent = [];
idToChildren.set(uniqueParentPid, childrenWithTheSameParent);
}
childrenWithTheSameParent.push(process);
}
// sort the children of each node
@ -50,9 +51,8 @@ export function factory(
/**
* Returns an array with any children `ProcessEvent`s of the passed in `process`
*/
export function children(tree: IndexedProcessTree, process: ResolverEvent): ResolverEvent[] {
const id = uniquePidForProcess(process);
const currentProcessSiblings = tree.idToChildren.get(id);
export function children(tree: IndexedProcessTree, parentID: string | undefined): ResolverEvent[] {
const currentProcessSiblings = tree.idToChildren.get(parentID);
return currentProcessSiblings === undefined ? [] : currentProcessSiblings;
}
@ -78,31 +78,6 @@ export function parent(
}
}
/**
* Returns the following sibling
*/
export function nextSibling(
tree: IndexedProcessTree,
sibling: ResolverEvent
): ResolverEvent | undefined {
const parentNode = parent(tree, sibling);
if (parentNode) {
// The siblings of `sibling` are the children of its parent.
const siblings = children(tree, parentNode);
// Find the sibling
const index = siblings.indexOf(sibling);
// if the sibling wasn't found, or if it was the last element in the array, return undefined
if (index === -1 || index === siblings.length - 1) {
return undefined;
}
// return the next sibling
return siblings[index + 1];
}
}
/**
* Number of processes in the tree
*/
@ -133,6 +108,8 @@ export function root(tree: IndexedProcessTree) {
export function* levelOrder(tree: IndexedProcessTree) {
const rootNode = root(tree);
if (rootNode !== null) {
yield* baseLevelOrder(rootNode, children.bind(null, tree));
yield* baseLevelOrder(rootNode, (parentNode: ResolverEvent): ResolverEvent[] =>
children(tree, uniquePidForProcess(parentNode))
);
}
}

View file

@ -19,6 +19,7 @@ import * as event from '../../../../common/endpoint/models/event';
import { ResolverEvent } from '../../../../common/endpoint/types';
import * as model from './index';
import { getFriendlyElapsedTime as elapsedTime } from '../../lib/date';
import { uniquePidForProcess } from '../process_event';
/**
* Graph the process tree
@ -146,10 +147,12 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): Proces
return widths;
}
const processesInReverseLevelOrder = [...model.levelOrder(indexedProcessTree)].reverse();
const processesInReverseLevelOrder: ResolverEvent[] = [
...model.levelOrder(indexedProcessTree),
].reverse();
for (const process of processesInReverseLevelOrder) {
const children = model.children(indexedProcessTree, process);
const children = model.children(indexedProcessTree, uniquePidForProcess(process));
const sumOfWidthOfChildren = function sumOfWidthOfChildren() {
return children.reduce(function sum(currentValue, child) {
@ -226,7 +229,7 @@ function processEdgeLineSegments(
metadata: edgeLineMetadata,
};
const siblings = model.children(indexedProcessTree, parent);
const siblings = model.children(indexedProcessTree, uniquePidForProcess(parent));
const isFirstChild = process === siblings[0];
if (metadata.isOnlyChild) {
@ -420,7 +423,7 @@ function* levelOrderWithWidths(
parentWidth,
};
const siblings = model.children(tree, parent);
const siblings = model.children(tree, uniquePidForProcess(parent));
if (siblings.length === 1) {
metadata.isOnlyChild = true;
metadata.lastChildWidth = width;

View file

@ -164,7 +164,8 @@ export const scale: (state: CameraState) => (time: number) => Vector2 = createSe
scalingConstants.maximum
);
return (time) => {
// memoizing this so the vector returned will be the same object reference if called with the same `time`.
return defaultMemoize((time) => {
/**
* If the animation has completed, return the `scaleNotCountingAnimation`, as
* the animation always completes with the scale set back at starting value.
@ -247,12 +248,13 @@ export const scale: (state: CameraState) => (time: number) => Vector2 = createSe
*/
return [lerpedScale, lerpedScale];
}
};
});
} else {
/**
* The scale should be the same in both axes.
* Memoizing this so the vector returned will be the same object reference every time.
*/
return () => [scaleNotCountingAnimation, scaleNotCountingAnimation];
return defaultMemoize(() => [scaleNotCountingAnimation, scaleNotCountingAnimation]);
}
/**
@ -277,22 +279,26 @@ export const clippingPlanes: (
) => (time: number) => ClippingPlanes = createSelector(
(state) => state.rasterSize,
scale,
(rasterSize, scaleAtTime) => (time: number) => {
const [scaleX, scaleY] = scaleAtTime(time);
const renderWidth = rasterSize[0];
const renderHeight = rasterSize[1];
const clippingPlaneRight = renderWidth / 2 / scaleX;
const clippingPlaneTop = renderHeight / 2 / scaleY;
(rasterSize, scaleAtTime) =>
/**
* memoizing this for object reference equality.
*/
defaultMemoize((time: number) => {
const [scaleX, scaleY] = scaleAtTime(time);
const renderWidth = rasterSize[0];
const renderHeight = rasterSize[1];
const clippingPlaneRight = renderWidth / 2 / scaleX;
const clippingPlaneTop = renderHeight / 2 / scaleY;
return {
renderWidth,
renderHeight,
clippingPlaneRight,
clippingPlaneTop,
clippingPlaneLeft: -clippingPlaneRight,
clippingPlaneBottom: -clippingPlaneTop,
};
}
return {
renderWidth,
renderHeight,
clippingPlaneRight,
clippingPlaneTop,
clippingPlaneLeft: -clippingPlaneRight,
clippingPlaneBottom: -clippingPlaneTop,
};
})
);
/**
@ -323,7 +329,10 @@ export const translation: (state: CameraState) => (time: number) => Vector2 = cr
scale,
(state) => state.animation,
(panning, translationNotCountingCurrentPanning, scaleAtTime, animation) => {
return (time: number) => {
/**
* Memoizing this for object reference equality.
*/
return defaultMemoize((time: number) => {
const [scaleX, scaleY] = scaleAtTime(time);
if (animation !== undefined && animationIsActive(animation, time)) {
return vector2.lerp(
@ -343,7 +352,7 @@ export const translation: (state: CameraState) => (time: number) => Vector2 = cr
} else {
return translationNotCountingCurrentPanning;
}
};
});
}
);
@ -357,7 +366,10 @@ export const inverseProjectionMatrix: (
clippingPlanes,
translation,
(clippingPlanesAtTime, translationAtTime) => {
return (time: number) => {
/**
* Memoizing this for object reference equality (and reduced memory churn.)
*/
return defaultMemoize((time: number) => {
const {
renderWidth,
renderHeight,
@ -404,7 +416,7 @@ export const inverseProjectionMatrix: (
translateForCamera,
multiply(scaleToClippingPlaneDimensions, multiply(invertY, screenToNDC))
);
};
});
}
);
@ -415,7 +427,8 @@ export const viewableBoundingBox: (state: CameraState) => (time: number) => AABB
clippingPlanes,
inverseProjectionMatrix,
(clippingPlanesAtTime, matrixAtTime) => {
return (time: number) => {
// memoizing this so the AABB returned will be the same object reference if called with the same `time`.
return defaultMemoize((time: number) => {
const { renderWidth, renderHeight } = clippingPlanesAtTime(time);
const matrix = matrixAtTime(time);
const bottomLeftCorner: Vector2 = [0, renderHeight];
@ -424,7 +437,7 @@ export const viewableBoundingBox: (state: CameraState) => (time: number) => AABB
minimum: vector2.applyMatrix3(bottomLeftCorner, matrix),
maximum: vector2.applyMatrix3(topRightCorner, matrix),
};
};
});
}
);
@ -436,6 +449,8 @@ export const projectionMatrix: (state: CameraState) => (time: number) => Matrix3
clippingPlanes,
translation,
(clippingPlanesAtTime, translationAtTime) => {
// memoizing this so the matrix returned will be the same object reference if called with the same `time`.
// this should also save on some memory allocation
return defaultMemoize((time: number) => {
const {
renderWidth,

View file

@ -9,6 +9,13 @@ import { DataState } from '../../types';
import { dataReducer } from './reducer';
import { DataAction } from './action';
import { createStore } from 'redux';
import {
mockTreeWithNoAncestorsAnd2Children,
mockTreeWith2AncestorsAndNoChildren,
} from '../mocks/resolver_tree';
import { uniquePidForProcess } from '../../models/process_event';
import { EndpointEvent } from '../../../../common/endpoint/types';
describe('data state', () => {
let actions: DataAction[] = [];
@ -263,4 +270,87 @@ describe('data state', () => {
});
});
});
describe('with a tree with no descendants and 2 ancestors', () => {
const originID = 'c';
const firstAncestorID = 'b';
const secondAncestorID = 'a';
beforeEach(() => {
actions.push({
type: 'serverReturnedResolverData',
payload: {
result: mockTreeWith2AncestorsAndNoChildren({
originID,
firstAncestorID,
secondAncestorID,
}),
// this value doesn't matter
databaseDocumentID: '',
},
});
});
it('should have no flowto candidate for the origin', () => {
expect(selectors.ariaFlowtoCandidate(state())(originID)).toBe(null);
});
it('should have no flowto candidate for the first ancestor', () => {
expect(selectors.ariaFlowtoCandidate(state())(firstAncestorID)).toBe(null);
});
it('should have no flowto candidate for the second ancestor ancestor', () => {
expect(selectors.ariaFlowtoCandidate(state())(secondAncestorID)).toBe(null);
});
});
describe('with a tree with 2 children and no ancestors', () => {
const originID = 'c';
const firstChildID = 'd';
const secondChildID = 'e';
beforeEach(() => {
actions.push({
type: 'serverReturnedResolverData',
payload: {
result: mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }),
// this value doesn't matter
databaseDocumentID: '',
},
});
});
it('should have no flowto candidate for the origin', () => {
expect(selectors.ariaFlowtoCandidate(state())(originID)).toBe(null);
});
it('should use the second child as the flowto candidate for the first child', () => {
expect(selectors.ariaFlowtoCandidate(state())(firstChildID)).toBe(secondChildID);
});
it('should have no flowto candidate for the second child', () => {
expect(selectors.ariaFlowtoCandidate(state())(secondChildID)).toBe(null);
});
});
describe('with a tree where the root process has no parent info at all', () => {
const originID = 'c';
const firstChildID = 'd';
const secondChildID = 'e';
beforeEach(() => {
const tree = mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID });
for (const event of tree.lifecycle) {
// delete the process.parent key, if present
// cast as `EndpointEvent` because `ResolverEvent` can also be `LegacyEndpointEvent` which has no `process` field
delete (event as EndpointEvent).process?.parent;
}
actions.push({
type: 'serverReturnedResolverData',
payload: {
result: tree,
// this value doesn't matter
databaseDocumentID: '',
},
});
});
it('should be able to calculate the aria flowto candidates for all processes nodes', () => {
const graphables = selectors.graphableProcesses(state());
expect(graphables.length).toBe(3);
for (const event of graphables) {
expect(() => {
selectors.ariaFlowtoCandidate(state())(uniquePidForProcess(event));
}).not.toThrow();
}
});
});
});

View file

@ -19,9 +19,9 @@ import {
isGraphableProcess,
isTerminatedProcess,
uniquePidForProcess,
uniqueParentPidForProcess,
} from '../../models/process_event';
import * as indexedProcessTreeModel from '../../models/indexed_process_tree';
import { isEqual } from '../../models/aabb';
import {
ResolverEvent,
@ -62,7 +62,7 @@ export function hasError(state: DataState): boolean {
* The last ResolverTree we received, if any. It may be stale (it might not be for the same databaseDocumentID that
* we're currently interested in.
*/
const resolverTree = (state: DataState): ResolverTree | undefined => {
const resolverTreeResponse = (state: DataState): ResolverTree | undefined => {
if (state.lastResponse && state.lastResponse.successful) {
return state.lastResponse.result;
} else {
@ -73,7 +73,9 @@ const resolverTree = (state: DataState): ResolverTree | undefined => {
/**
* Process events that will be displayed as terminated.
*/
export const terminatedProcesses = createSelector(resolverTree, function (tree?: ResolverTree) {
export const terminatedProcesses = createSelector(resolverTreeResponse, function (
tree?: ResolverTree
) {
if (!tree) {
return new Set();
}
@ -90,7 +92,7 @@ export const terminatedProcesses = createSelector(resolverTree, function (tree?:
/**
* Process events that will be graphed.
*/
export const graphableProcesses = createSelector(resolverTree, function (tree?) {
export const graphableProcesses = createSelector(resolverTreeResponse, function (tree?) {
if (tree) {
return resolverTreeModel.lifecycleEvents(tree).filter(isGraphableProcess);
} else {
@ -101,7 +103,7 @@ export const graphableProcesses = createSelector(resolverTree, function (tree?)
/**
* The 'indexed process tree' contains the tree data, indexed in helpful ways. Used for O(1) access to stuff during graph layout.
*/
export const indexedProcessTree = createSelector(graphableProcesses, function indexedTree(
export const tree = createSelector(graphableProcesses, function indexedTree(
/* eslint-disable no-shadow */
graphableProcesses
/* eslint-enable no-shadow */
@ -114,13 +116,16 @@ export const indexedProcessTree = createSelector(graphableProcesses, function in
*/
export const relatedEventsStats: (
state: DataState
) => Map<string, ResolverNodeStats> | null = createSelector(resolverTree, (tree?: ResolverTree) => {
if (tree) {
return resolverTreeModel.relatedEventsStats(tree);
} else {
return null;
) => Map<string, ResolverNodeStats> | null = createSelector(
resolverTreeResponse,
(resolverTree?: ResolverTree) => {
if (resolverTree) {
return resolverTreeModel.relatedEventsStats(resolverTree);
} else {
return null;
}
}
});
);
/**
* returns a map of entity_ids to related event data.
@ -133,7 +138,9 @@ export function relatedEventsByEntityId(data: DataState): Map<string, ResolverRe
* Returns a function that returns a function (when supplied with an entity id for a node)
* that returns related events for a node that match an event.category (when supplied with the category)
*/
export const relatedEventsByCategory = createSelector(
export const relatedEventsByCategory: (
state: DataState
) => (entityID: string) => (ecsCategory: string) => ResolverEvent[] = createSelector(
relatedEventsByEntityId,
function provideGettersByCategory(
/* eslint-disable no-shadow */
@ -173,16 +180,16 @@ export function relatedEventsReady(data: DataState): Map<string, boolean> {
* `true` if there were more children than we got in the last request.
*/
export function hasMoreChildren(state: DataState): boolean {
const tree = resolverTree(state);
return tree ? resolverTreeModel.hasMoreChildren(tree) : false;
const resolverTree = resolverTreeResponse(state);
return resolverTree ? resolverTreeModel.hasMoreChildren(resolverTree) : false;
}
/**
* `true` if there were more ancestors than we got in the last request.
*/
export function hasMoreAncestors(state: DataState): boolean {
const tree = resolverTree(state);
return tree ? resolverTreeModel.hasMoreAncestors(tree) : false;
const resolverTree = resolverTreeResponse(state);
return resolverTree ? resolverTreeModel.hasMoreAncestors(resolverTree) : false;
}
interface RelatedInfoFunctions {
@ -248,7 +255,7 @@ export const relatedEventInfoByEntityId: (
});
};
const matchingEventsForCategory = defaultMemoize(unmemoizedMatchingEventsForCategory);
const matchingEventsForCategory = unmemoizedMatchingEventsForCategory;
/**
* The number of events that occurred before the API limit was reached.
@ -313,16 +320,13 @@ export function databaseDocumentIDToFetch(state: DataState): string | null {
}
}
export const layout = createSelector(
indexedProcessTree,
function processNodePositionsAndEdgeLineSegments(
/* eslint-disable no-shadow */
indexedProcessTree
/* eslint-enable no-shadow */
) {
return isometricTaxiLayout(indexedProcessTree);
}
);
export const layout = createSelector(tree, function processNodePositionsAndEdgeLineSegments(
/* eslint-disable no-shadow */
indexedProcessTree
/* eslint-enable no-shadow */
) {
return isometricTaxiLayout(indexedProcessTree);
});
/**
* Given a nodeID (aka entity_id) get the indexed process event.
@ -332,8 +336,9 @@ export const layout = createSelector(
export const processEventForID: (
state: DataState
) => (nodeID: string) => ResolverEvent | null = createSelector(
indexedProcessTree,
(tree) => (nodeID: string) => indexedProcessTreeModel.processEvent(tree, nodeID)
tree,
(indexedProcessTree) => (nodeID: string) =>
indexedProcessTreeModel.processEvent(indexedProcessTree, nodeID)
);
/**
@ -349,30 +354,66 @@ export const ariaLevel: (state: DataState) => (nodeID: string) => number | null
);
/**
* Returns the following sibling if there is one, or `null`.
* Returns the following sibling if there is one, or `null` if there isn't.
* For root nodes, other root nodes are treated as siblings.
* This is used to calculate the `aria-flowto` attribute.
*/
export const followingSibling: (
export const ariaFlowtoCandidate: (
state: DataState
) => (nodeID: string) => string | null = createSelector(
indexedProcessTree,
tree,
processEventForID,
(tree, eventGetter) => {
return (nodeID: string) => {
const event = eventGetter(nodeID);
(indexedProcessTree, eventGetter) => {
// A map of preceding sibling IDs to following sibling IDs or `null`, if there is no following sibling
const memo: Map<string, string | null> = new Map();
// event not found
if (event === null) {
return null;
}
const nextSibling = indexedProcessTreeModel.nextSibling(tree, event);
return function memoizedGetter(/** the unique ID of a node. **/ nodeID: string): string | null {
// Previous calculations are memoized. Check for a value in the memo.
const existingValue = memo.get(nodeID);
// next sibling not found
if (nextSibling === undefined) {
return null;
/**
* `undefined` means the key wasn't in the map.
* Note: the value may be null, meaning that we checked and there is no following sibling.
* If there is a value in the map, return it.
*/
if (existingValue !== undefined) {
return existingValue;
}
// return the node ID
return uniquePidForProcess(nextSibling);
/**
* Getting the following sibling of a node has an `O(n)` time complexity where `n` is the number of children the parent of the node has.
* For this reason, we calculate the following siblings of the node and all of its siblings at once and cache them.
*/
const nodeEvent: ResolverEvent | null = eventGetter(nodeID);
if (!nodeEvent) {
// this should never happen.
throw new Error('could not find child event in process tree.');
}
// nodes with the same parent ID
const children = indexedProcessTreeModel.children(
indexedProcessTree,
uniqueParentPidForProcess(nodeEvent)
);
let previousChild: ResolverEvent | null = null;
// Loop over all nodes that have the same parent ID (even if the parent ID is undefined or points to a node that isn't in the tree.)
for (const child of children) {
if (previousChild !== null) {
// Set the `child` as the following sibling of `previousChild`.
memo.set(uniquePidForProcess(previousChild), uniquePidForProcess(child));
}
// Set the child as the previous child.
previousChild = child;
}
if (previousChild) {
// if there is a previous child, it has no following sibling.
memo.set(uniquePidForProcess(previousChild), null);
}
return memoizedGetter(nodeID);
};
}
);
@ -385,7 +426,7 @@ const spatiallyIndexedLayout: (state: DataState) => rbush<IndexedEntity> = creat
edgeLineSegments,
/* eslint-enable no-shadow */
}) {
const tree: rbush<IndexedEntity> = new rbush();
const spatialIndex: rbush<IndexedEntity> = new rbush();
const processesToIndex: IndexedProcessNode[] = [];
const edgeLineSegmentsToIndex: IndexedEdgeLineSegment[] = [];
@ -421,50 +462,49 @@ const spatiallyIndexedLayout: (state: DataState) => rbush<IndexedEntity> = creat
};
edgeLineSegmentsToIndex.push(indexedLineSegment);
}
tree.load([...processesToIndex, ...edgeLineSegmentsToIndex]);
return tree;
spatialIndex.load([...processesToIndex, ...edgeLineSegmentsToIndex]);
return spatialIndex;
}
);
/**
* Returns nodes and edge lines that could be visible in the `query`.
*/
export const nodesAndEdgelines: (
state: DataState
) => (query: AABB) => VisibleEntites = createSelector(spatiallyIndexedLayout, function (tree) {
// memoize the results of this call to avoid unnecessarily rerunning
let lastBoundingBox: AABB | null = null;
let currentlyVisible: VisibleEntites = {
processNodePositions: new Map<ResolverEvent, Vector2>(),
connectingEdgeLineSegments: [],
};
return (boundingBox: AABB) => {
if (lastBoundingBox !== null && isEqual(lastBoundingBox, boundingBox)) {
return currentlyVisible;
} else {
const {
minimum: [minX, minY],
maximum: [maxX, maxY],
} = boundingBox;
const entities = tree.search({
minX,
minY,
maxX,
maxY,
});
const visibleProcessNodePositions = new Map<ResolverEvent, Vector2>(
entities
.filter((entity): entity is IndexedProcessNode => entity.type === 'processNode')
.map((node) => [node.entity, node.position])
);
const connectingEdgeLineSegments = entities
.filter((entity): entity is IndexedEdgeLineSegment => entity.type === 'edgeLine')
.map((node) => node.entity);
currentlyVisible = {
processNodePositions: visibleProcessNodePositions,
connectingEdgeLineSegments,
};
lastBoundingBox = boundingBox;
return currentlyVisible;
}
};
) => (
/**
* An axis aligned bounding box (in world corrdinates) to search in. Any entities that might collide with this box will be returned.
*/
query: AABB
) => VisibleEntites = createSelector(spatiallyIndexedLayout, function (spatialIndex) {
/**
* Memoized for performance and object reference equality.
*/
return defaultMemoize((boundingBox: AABB) => {
const {
minimum: [minX, minY],
maximum: [maxX, maxY],
} = boundingBox;
const entities = spatialIndex.search({
minX,
minY,
maxX,
maxY,
});
const visibleProcessNodePositions = new Map<ResolverEvent, Vector2>(
entities
.filter((entity): entity is IndexedProcessNode => entity.type === 'processNode')
.map((node) => [node.entity, node.position])
);
const connectingEdgeLineSegments = entities
.filter((entity): entity is IndexedEdgeLineSegment => entity.type === 'edgeLine')
.map((node) => node.entity);
return {
processNodePositions: visibleProcessNodePositions,
connectingEdgeLineSegments,
};
});
});
/**

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EndpointEvent } from '../../../../common/endpoint/types';
/**
* Simple mock endpoint event that works for tree layouts.
*/
export function mockEndpointEvent({
entityID,
name,
parentEntityId,
timestamp,
}: {
entityID: string;
name: string;
parentEntityId: string | undefined;
timestamp: number;
}): EndpointEvent {
return {
'@timestamp': timestamp,
event: {
type: 'start',
category: 'process',
},
process: {
entity_id: entityID,
name,
parent: {
entity_id: parentEntityId,
},
},
} as EndpointEvent;
}

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { mockEndpointEvent } from './endpoint_event';
import { ResolverTree, ResolverEvent } from '../../../../common/endpoint/types';
export function mockTreeWith2AncestorsAndNoChildren({
originID,
firstAncestorID,
secondAncestorID,
}: {
secondAncestorID: string;
firstAncestorID: string;
originID: string;
}): ResolverTree {
const secondAncestor: ResolverEvent = mockEndpointEvent({
entityID: secondAncestorID,
name: 'a',
parentEntityId: 'none',
timestamp: 0,
});
const firstAncestor: ResolverEvent = mockEndpointEvent({
entityID: firstAncestorID,
name: 'b',
parentEntityId: secondAncestorID,
timestamp: 1,
});
const originEvent: ResolverEvent = mockEndpointEvent({
entityID: originID,
name: 'c',
parentEntityId: firstAncestorID,
timestamp: 2,
});
return ({
entityID: originID,
children: {
childNodes: [],
},
ancestry: {
ancestors: [{ lifecycle: [secondAncestor] }, { lifecycle: [firstAncestor] }],
},
lifecycle: [originEvent],
} as unknown) as ResolverTree;
}
export function mockTreeWithNoAncestorsAnd2Children({
originID,
firstChildID,
secondChildID,
}: {
originID: string;
firstChildID: string;
secondChildID: string;
}): ResolverTree {
const origin: ResolverEvent = mockEndpointEvent({
entityID: originID,
name: 'c',
parentEntityId: 'none',
timestamp: 0,
});
const firstChild: ResolverEvent = mockEndpointEvent({
entityID: firstChildID,
name: 'd',
parentEntityId: originID,
timestamp: 1,
});
const secondChild: ResolverEvent = mockEndpointEvent({
entityID: secondChildID,
name: 'e',
parentEntityId: originID,
timestamp: 2,
});
return ({
entityID: originID,
children: {
childNodes: [{ lifecycle: [firstChild] }, { lifecycle: [secondChild] }],
},
ancestry: {
ancestors: [],
},
lifecycle: [origin],
} as unknown) as ResolverTree;
}

View file

@ -9,7 +9,10 @@ import { createStore } from 'redux';
import { ResolverAction } from './actions';
import { resolverReducer } from './reducer';
import * as selectors from './selectors';
import { EndpointEvent, ResolverEvent, ResolverTree } from '../../../common/endpoint/types';
import {
mockTreeWith2AncestorsAndNoChildren,
mockTreeWithNoAncestorsAnd2Children,
} from './mocks/resolver_tree';
describe('resolver selectors', () => {
const actions: ResolverAction[] = [];
@ -33,7 +36,7 @@ describe('resolver selectors', () => {
actions.push({
type: 'serverReturnedResolverData',
payload: {
result: treeWith2AncestorsAndNoChildren({
result: mockTreeWith2AncestorsAndNoChildren({
originID,
firstAncestorID,
secondAncestorID,
@ -71,7 +74,7 @@ describe('resolver selectors', () => {
actions.push({
type: 'serverReturnedResolverData',
payload: {
result: treeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }),
result: mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }),
// this value doesn't matter
databaseDocumentID: '',
},
@ -149,111 +152,3 @@ describe('resolver selectors', () => {
});
});
});
/**
* Simple mock endpoint event that works for tree layouts.
*/
function mockEndpointEvent({
entityID,
name,
parentEntityId,
timestamp,
}: {
entityID: string;
name: string;
parentEntityId: string;
timestamp: number;
}): EndpointEvent {
return {
'@timestamp': timestamp,
event: {
type: 'start',
category: 'process',
},
process: {
entity_id: entityID,
name,
parent: {
entity_id: parentEntityId,
},
},
} as EndpointEvent;
}
function treeWith2AncestorsAndNoChildren({
originID,
firstAncestorID,
secondAncestorID,
}: {
secondAncestorID: string;
firstAncestorID: string;
originID: string;
}): ResolverTree {
const secondAncestor: ResolverEvent = mockEndpointEvent({
entityID: secondAncestorID,
name: 'a',
parentEntityId: 'none',
timestamp: 0,
});
const firstAncestor: ResolverEvent = mockEndpointEvent({
entityID: firstAncestorID,
name: 'b',
parentEntityId: secondAncestorID,
timestamp: 1,
});
const originEvent: ResolverEvent = mockEndpointEvent({
entityID: originID,
name: 'c',
parentEntityId: firstAncestorID,
timestamp: 2,
});
return ({
entityID: originID,
children: {
childNodes: [],
},
ancestry: {
ancestors: [{ lifecycle: [secondAncestor] }, { lifecycle: [firstAncestor] }],
},
lifecycle: [originEvent],
} as unknown) as ResolverTree;
}
function treeWithNoAncestorsAnd2Children({
originID,
firstChildID,
secondChildID,
}: {
originID: string;
firstChildID: string;
secondChildID: string;
}): ResolverTree {
const origin: ResolverEvent = mockEndpointEvent({
entityID: originID,
name: 'c',
parentEntityId: 'none',
timestamp: 0,
});
const firstChild: ResolverEvent = mockEndpointEvent({
entityID: firstChildID,
name: 'd',
parentEntityId: originID,
timestamp: 1,
});
const secondChild: ResolverEvent = mockEndpointEvent({
entityID: secondChildID,
name: 'e',
parentEntityId: originID,
timestamp: 2,
});
return ({
entityID: originID,
children: {
childNodes: [{ lifecycle: [firstChild] }, { lifecycle: [secondChild] }],
},
ancestry: {
ancestors: [],
},
lifecycle: [origin],
} as unknown) as ResolverTree;
}

View file

@ -212,17 +212,6 @@ export const graphableProcesses = composeSelectors(
dataSelectors.graphableProcesses
);
/**
* Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a
* concern-specific selector. `selector` should return the concern-specific state.
*/
function composeSelectors<OuterState, InnerState, ReturnValue>(
selector: (state: OuterState) => InnerState,
secondSelector: (state: InnerState) => ReturnValue
): (state: OuterState) => ReturnValue {
return (state) => secondSelector(selector(state));
}
const boundingBox = composeSelectors(cameraStateSelector, cameraSelectors.viewableBoundingBox);
const nodesAndEdgelines = composeSelectors(dataStateSelector, dataSelectors.nodesAndEdgelines);
@ -246,6 +235,7 @@ export const visibleNodesAndEdgeLines = createSelector(nodesAndEdgelines, boundi
boundingBox
/* eslint-enable no-shadow */
) {
// `boundingBox` and `nodesAndEdgelines` are each memoized.
return (time: number) => nodesAndEdgelines(boundingBox(time));
});
@ -261,14 +251,14 @@ export const ariaLevel: (
/**
* Takes a nodeID (aka entity_id) and returns the node ID of the node that aria should 'flowto' or null
* If the node has a following sibling that is currently visible, that will be returned, otherwise null.
* If the node has a flowto candidate that is currently visible, that will be returned, otherwise null.
*/
export const ariaFlowtoNodeID: (
state: ResolverState
) => (time: number) => (nodeID: string) => string | null = createSelector(
visibleNodesAndEdgeLines,
composeSelectors(dataStateSelector, dataSelectors.followingSibling),
(visibleNodesAndEdgeLinesAtTime, followingSibling) => {
composeSelectors(dataStateSelector, dataSelectors.ariaFlowtoCandidate),
(visibleNodesAndEdgeLinesAtTime, ariaFlowtoCandidate) => {
return defaultMemoize((time: number) => {
// get the visible nodes at `time`
const { processNodePositions } = visibleNodesAndEdgeLinesAtTime(time);
@ -280,10 +270,23 @@ export const ariaFlowtoNodeID: (
// return the ID of `nodeID`'s following sibling, if it is visible
return (nodeID: string): string | null => {
const sibling: string | null = followingSibling(nodeID);
const flowtoNode: string | null = ariaFlowtoCandidate(nodeID);
return sibling === null || nodesVisibleAtTime.has(sibling) === false ? null : sibling;
return flowtoNode === null || nodesVisibleAtTime.has(flowtoNode) === false
? null
: flowtoNode;
};
});
}
);
/**
* Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a
* concern-specific selector. `selector` should return the concern-specific state.
*/
function composeSelectors<OuterState, InnerState, ReturnValue>(
selector: (state: OuterState) => InnerState,
secondSelector: (state: InnerState) => ReturnValue
): (state: OuterState) => ReturnValue {
return (state) => secondSelector(selector(state));
}

View file

@ -169,7 +169,9 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({
process,
...relevantData
} = relatedEventToShowDetailsFor as ResolverEvent & {
// Type this with various unknown keys so that ts will let us delete those keys
ecs: unknown;
process: unknown;
};
let displayDate = '';
const sectionData: Array<{
@ -371,4 +373,3 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({
</>
);
});
RelatedEventDetail.displayName = 'RelatedEventDetail';