Skip to content

Commit 67992ea

Browse files
authored
Merge pull request #3672 from BSd3v/fix/3661-compute-graphs2
adjustments for computeGraphs
2 parents 550a4d6 + ac5ca89 commit 67992ea

File tree

6 files changed

+244
-41
lines changed

6 files changed

+244
-41
lines changed

dash/dash-renderer/src/APIController.react.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ function storeEffect(props, events, setErrorLoading) {
143143
graphs,
144144
hooks,
145145
layout,
146-
layoutRequest
146+
layoutRequest,
147+
config
147148
} = props;
148149

149150
batch(() => {
@@ -187,7 +188,8 @@ function storeEffect(props, events, setErrorLoading) {
187188
setGraphs(
188189
computeGraphs(
189190
dependenciesRequest.content,
190-
dispatchError(dispatch)
191+
dispatchError(dispatch),
192+
config
191193
)
192194
)
193195
);

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

Lines changed: 86 additions & 37 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 = [];
@@ -626,9 +652,10 @@ export function validateCallbacksToLayout(state_, dispatchError) {
626652
validatePatterns(inputPatterns, 'Input');
627653
}
628654

629-
export function computeGraphs(dependencies, dispatchError) {
655+
export function computeGraphs(dependencies, dispatchError, config) {
630656
// multiGraph is just for finding circular deps
631657
const multiGraph = new DepGraph();
658+
const start = performance.now();
632659

633660
const wildcardPlaceholders = {};
634661

@@ -657,7 +684,9 @@ export function computeGraphs(dependencies, dispatchError) {
657684
hasError = true;
658685
dispatchError(message, lines);
659686
};
660-
validateDependencies(parsedDependencies, wrappedDE);
687+
if (config.validate_callbacks) {
688+
validateDependencies(parsedDependencies, wrappedDE);
689+
}
661690

662691
/*
663692
* For regular ids, outputMap and inputMap are:
@@ -683,8 +712,10 @@ export function computeGraphs(dependencies, dispatchError) {
683712
*/
684713
const outputMap = {};
685714
const inputMap = {};
686-
const outputPatterns = {};
687-
const inputPatterns = {};
715+
const outputPatternMap = new Map();
716+
const inputPatternMap = new Map();
717+
let outputPatterns = {};
718+
let inputPatterns = {};
688719

689720
const finalGraphs = {
690721
MultiGraph: multiGraph,
@@ -701,12 +732,14 @@ export function computeGraphs(dependencies, dispatchError) {
701732
return finalGraphs;
702733
}
703734

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.
704736
parsedDependencies.forEach(dependency => {
705737
const {outputs, inputs} = dependency;
706738

707-
outputs.concat(inputs).forEach(item => {
708-
const {id} = item;
709-
if (typeof id === 'object') {
739+
outputs
740+
.concat(inputs)
741+
.filter(item => typeof item.id === 'object')
742+
.forEach(item => {
710743
forEachObjIndexed((val, key) => {
711744
if (!wildcardPlaceholders[key]) {
712745
wildcardPlaceholders[key] = {
@@ -722,11 +755,11 @@ export function computeGraphs(dependencies, dispatchError) {
722755
} else if (keyPlaceholders.exact.indexOf(val) === -1) {
723756
keyPlaceholders.exact.push(val);
724757
}
725-
}, id);
726-
}
727-
});
758+
}, item.id);
759+
});
728760
});
729761

762+
// Efficiently build wildcardPlaceholders.vals arrays
730763
forEachObjIndexed(keyPlaceholders => {
731764
const {exact, expand} = keyPlaceholders;
732765
const vals = exact.slice().sort(idValSort);
@@ -808,6 +841,7 @@ export function computeGraphs(dependencies, dispatchError) {
808841
const cbOut = [];
809842

810843
function addInputToMulti(inIdProp, outIdProp, firstPass = true) {
844+
if (!config.validate_callbacks) return;
811845
multiGraph.addNode(inIdProp);
812846
multiGraph.addDependency(inIdProp, outIdProp);
813847
// only store callback inputs and outputs during the first pass
@@ -825,6 +859,7 @@ export function computeGraphs(dependencies, dispatchError) {
825859
cbOut.push([]);
826860

827861
function addOutputToMulti(outIdFinal, outIdProp) {
862+
if (!config.validate_callbacks) return;
828863
multiGraph.addNode(outIdProp);
829864
inputs.forEach(inObj => {
830865
const {id: inId, property} = inObj;
@@ -859,41 +894,50 @@ export function computeGraphs(dependencies, dispatchError) {
859894
outputs.forEach(outIdProp => {
860895
const {id: outId, property} = outIdProp;
861896
// check if this output is also an input to the same callback
862-
const alsoInput = checkInOutOverlap(outIdProp, inputs);
897+
let alsoInput;
898+
if (config.validate_callbacks) {
899+
alsoInput = checkInOutOverlap(outIdProp, inputs);
900+
}
863901
if (typeof outId === 'object') {
864-
const outIdList = makeAllIds(outId, {});
865-
outIdList.forEach(id => {
866-
const tempOutIdProp = {id, property};
867-
let outIdName = combineIdAndProp(tempOutIdProp);
902+
if (config.validate_callbacks) {
903+
const outIdList = makeAllIds(outId, {});
904+
outIdList.forEach(id => {
905+
const tempOutIdProp = {id, property};
906+
let outIdName = combineIdAndProp(tempOutIdProp);
907+
// if this output is also an input, add `outputTag` to the name
908+
if (alsoInput) {
909+
duplicateOutputs.push(tempOutIdProp);
910+
outIdName += outputTag;
911+
}
912+
addOutputToMulti(id, outIdName);
913+
});
914+
}
915+
addPattern(outputPatternMap, outId, property, finalDependency);
916+
} else {
917+
if (config.validate_callbacks) {
918+
let outIdName = combineIdAndProp(outIdProp);
868919
// if this output is also an input, add `outputTag` to the name
869920
if (alsoInput) {
870-
duplicateOutputs.push(tempOutIdProp);
921+
duplicateOutputs.push(outIdProp);
871922
outIdName += outputTag;
872923
}
873-
addOutputToMulti(id, outIdName);
874-
});
875-
addPattern(outputPatterns, outId, property, finalDependency);
876-
} else {
877-
let outIdName = combineIdAndProp(outIdProp);
878-
// if this output is also an input, add `outputTag` to the name
879-
if (alsoInput) {
880-
duplicateOutputs.push(outIdProp);
881-
outIdName += outputTag;
924+
addOutputToMulti({}, outIdName);
882925
}
883-
addOutputToMulti({}, outIdName);
884926
addMap(outputMap, outId, property, finalDependency);
885927
}
886928
});
887929

888930
inputs.forEach(inputObject => {
889931
const {id: inId, property: inProp} = inputObject;
890932
if (typeof inId === 'object') {
891-
addPattern(inputPatterns, inId, inProp, finalDependency);
933+
addPattern(inputPatternMap, inId, inProp, finalDependency);
892934
} else {
893935
addMap(inputMap, inId, inProp, finalDependency);
894936
}
895937
});
896938
});
939+
outputPatterns = offloadPatterns(outputPatternMap, outputPatterns);
940+
inputPatterns = offloadPatterns(inputPatternMap, inputPatterns);
897941

898942
// second pass for adding new output nodes as dependencies where needed
899943
duplicateOutputs.forEach(dupeOutIdProp => {
@@ -913,6 +957,11 @@ export function computeGraphs(dependencies, dispatchError) {
913957
}
914958
}
915959
});
960+
const end = performance.now();
961+
if (!window.dash_component_api) {
962+
window.dash_component_api = {};
963+
}
964+
window.dash_component_api.callbackGraphTime = (end - start).toFixed(2);
916965

917966
return finalGraphs;
918967
}

dash/dash-renderer/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type DashConfig = {
2121
};
2222
serve_locally?: boolean;
2323
plotlyjs_url?: string;
24+
validate_callbacks: boolean;
2425
};
2526

2627
export default function getConfigFromDOM(): DashConfig {

dash/dash-renderer/src/dashApi.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function getLayout(componentPathOrId: DashLayoutPath | string): any {
3535
}
3636
}
3737

38-
window.dash_component_api = {
38+
window.dash_component_api = Object.assign(window.dash_component_api || {}, {
3939
ExternalWrapper,
4040
DashContext,
4141
useDashContext,
@@ -46,4 +46,4 @@ window.dash_component_api = {
4646
useDevtool,
4747
useDevtoolMenuButtonClassName
4848
}
49-
};
49+
});

dash/dash.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,7 @@ def _config(self):
937937
"dash_version_url": DASH_VERSION_URL,
938938
"ddk_version": ddk_version,
939939
"plotly_version": plotly_version,
940+
"validate_callbacks": self._dev_tools.validate_callbacks,
940941
}
941942
if self._plotly_cloud is None:
942943
if os.getenv("DASH_ENTERPRISE_ENV") == "WORKSPACE":
@@ -1968,6 +1969,7 @@ def _setup_dev_tools(self, **kwargs):
19681969
"hot_reload",
19691970
"silence_routes_logging",
19701971
"prune_errors",
1972+
"validate_callbacks",
19711973
):
19721974
dev_tools[attr] = get_combined_config(
19731975
attr, kwargs.get(attr, None), default=debug
@@ -2003,6 +2005,7 @@ def enable_dev_tools( # pylint: disable=too-many-branches
20032005
dev_tools_silence_routes_logging: Optional[bool] = None,
20042006
dev_tools_disable_version_check: Optional[bool] = None,
20052007
dev_tools_prune_errors: Optional[bool] = None,
2008+
dev_tools_validate_callbacks: Optional[bool] = None,
20062009
) -> bool:
20072010
"""Activate the dev tools, called by `run`. If your application
20082011
is served by wsgi and you want to activate the dev tools, you can call
@@ -2024,6 +2027,7 @@ def enable_dev_tools( # pylint: disable=too-many-branches
20242027
- DASH_SILENCE_ROUTES_LOGGING
20252028
- DASH_DISABLE_VERSION_CHECK
20262029
- DASH_PRUNE_ERRORS
2030+
- DASH_VALIDATE_CALLBACKS
20272031
20282032
:param debug: Enable/disable all the dev tools unless overridden by the
20292033
arguments or environment variables. Default is ``True`` when
@@ -2079,6 +2083,10 @@ def enable_dev_tools( # pylint: disable=too-many-branches
20792083
env: ``DASH_PRUNE_ERRORS``
20802084
:type dev_tools_prune_errors: bool
20812085
2086+
:param dev_tools_validate_callbacks: Check for circular callback
2087+
dependencies and raise an error if any are found. env: ``DASH_VALIDATE_CALLBACKS``
2088+
:type dev_tools_validate_callbacks: bool
2089+
20822090
:return: debug
20832091
"""
20842092
if debug is None:
@@ -2096,6 +2104,7 @@ def enable_dev_tools( # pylint: disable=too-many-branches
20962104
silence_routes_logging=dev_tools_silence_routes_logging,
20972105
disable_version_check=dev_tools_disable_version_check,
20982106
prune_errors=dev_tools_prune_errors,
2107+
validate_callbacks=dev_tools_validate_callbacks,
20992108
)
21002109

21012110
if dev_tools.silence_routes_logging:
@@ -2319,6 +2328,7 @@ def run(
23192328
dev_tools_silence_routes_logging: Optional[bool] = None,
23202329
dev_tools_disable_version_check: Optional[bool] = None,
23212330
dev_tools_prune_errors: Optional[bool] = None,
2331+
dev_tools_validate_callbacks: Optional[bool] = None,
23222332
**flask_run_options,
23232333
):
23242334
"""Start the flask server in local mode, you should not run this on a
@@ -2409,6 +2419,10 @@ def run(
24092419
env: ``DASH_PRUNE_ERRORS``
24102420
:type dev_tools_prune_errors: bool
24112421
2422+
:param dev_tools_validate_callbacks: Check for circular callback
2423+
dependencies and raise an error if any are found. env: ``DASH_VALIDATE_CALLBACKS``
2424+
:type dev_tools_validate_callbacks: bool
2425+
24122426
:param jupyter_mode: How to display the application when running
24132427
inside a jupyter notebook.
24142428
@@ -2446,6 +2460,7 @@ def run(
24462460
dev_tools_silence_routes_logging,
24472461
dev_tools_disable_version_check,
24482462
dev_tools_prune_errors,
2463+
dev_tools_validate_callbacks,
24492464
)
24502465

24512466
# Evaluate the env variables at runtime

0 commit comments

Comments
 (0)