Skip to content

Commit 680ad95

Browse files
committed
adjustments to use pattern maps and then offload to the expected patterns for callbacks, great reduction in overhead
1 parent f6c604c commit 680ad95

2 files changed

Lines changed: 58 additions & 26 deletions

File tree

dash/dash-renderer/src/actions/dependencies.js

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -163,26 +163,52 @@ function addMap(depMap, id, prop, dependency) {
163163
callbacks.push(dependency);
164164
}
165165

166-
function addPattern(depMap, idSpec, prop, dependency) {
166+
// Patterns are stored in a nested Map structure to avoid the overhead of
167+
// stringifying ids for every callback.
168+
function addPattern(patterns, idSpec, prop, dependency) {
167169
const keys = Object.keys(idSpec).sort();
168170
const keyStr = keys.join(',');
169171
const values = props(keys, idSpec);
170-
const keyCallbacks = (depMap[keyStr] = depMap[keyStr] || {});
171-
const propCallbacks = (keyCallbacks[prop] = keyCallbacks[prop] || []);
172-
let valMatch = false;
173-
for (let i = 0; i < propCallbacks.length; i++) {
174-
if (equals(values, propCallbacks[i].values)) {
175-
valMatch = propCallbacks[i];
176-
break;
177-
}
172+
const valuesKey = values
173+
.map(v =>
174+
typeof v === 'object' && v !== null
175+
? v.wild
176+
? v.wild
177+
: JSON.stringify(v)
178+
: String(v)
179+
)
180+
.join('|');
181+
182+
if (!patterns.has(keyStr)) {
183+
patterns.set(keyStr, new Map());
184+
}
185+
const propMap = patterns.get(keyStr);
186+
if (!propMap.has(prop)) {
187+
propMap.set(prop, new Map());
178188
}
189+
const valueMap = propMap.get(prop);
190+
191+
let valMatch = valueMap.get(valuesKey);
179192
if (!valMatch) {
180193
valMatch = {keys, values, callbacks: []};
181-
propCallbacks.push(valMatch);
194+
valueMap.set(valuesKey, valMatch);
182195
}
183196
valMatch.callbacks.push(dependency);
184197
}
185198

199+
// Convert the nested Map structure of patterns into the plain nested object structure
200+
// expected by the rest of the code, with stringified id keys.
201+
// This is only done once per pattern, at the end of graph construction,
202+
// to minimize the overhead of stringifying ids.
203+
function offloadPatterns(patternsMap, targetMap) {
204+
for (const [keyStr, propMap] of patternsMap.entries()) {
205+
targetMap[keyStr] = {};
206+
for (const [prop, valueMap] of propMap.entries()) {
207+
targetMap[keyStr][prop] = Array.from(valueMap.values());
208+
}
209+
}
210+
}
211+
186212
function validateDependencies(parsedDependencies, dispatchError) {
187213
const outStrs = {};
188214
const outObjs = [];
@@ -686,8 +712,10 @@ export function computeGraphs(dependencies, dispatchError, config) {
686712
*/
687713
const outputMap = {};
688714
const inputMap = {};
689-
const outputPatterns = {};
690-
const inputPatterns = {};
715+
const outputPatternMap = new Map();
716+
const inputPatternMap = new Map();
717+
let outputPatterns = {};
718+
let inputPatterns = {};
691719

692720
const finalGraphs = {
693721
MultiGraph: multiGraph,
@@ -704,12 +732,14 @@ export function computeGraphs(dependencies, dispatchError, config) {
704732
return finalGraphs;
705733
}
706734

735+
// builds up wildcardPlaceholders with all the wildcard keys and values used in the callbacks, so we can generate the full list of ids that each callback depends on.
707736
parsedDependencies.forEach(dependency => {
708737
const {outputs, inputs} = dependency;
709738

710-
outputs.concat(inputs).forEach(item => {
711-
const {id} = item;
712-
if (typeof id === 'object') {
739+
outputs
740+
.concat(inputs)
741+
.filter(item => typeof item.id === 'object')
742+
.forEach(item => {
713743
forEachObjIndexed((val, key) => {
714744
if (!wildcardPlaceholders[key]) {
715745
wildcardPlaceholders[key] = {
@@ -725,11 +755,11 @@ export function computeGraphs(dependencies, dispatchError, config) {
725755
} else if (keyPlaceholders.exact.indexOf(val) === -1) {
726756
keyPlaceholders.exact.push(val);
727757
}
728-
}, id);
729-
}
730-
});
758+
}, item.id);
759+
});
731760
});
732761

762+
// Efficiently build wildcardPlaceholders.vals arrays
733763
forEachObjIndexed(keyPlaceholders => {
734764
const {exact, expand} = keyPlaceholders;
735765
const vals = exact.slice().sort(idValSort);
@@ -811,7 +841,7 @@ export function computeGraphs(dependencies, dispatchError, config) {
811841
const cbOut = [];
812842

813843
function addInputToMulti(inIdProp, outIdProp, firstPass = true) {
814-
if (!config.validate_callbacks) return
844+
if (!config.validate_callbacks) return;
815845
multiGraph.addNode(inIdProp);
816846
multiGraph.addDependency(inIdProp, outIdProp);
817847
// only store callback inputs and outputs during the first pass
@@ -829,7 +859,7 @@ export function computeGraphs(dependencies, dispatchError, config) {
829859
cbOut.push([]);
830860

831861
function addOutputToMulti(outIdFinal, outIdProp) {
832-
if (!config.validate_callbacks) return
862+
if (!config.validate_callbacks) return;
833863
multiGraph.addNode(outIdProp);
834864
inputs.forEach(inObj => {
835865
const {id: inId, property} = inObj;
@@ -866,7 +896,7 @@ export function computeGraphs(dependencies, dispatchError, config) {
866896
// check if this output is also an input to the same callback
867897
let alsoInput;
868898
if (config.validate_callbacks) {
869-
alsoInput = checkInOutOverlap(outIdProp, inputs);
899+
alsoInput = checkInOutOverlap(outIdProp, inputs);
870900
}
871901
if (typeof outId === 'object') {
872902
if (config.validate_callbacks) {
@@ -882,7 +912,7 @@ export function computeGraphs(dependencies, dispatchError, config) {
882912
addOutputToMulti(id, outIdName);
883913
});
884914
}
885-
addPattern(outputPatterns, outId, property, finalDependency);
915+
addPattern(outputPatternMap, outId, property, finalDependency);
886916
} else {
887917
if (config.validate_callbacks) {
888918
let outIdName = combineIdAndProp(outIdProp);
@@ -900,12 +930,14 @@ export function computeGraphs(dependencies, dispatchError, config) {
900930
inputs.forEach(inputObject => {
901931
const {id: inId, property: inProp} = inputObject;
902932
if (typeof inId === 'object') {
903-
addPattern(inputPatterns, inId, inProp, finalDependency);
933+
addPattern(inputPatternMap, inId, inProp, finalDependency);
904934
} else {
905935
addMap(inputMap, inId, inProp, finalDependency);
906936
}
907937
});
908938
});
939+
outputPatterns = offloadPatterns(outputPatternMap, outputPatterns);
940+
inputPatterns = offloadPatterns(inputPatternMap, inputPatterns);
909941

910942
// second pass for adding new output nodes as dependencies where needed
911943
duplicateOutputs.forEach(dupeOutIdProp => {

tests/integration/renderer/test_benchmarking.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pytest
55

66

7-
def make_app(num_groups=500, items_per_group=10):
7+
def make_app(num_groups=500, items_per_group=20):
88
app = Dash(__name__)
99

1010
NUM_GROUPS = num_groups
@@ -131,5 +131,5 @@ def test_compute_graph_timing(dash_duo, dev_tools, store):
131131
if store == "disabled":
132132
print(f"Average time with store disabled: {avg_time:.2f} ms")
133133
assert (
134-
avg_time < 500
135-
), "Expected average time to be under 1/2 seconds with circular callback check disabled"
134+
avg_time < 100
135+
), "Expected average time to be under 100 ms with circular callback check disabled"

0 commit comments

Comments
 (0)