Skip to content

Commit 6031959

Browse files
author
Elias Talcott
committed
fix: exporting a large figure hangs
Choreographer uses remote debugging pipes to execute Javascript functions in Chrome. These have a receive buffer size of 100MB. Previously, if a spec exceeded this size, we would try to send it anyways and the image export would never complete. This makes it so we chunk large specs to keep each send within the buffer size. chrome ref: https://chromium.googlesource.com/chromium/src/+/refs/heads/main/content/browser/devtools/devtools_pipe_handler.cc#44 issue: #419
1 parent 421a9ee commit 6031959

1 file changed

Lines changed: 78 additions & 15 deletions

File tree

src/py/kaleido/_kaleido_tab/_tab.py

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import TYPE_CHECKING
55

66
import logistro
7+
import orjson
78

89
from . import _devtools_utils as _dtools
910
from . import _js_logger
@@ -19,10 +20,18 @@
1920

2021

2122
_TEXT_FORMATS = ("svg", "json") # eps
23+
_CHUNK_SIZE = 10 * 1024 * 1024 # 10 MB
2224

2325
_logger = logistro.getLogger(__name__)
2426

2527

28+
def _orjson_default(obj):
29+
"""Fallback for types orjson can't handle natively (e.g. NumPy string arrays)."""
30+
if hasattr(obj, "tolist"):
31+
return obj.tolist()
32+
raise TypeError(f"Type is not JSON serializable: {type(obj).__name__}")
33+
34+
2635
def _subscribe_new(tab: choreo.Tab, event: str) -> asyncio.Future:
2736
"""Create subscription to tab clearing old ones first: helper function."""
2837
new_future = tab.subscribe_once(event)
@@ -117,22 +126,38 @@ async def _calc_fig(
117126
render_prof,
118127
stepper,
119128
) -> bytes:
120-
# js script
121-
kaleido_js_fn = (
122-
r"function(spec, ...args)"
123-
r"{"
124-
r"return kaleido_scopes.plotly(spec, ...args).then(JSON.stringify);"
125-
r"}"
126-
)
127-
render_prof.profile_log.tick("sending javascript")
128-
result = await _dtools.exec_js_fn(
129-
self.tab,
130-
self._current_js_id,
131-
kaleido_js_fn,
129+
render_prof.profile_log.tick("serializing spec")
130+
spec_str = orjson.dumps(
132131
spec,
133-
topojson,
134-
stepper,
135-
)
132+
default=_orjson_default,
133+
option=orjson.OPT_SERIALIZE_NUMPY,
134+
).decode()
135+
render_prof.profile_log.tick("spec serialized")
136+
137+
render_prof.profile_log.tick("sending javascript")
138+
if len(spec_str) <= _CHUNK_SIZE:
139+
kaleido_js_fn = (
140+
r"function(specStr, ...args)"
141+
r"{"
142+
r"return kaleido_scopes"
143+
r".plotly(JSON.parse(specStr), ...args)"
144+
r".then(JSON.stringify);"
145+
r"}"
146+
)
147+
result = await _dtools.exec_js_fn(
148+
self.tab,
149+
self._current_js_id,
150+
kaleido_js_fn,
151+
spec_str,
152+
topojson,
153+
stepper,
154+
)
155+
else:
156+
result = await self._calc_fig_chunked(
157+
spec_str,
158+
topojson=topojson,
159+
stepper=stepper,
160+
)
136161
_raise_error(result)
137162
render_prof.profile_log.tick("javascript sent")
138163

@@ -154,3 +179,41 @@ async def _calc_fig(
154179
render_prof.data_out_size = len(res)
155180
render_prof.js_log = self.js_logger.log
156181
return res
182+
183+
async def _calc_fig_chunked(
184+
self,
185+
spec_str: str,
186+
*,
187+
topojson: str | None,
188+
stepper,
189+
):
190+
await _dtools.exec_js_fn(
191+
self.tab,
192+
self._current_js_id,
193+
r"function() { window.__kaleido_chunks = []; }",
194+
)
195+
196+
for i in range(0, len(spec_str), _CHUNK_SIZE):
197+
chunk = spec_str[i : i + _CHUNK_SIZE]
198+
await _dtools.exec_js_fn(
199+
self.tab,
200+
self._current_js_id,
201+
r"function(c) { window.__kaleido_chunks.push(c); }",
202+
chunk,
203+
)
204+
205+
kaleido_js_fn = (
206+
r"function(...args)"
207+
r"{"
208+
r"var spec = JSON.parse(window.__kaleido_chunks.join(''));"
209+
r"delete window.__kaleido_chunks;"
210+
r"return kaleido_scopes.plotly(spec, ...args).then(JSON.stringify);"
211+
r"}"
212+
)
213+
return await _dtools.exec_js_fn(
214+
self.tab,
215+
self._current_js_id,
216+
kaleido_js_fn,
217+
topojson,
218+
stepper,
219+
)

0 commit comments

Comments
 (0)