perf: optimize to_dict() serialization — 40% faster for data-heavy figures#5577
perf: optimize to_dict() serialization — 40% faster for data-heavy figures#5577KRRT7 wants to merge 3 commits intoplotly:mainfrom
Conversation
Three changes to the hot path hit by every fig.show(), write_html(), to_json(), and write_image() call: 1. to_typed_array_spec: replace copy_to_readonly_numpy_array (which copies the array, wraps through narwhals, and sets readonly flag) with a lightweight np.asarray — the input is already a deepcopy from to_dict(), so copying again is pure waste. 2. convert_to_base64: replace is_homogeneous_array (which checks numpy, pandas, narwhals, and __array_interface__) with a direct isinstance(value, np.ndarray) check. In the to_dict() context, data is already validated and stored as numpy arrays. 3. is_skipped_key: replace list scan with frozenset lookup (O(1)). Profile results (10 traces × 100K points, 20 calls): to_typed_array_spec: 1811ms → 1097ms (40% faster) copy_to_readonly_numpy_array: 226ms → 0ms (eliminated) narwhals from_native: 68ms → 0ms (eliminated) is_skipped_key: 41ms → ~0ms (eliminated)
|
Hi @KRRT7, thanks for the contribution.
What is the total runtime of |
In convert_to_base64, when iterating list/tuple elements, only recurse into dicts, lists, and tuples. Strings and numbers can never contain numpy arrays, so recursing into them wastes ~500K function calls on figures with large text arrays.
|
Thanks for looking at this, @emilykl! I've added isolated VM benchmarks to the PR description. The headline number: Measured on a dedicated Azure |
|
To give some broader context — we've been profiling plotly.py's hot paths end-to-end and have a few more optimizations in the pipeline (e.g., #5576 for That said, we'd love your input on where to focus next. You and the team have the best sense of which workflows and codepaths matter most to users in practice. If there are specific bottlenecks you've been wanting to address — or areas where users have reported slowness — we'd be happy to target those. Would rather align with your priorities than optimize in a vacuum. |
Overview
Optimizes the
to_dict()→convert_to_base64→to_typed_array_spechot path that runs on everyfig.show(),fig.write_html(),fig.to_json(), andfig.write_image()call.Changes
1.
to_typed_array_spec: eliminate redundant array copyThe function called
copy_to_readonly_numpy_array(v)which:from_native()(unnecessary for numpy arrays).copy()(unnecessary — input is already adeepcopyfromto_dict())Replaced with a lightweight
np.asarray(v)that only converts non-numpy types.2.
convert_to_base64: fast numpy detection + skip non-container recursionReplaced
is_homogeneous_array(value)(which checks numpy, pandas, narwhals, and__array_interface__) with a directisinstance(value, np.ndarray)— in theto_dict()context, data has already been validated and stored as numpy arrays.Also inlined the numpy module lookup to avoid repeated
get_modulecalls during recursion.Added a guard in the list/tuple branch to only recurse into container types (dict, list, tuple). Previously, text arrays like
["point_0", "point_1", ...]caused ~500K useless recursive calls since each string element was visited individually.3.
is_skipped_key: frozenset instead of list scanReplaced
any(skipped_key == key for skipped_key in skipped_keys)withkey in frozenset(...)for O(1) lookup. Called once per dict key during base64 conversion.Benchmarks
Measured on an isolated Azure VM to eliminate noise:
Standard_D2s_v5(2 dedicated vCPUs, 8 GB RAM)main)to_typed_array_spec(100K f64)convert_to_base64(5×100K)to_dict(full end-to-end)Reproduce benchmark
Testing
requestsmodule)ruff formatpasses