From 1542bc61e2af52bd4c96b68fa66d9a6aa35688c3 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Tue, 29 Oct 2024 09:31:50 +1300 Subject: [PATCH 1/2] Implement performance flags structure Performance flags (reduxState.controls.performanceFlags) represent flags for which we enable/disable certain functionality in Auspice. These flags shouldn't be depended on, i.e. Auspice should work just fine without them (but may be a little slow). --- src/components/tree/index.ts | 3 ++- src/middleware/performanceFlags.js | 26 ++++++++++++++++++++++++++ src/reducers/controls.ts | 3 ++- src/store.ts | 2 ++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/middleware/performanceFlags.js diff --git a/src/components/tree/index.ts b/src/components/tree/index.ts index 07218e725..9f0cc1d2c 100644 --- a/src/components/tree/index.ts +++ b/src/components/tree/index.ts @@ -30,7 +30,8 @@ const Tree = connect((state: RootState) => ({ tipLabelKey: state.controls.tipLabelKey, narrativeMode: state.narrative.display, animationPlayPauseButton: state.controls.animationPlayPauseButton, - showOnlyPanels: state.controls.showOnlyPanels + showOnlyPanels: state.controls.showOnlyPanels, + performanceFlags: state.controls.performanceFlags, }))(UnconnectedTree); export default Tree; diff --git a/src/middleware/performanceFlags.js b/src/middleware/performanceFlags.js new file mode 100644 index 000000000..ee0c889b0 --- /dev/null +++ b/src/middleware/performanceFlags.js @@ -0,0 +1,26 @@ +import * as types from "../actions/types"; + +/** + * Performance flags (reduxState.controls.performanceFlags) represent flags + * for which we enable/disable certain functionality in Auspice. These flags + * shouldn't be depended on, i.e. Auspice should work just fine without them + * (but may be a little slow). + */ + + +export const performanceFlags = (_store) => (next) => (action) => { + let modifiedAction; + switch (action.type) { + case types.URL_QUERY_CHANGE_WITH_COMPUTED_STATE: /* fallthrough */ + case types.CLEAN_START: { + modifiedAction = {...action}; + modifiedAction.controls.performanceFlags = calculate() + } + } + return next(modifiedAction || action); // send action to other middleware / reducers +}; + +function calculate() { + const flags = new Map(); + return flags; +} \ No newline at end of file diff --git a/src/reducers/controls.ts b/src/reducers/controls.ts index 5dc967b11..5290c2440 100644 --- a/src/reducers/controls.ts +++ b/src/reducers/controls.ts @@ -137,7 +137,8 @@ export const getDefaultControlsState = () => { measurementsDisplay: undefined, measurementsShowOverallMean: undefined, measurementsShowThreshold: undefined, - measurementsFilters: {} + measurementsFilters: {}, + performanceFlags: new Map(), }; }; diff --git a/src/store.ts b/src/store.ts index 4943b23ae..d68e74d09 100644 --- a/src/store.ts +++ b/src/store.ts @@ -3,10 +3,12 @@ import { changeURLMiddleware } from "./middleware/changeURL"; import rootReducer from "./reducers"; // import { loggingMiddleware } from "./middleware/logActions"; import { keepScatterplotStateInSync } from "./middleware/scatterplot"; +import { performanceFlags } from "./middleware/performanceFlags"; const middleware = [ keepScatterplotStateInSync, changeURLMiddleware, + performanceFlags, // loggingMiddleware ]; From 3a18d773adedafe6f41fce26fa7d814ceeec395f Mon Sep 17 00:00:00 2001 From: james hadfield Date: Tue, 29 Oct 2024 10:04:38 +1300 Subject: [PATCH 2/2] Skip (some) tree animations for big trees For big trees, attempting to animate changes results in poor performance and we're better off making all SVG changes in a single pass. "big" here depends on both the browser's performance and the size of the tree. Here we just use the latter as it's simpler to code, but in the future we may measure performance as the user interacts with the viz and update these flags accordingly. (This is one of the reasons we calculate these in middleware.) For future, we should add nuance to this performance flag. For instance, viewing a 2k tree and zooming into a clade has poor performance and we'd be better off skipping the animation, however once in a small clade it works well. For that same tree (viewing all 2k tips) filtering to a clade still looks good with the animation. 4000 tips is arbitrary, it was chosen after testing a variety of datasets locally. Note that there is a big difference between not setting a transition (within d3) and setting a transition of 0ms - the former is much faster. See for more details here. I didn't implement this change for `modifySVGInStages` as it uses a more complex scheduling system; but the performance here is terrible for really large trees so we should revisit this in future work. Performance changes ------------------- Auspice built in production mode on Chrome 130.0 on a Apple M3 pro chip. Datasets: a large ncov tree (23k tips) (see for more), and a flu tree of 2k tips. Both only rendering the tree panel. Flu performance is unchanged, as 2k tips isn't enough to trigger this performance toggle. For posterity: * Zooming into clade 2a. Initial frame ~160ms, then 30fps. * Filtering to clade 2a. Initial frame ~115ms, then 60fps. * Animation: 60fps ncov performance: * Zoom into clade 21M. * Previously: Initial frame 1500ms, second (final frame) 430ms later, main blocked for a further 700ms * Now: Single frame of 921ms. Main blocked for a further 550ms. * Change: Time to correct tree reduced by ~1000ms (50%). Time until main is no longer blocked: reduced by ~1100ms (45%). * Filter to clade 21M. * Previously: Initial frame 1050ms, second frame 930ms later, main blocked for a further 660ms * Now: Single frame of 1018ms, main blocked for a further 660ms * Change: Time to correct tree reduced by ~950ms (50%). Time until main is no longer blocked: reduced by ~950ms (35%). * Animate. This is unchanged - Auspice sets the transition time to 0 (see end of this commit message). We get around 4 frames at 70ms each (14fps) then a long commit phase blocking for ~650ms. For unknown reasons we trigger a timer flush but it doesn't show up in the performance snapshot, which is why these times are unchanged by this commit. Note that the timings for the initial frame are slightly variable in chrome. The JS time / blinkng pipeline parts shown on the main thread seem pretty consistent, but when the frame is drawn (relative to when the commit phase starts) is more variable. So take the above results with a grain of salt. P.S. there is a somewhat parallel implementation of this concept in Auspice where PhyloTree sets a transition time of zero if subsequent updates come in quick enough, but that is buggy. Firstly, if an update takes a long time the main thread will be blocked so the next update will be indistinguishable from one which was triggered an acceptable time later. Secondly, it only works for animation where we have a stream of updates. --- src/components/tree/phyloTree/change.js | 19 +++++++++---------- src/components/tree/phyloTree/labels.js | 11 +++++------ .../tree/reactD3Interface/change.js | 1 + src/middleware/performanceFlags.js | 6 ++++-- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/components/tree/phyloTree/change.js b/src/components/tree/phyloTree/change.js index 78fd96e1d..1a0b6bedc 100644 --- a/src/components/tree/phyloTree/change.js +++ b/src/components/tree/phyloTree/change.js @@ -99,15 +99,12 @@ const createUpdateCall = (treeElem, properties) => (selection) => { const genericSelectAndModify = (svg, treeElem, updateCall, transitionTime) => { // console.log("general svg update for", treeElem); - svg.selectAll(treeElem) - .filter((d) => d.update) - .transition().duration(transitionTime) - .call(updateCall); - if (!transitionTime) { - /* https://github.com/d3/d3-timer#timerFlush */ - timerFlush(); - // console.log("\t\t--FLUSHING TIMER--"); + let selection = svg.selectAll(treeElem) + .filter((d) => d.update); + if (transitionTime) { + selection = selection.transition().duration(transitionTime); } + selection.call(updateCall); }; /* use D3 to select and modify elements, such that a given element is only ever modified _once_ @@ -271,7 +268,8 @@ export const change = function change({ branchThickness = undefined, /* other data */ focus = undefined, - scatterVariables = undefined + scatterVariables = undefined, + performanceFlags = {}, }) { // console.log("\n** phylotree.change() (time since last run:", Date.now() - this.timeLastRenderRequested, "ms) **\n\n"); timerStart("phylotree.change()"); @@ -280,10 +278,11 @@ export const change = function change({ const svgPropsToUpdate = new Set(); /* which SVG properties shall be changed. E.g. "fill", "stroke" */ const useModifySVGInStages = newLayout; /* use modifySVGInStages rather than modifySVG. Not used often. */ + /* calculate dt */ const idealTransitionTime = 500; let transitionTime = idealTransitionTime; - if ((Date.now() - this.timeLastRenderRequested) < idealTransitionTime * 2) { + if ((Date.now() - this.timeLastRenderRequested) < idealTransitionTime * 2 || performanceFlags.get("skipTreeAnimation")===true) { transitionTime = 0; } diff --git a/src/components/tree/phyloTree/labels.js b/src/components/tree/phyloTree/labels.js index 31fe9db88..41d0ca35e 100644 --- a/src/components/tree/phyloTree/labels.js +++ b/src/components/tree/phyloTree/labels.js @@ -1,4 +1,3 @@ -import { timerFlush } from "d3-timer"; import { NODE_VISIBLE } from "../../../util/globals"; import { numericToDateObject, prettifyDate } from "../../../util/dateHelpers"; import { getTraitFromNode } from "../../../util/treeMiscHelpers"; @@ -108,16 +107,16 @@ export const updateBranchLabels = function updateBranchLabels(dt) { ); const labelSize = branchLabelSize(this.params.branchLabelKey); const fontWeight = branchLabelFontWeight(this.params.branchLabelKey); - this.groups.branchLabels + let selection = this.groups.branchLabels .selectAll(".branchLabel") - .transition() - .duration(dt) - .attr("x", (d) => d.xTip - 5) + if (dt) { + selection = selection.transition().duration(dt); + } + selection.attr("x", (d) => d.xTip - 5) .attr("y", (d) => d.yTip - this.params.branchLabelPadY) .style("visibility", visibility) .style("font-weight", fontWeight) .style("font-size", labelSize); - if (!dt) timerFlush(); }; export const removeBranchLabels = function removeBranchLabels() { diff --git a/src/components/tree/reactD3Interface/change.js b/src/components/tree/reactD3Interface/change.js index 67a3b6707..c657bd7a2 100644 --- a/src/components/tree/reactD3Interface/change.js +++ b/src/components/tree/reactD3Interface/change.js @@ -127,6 +127,7 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps, const change = Object.keys(args).length; if (change) { args.animationInProgress = newProps.animationPlayPauseButton === "Pause"; + args.performanceFlags = newProps.performanceFlags; // console.log('\n\n** ', phylotree.id, 'changePhyloTreeViaPropsComparison **', args); phylotree.change(args); } diff --git a/src/middleware/performanceFlags.js b/src/middleware/performanceFlags.js index ee0c889b0..11650ad7f 100644 --- a/src/middleware/performanceFlags.js +++ b/src/middleware/performanceFlags.js @@ -14,13 +14,15 @@ export const performanceFlags = (_store) => (next) => (action) => { case types.URL_QUERY_CHANGE_WITH_COMPUTED_STATE: /* fallthrough */ case types.CLEAN_START: { modifiedAction = {...action}; - modifiedAction.controls.performanceFlags = calculate() + modifiedAction.controls.performanceFlags = calculate(action) } } return next(modifiedAction || action); // send action to other middleware / reducers }; -function calculate() { +function calculate({tree}) { const flags = new Map(); + const totalTipCount = tree?.nodes?.[0]?.fullTipCount; + flags.set("skipTreeAnimation", totalTipCount > 4000); return flags; } \ No newline at end of file