Skip to content

Commit 98da561

Browse files
committed
adjustments for computeGraphs to not have validation when in debug is False or dev_tools_validate_callbacks is False
(cherry picked from commit 077a3e7)
1 parent eb69078 commit 98da561

5 files changed

Lines changed: 187 additions & 21 deletions

File tree

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: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -626,9 +626,10 @@ export function validateCallbacksToLayout(state_, dispatchError) {
626626
validatePatterns(inputPatterns, 'Input');
627627
}
628628

629-
export function computeGraphs(dependencies, dispatchError) {
629+
export function computeGraphs(dependencies, dispatchError, config) {
630630
// multiGraph is just for finding circular deps
631631
const multiGraph = new DepGraph();
632+
const start = performance.now();
632633

633634
const wildcardPlaceholders = {};
634635

@@ -657,7 +658,9 @@ export function computeGraphs(dependencies, dispatchError) {
657658
hasError = true;
658659
dispatchError(message, lines);
659660
};
660-
validateDependencies(parsedDependencies, wrappedDE);
661+
if (config.validate_callbacks) {
662+
validateDependencies(parsedDependencies, wrappedDE);
663+
}
661664

662665
/*
663666
* For regular ids, outputMap and inputMap are:
@@ -808,6 +811,7 @@ export function computeGraphs(dependencies, dispatchError) {
808811
const cbOut = [];
809812

810813
function addInputToMulti(inIdProp, outIdProp, firstPass = true) {
814+
if (!config.validate_callbacks) return
811815
multiGraph.addNode(inIdProp);
812816
multiGraph.addDependency(inIdProp, outIdProp);
813817
// only store callback inputs and outputs during the first pass
@@ -825,6 +829,7 @@ export function computeGraphs(dependencies, dispatchError) {
825829
cbOut.push([]);
826830

827831
function addOutputToMulti(outIdFinal, outIdProp) {
832+
if (!config.validate_callbacks) return
828833
multiGraph.addNode(outIdProp);
829834
inputs.forEach(inObj => {
830835
const {id: inId, property} = inObj;
@@ -859,28 +864,35 @@ export function computeGraphs(dependencies, dispatchError) {
859864
outputs.forEach(outIdProp => {
860865
const {id: outId, property} = outIdProp;
861866
// check if this output is also an input to the same callback
862-
const alsoInput = checkInOutOverlap(outIdProp, inputs);
867+
let alsoInput;
868+
if (config.validate_callbacks) {
869+
alsoInput = checkInOutOverlap(outIdProp, inputs);
870+
}
863871
if (typeof outId === 'object') {
864-
const outIdList = makeAllIds(outId, {});
865-
outIdList.forEach(id => {
866-
const tempOutIdProp = {id, property};
867-
let outIdName = combineIdAndProp(tempOutIdProp);
872+
if (config.validate_callbacks) {
873+
const outIdList = makeAllIds(outId, {});
874+
outIdList.forEach(id => {
875+
const tempOutIdProp = {id, property};
876+
let outIdName = combineIdAndProp(tempOutIdProp);
877+
// if this output is also an input, add `outputTag` to the name
878+
if (alsoInput) {
879+
duplicateOutputs.push(tempOutIdProp);
880+
outIdName += outputTag;
881+
}
882+
addOutputToMulti(id, outIdName);
883+
});
884+
}
885+
addPattern(outputPatterns, outId, property, finalDependency);
886+
} else {
887+
if (config.validate_callbacks) {
888+
let outIdName = combineIdAndProp(outIdProp);
868889
// if this output is also an input, add `outputTag` to the name
869890
if (alsoInput) {
870-
duplicateOutputs.push(tempOutIdProp);
891+
duplicateOutputs.push(outIdProp);
871892
outIdName += outputTag;
872893
}
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;
894+
addOutputToMulti({}, outIdName);
882895
}
883-
addOutputToMulti({}, outIdName);
884896
addMap(outputMap, outId, property, finalDependency);
885897
}
886898
});
@@ -913,7 +925,8 @@ export function computeGraphs(dependencies, dispatchError) {
913925
}
914926
}
915927
});
916-
928+
const end = performance.now();
929+
window.dash_clientside.callbackGraphTime = (end - start).toFixed(2);
917930
return finalGraphs;
918931
}
919932

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.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
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
from dash import Dash, html, dcc, Input, Output, State, ALL, callback
2+
import dash.testing.wait as wait
3+
import time
4+
import pytest
5+
6+
7+
def make_app(num_groups=500, items_per_group=20):
8+
app = Dash(__name__)
9+
10+
NUM_GROUPS = num_groups
11+
ITEMS_PER_GROUP = items_per_group
12+
13+
children = []
14+
for g in range(NUM_GROUPS):
15+
group_children = []
16+
for i in range(ITEMS_PER_GROUP):
17+
group_children.append(
18+
html.Div(
19+
[
20+
dcc.Input(
21+
id={"type": "input", "group": g, "index": i},
22+
value=f"g{g}-i{i}",
23+
),
24+
html.Div(
25+
id={"type": "output", "group": g, "index": i},
26+
),
27+
]
28+
)
29+
)
30+
children.append(
31+
html.Details(
32+
[
33+
html.Summary(f"Group {g}"),
34+
html.Div(group_children),
35+
]
36+
)
37+
)
38+
39+
for g in range(NUM_GROUPS):
40+
41+
@callback(
42+
Output({"type": "output", "group": g, "index": ALL}, "children"),
43+
Input({"type": "input", "group": g, "index": ALL}, "value"),
44+
prevent_initial_call=True,
45+
)
46+
def update(v, _g=g):
47+
return f"Updated: {v}"
48+
49+
for g in range(NUM_GROUPS - 1):
50+
51+
@callback(
52+
Output({"type": "output", "group": g + 1, "index": ALL}, "style"),
53+
Input({"type": "input", "group": g, "index": ALL}, "value"),
54+
prevent_initial_call=True,
55+
)
56+
def cross_update(values, _g=g):
57+
return [{"color": "blue"} for _ in values]
58+
59+
for g in range(0, NUM_GROUPS, 3):
60+
61+
@callback(
62+
Output({"type": "output", "group": g, "index": ALL}, "title"),
63+
Input({"type": "input", "group": g, "index": ALL}, "value"),
64+
State({"type": "output", "group": g, "index": ALL}, "children"),
65+
prevent_initial_call=True,
66+
)
67+
def tooltip_update(values, current, _g=g):
68+
return [f"{v} ({c})" for v, c in zip(values, current or [""] * len(values))]
69+
70+
def layout():
71+
return html.Div(
72+
[
73+
html.H3("Dash 4 Firefox Performance MWE"),
74+
dcc.Input(id="input", value="initial value", type="text"),
75+
html.Div(id="output"),
76+
dcc.Store(id="store", data=time.time()),
77+
html.Div(children),
78+
]
79+
)
80+
81+
app.layout = layout
82+
83+
app.clientside_callback(
84+
"""
85+
function(value, ts) {
86+
if (!ts) return '';
87+
var now = Date.now() / 1000;
88+
return (now - ts).toFixed(2);
89+
}
90+
""",
91+
Output("output", "children"),
92+
Input("input", "value"),
93+
State("store", "data"),
94+
)
95+
96+
return app
97+
98+
99+
check_timing = {}
100+
101+
102+
@pytest.mark.parametrize(
103+
"dev_tools,store",
104+
[
105+
({"dev_tools_validate_callbacks": False}, "disabled"),
106+
({"dev_tools_validate_callbacks": True}, "enabled"),
107+
],
108+
)
109+
def test_compute_graph_timing(dash_duo, dev_tools, store):
110+
app = make_app()
111+
dash_duo.start_server(app, **dev_tools)
112+
times = []
113+
for _ in range(10):
114+
wait.until(
115+
lambda: dash_duo.find_element("#output").text.strip() != "", timeout=4
116+
)
117+
graph_compute_time = float(
118+
dash_duo.driver.execute_script(
119+
"return window.dash_clientside.callbackGraphTime"
120+
)
121+
)
122+
times.append(graph_compute_time)
123+
dash_duo.driver.refresh()
124+
avg_time = sum(times) / len(times) if times else 0
125+
check_timing[store] = avg_time
126+
if store == "enabled":
127+
print(f"Average time with store enabled: {avg_time:.2f} ms")
128+
assert (
129+
check_timing["disabled"] < avg_time
130+
), "Expected faster performance with circular callback check disabled"
131+
if store == "disabled":
132+
print(f"Average time with store disabled: {avg_time:.2f} ms")
133+
assert (
134+
avg_time < 500
135+
), "Expected average time to be under 1/2 seconds with circular callback check disabled"

0 commit comments

Comments
 (0)