arrayview 0.42.1__tar.gz → 0.42.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arrayview-0.42.1 → arrayview-0.42.2}/PKG-INFO +1 -1
- {arrayview-0.42.1 → arrayview-0.42.2}/pyproject.toml +1 -1
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_io.py +39 -3
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_routes_loading.py +12 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_routes_query.py +18 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_routes_websocket.py +57 -3
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_session.py +4 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_viewer.html +30 -2
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_vscode_extension.py +1 -1
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_vscode_signal.py +52 -13
- arrayview-0.42.2/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_api.py +4 -2
- arrayview-0.42.1/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/.gitignore +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/LICENSE +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/README.md +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/__init__.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/__main__.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_analysis.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_app.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_config.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_diagnostics.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_dicom.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_diff.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_icon.png +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_instance_registry.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_launch_plan.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_launch_trace.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_launcher.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_lifecycle.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_platform.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_render.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_routes_drop.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_routes_preferences.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_routes_rendering.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_server.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_session_spec.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_shell.html +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_source_safety.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_torch.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_tutorial.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_vscode_browser.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/_watch.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/src/arrayview/todo.md +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/capture_patient_loading.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/capture_v_animation.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/conftest.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/lifecycle_matrix.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_1d_arrays.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_backend_shared.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_browser.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_cli.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_colorbar_hover_highlight.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_config.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_dicom.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_display_opener_results.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_doctor_commands.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_drop_import.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_instance_registry.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_invocation_contract_matrix.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_io.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_large_arrays.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_launch_blackbox.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_launch_diagnostics.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_launch_execution_ownership.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_launch_plan.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_launch_trace.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_lifecycle_contract.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_loading_server.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_nifti_series.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_preferences_ui.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_public_invocation_regressions.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_render_failure_reporting.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_render_progress.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_safe_server_stop.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_server_identity_status.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_session_spec.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_source_safety.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_spawn_ownership.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_torch.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_tutorial.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_tutorial_browser.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_value_formatting.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_view_handle_lifecycle.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_view_launch_execution.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_vscode_ack_protocol.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/test_window_level_preview.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/ui_audit.py +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/v_anim_frames/.gitkeep +0 -0
- {arrayview-0.42.1 → arrayview-0.42.2}/tests/visual_smoke.py +0 -0
|
@@ -1521,7 +1521,41 @@ def mmap_first_frame_is_cheap(arr, total_bytes) -> bool:
|
|
|
1521
1521
|
return runs <= max_runs and faulted <= fraction * total_bytes
|
|
1522
1522
|
|
|
1523
1523
|
|
|
1524
|
-
def
|
|
1524
|
+
def _read_npy_reporting_progress(mapped, progress):
|
|
1525
|
+
"""Copy a memmapped .npy into RAM in chunks, saying how far along it is.
|
|
1526
|
+
|
|
1527
|
+
For a large array this read is the whole wait between a click and the
|
|
1528
|
+
first frame — 9 s for a 1.6 GB file on a cold cache — and ``np.load``
|
|
1529
|
+
reports nothing while it runs. Copying in pieces costs the same reads
|
|
1530
|
+
and makes the wait explainable.
|
|
1531
|
+
|
|
1532
|
+
The flat view follows the array's own memory order, so the copy stays
|
|
1533
|
+
sequential whether the file is C- or Fortran-ordered. A .npy memmap is
|
|
1534
|
+
always contiguous in one of the two, so both reshapes are views.
|
|
1535
|
+
"""
|
|
1536
|
+
order = "F" if mapped.flags.f_contiguous and not mapped.flags.c_contiguous else "C"
|
|
1537
|
+
out = np.empty(mapped.shape, dtype=mapped.dtype, order=order)
|
|
1538
|
+
src = mapped.reshape(-1, order="A")
|
|
1539
|
+
dst = out.reshape(-1, order="A")
|
|
1540
|
+
total = int(src.shape[0])
|
|
1541
|
+
itemsize = int(mapped.dtype.itemsize)
|
|
1542
|
+
# ~200 updates: smooth enough to read as motion, coarse enough that the
|
|
1543
|
+
# per-chunk overhead stays lost in the read itself.
|
|
1544
|
+
step = max(1, total // 200)
|
|
1545
|
+
done = 0
|
|
1546
|
+
while done < total:
|
|
1547
|
+
end = min(done + step, total)
|
|
1548
|
+
dst[done:end] = src[done:end]
|
|
1549
|
+
done = end
|
|
1550
|
+
try:
|
|
1551
|
+
progress(done * itemsize, total * itemsize)
|
|
1552
|
+
except Exception:
|
|
1553
|
+
pass
|
|
1554
|
+
return out
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
def load_data_with_meta(filepath, key=None, *, load="lazy", stack="auto", select=None,
|
|
1558
|
+
progress=None):
|
|
1525
1559
|
"""Like load_data but also returns spatial metadata for NIfTI files.
|
|
1526
1560
|
|
|
1527
1561
|
Returns (array, meta_or_None). meta is None for non-NIfTI formats.
|
|
@@ -1547,10 +1581,10 @@ def load_data_with_meta(filepath, key=None, *, load="lazy", stack="auto", select
|
|
|
1547
1581
|
return _load_file_series(filepath, load=load, stack=stack)
|
|
1548
1582
|
if filepath.endswith(".nii") or filepath.endswith(".nii.gz"):
|
|
1549
1583
|
return _load_nifti_with_meta(filepath)
|
|
1550
|
-
return load_data(filepath, key=key), None
|
|
1584
|
+
return load_data(filepath, key=key, progress=progress), None
|
|
1551
1585
|
|
|
1552
1586
|
|
|
1553
|
-
def load_data(filepath, key=None):
|
|
1587
|
+
def load_data(filepath, key=None, *, progress=None):
|
|
1554
1588
|
if filepath.lower().endswith(".dcm"):
|
|
1555
1589
|
from ._dicom import load_dicom_series
|
|
1556
1590
|
|
|
@@ -1577,6 +1611,8 @@ def load_data(filepath, key=None):
|
|
|
1577
1611
|
if mmap_first_frame_is_cheap(mapped, size):
|
|
1578
1612
|
# Session creation upgrades this to an in-RAM array in the background.
|
|
1579
1613
|
return mapped
|
|
1614
|
+
if progress is not None:
|
|
1615
|
+
return _read_npy_reporting_progress(mapped, progress)
|
|
1580
1616
|
del mapped
|
|
1581
1617
|
return np.load(filepath)
|
|
1582
1618
|
elif filepath.endswith(".npz"):
|
|
@@ -320,6 +320,14 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
320
320
|
PENDING_SESSION_EVENTS[sid] = pending_event
|
|
321
321
|
signature_before_load = file_signature(abs_path)
|
|
322
322
|
|
|
323
|
+
def _report_read_progress(done: int, total: int) -> None:
|
|
324
|
+
# Read from the viewer's websocket while it waits; see
|
|
325
|
+
# LOAD_PROGRESS. Kept to a plain assignment so the read loop
|
|
326
|
+
# pays nothing for it.
|
|
327
|
+
import arrayview._session as _sm
|
|
328
|
+
|
|
329
|
+
_sm.LOAD_PROGRESS[sid] = (done, total)
|
|
330
|
+
|
|
323
331
|
def _load_in_background() -> None:
|
|
324
332
|
try:
|
|
325
333
|
from ._io import load_data_with_meta
|
|
@@ -327,6 +335,7 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
327
335
|
data, spatial_meta = load_data_with_meta(
|
|
328
336
|
filepath,
|
|
329
337
|
select=body.get("select"),
|
|
338
|
+
progress=_report_read_progress,
|
|
330
339
|
)
|
|
331
340
|
session = Session(data, filepath=filepath, name=name)
|
|
332
341
|
session.sid = sid
|
|
@@ -369,6 +378,9 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
369
378
|
if staging_dir:
|
|
370
379
|
cleanup_staging_directory(staging_dir)
|
|
371
380
|
finally:
|
|
381
|
+
import arrayview._session as _sm
|
|
382
|
+
|
|
383
|
+
_sm.LOAD_PROGRESS.pop(sid, None)
|
|
372
384
|
PENDING_SESSIONS.discard(sid)
|
|
373
385
|
CANCELLED_PENDING_SESSIONS.discard(sid)
|
|
374
386
|
pending_event.set()
|
|
@@ -217,6 +217,24 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
|
|
|
217
217
|
phases = journal["phases"]
|
|
218
218
|
if phase not in phases:
|
|
219
219
|
phases.append(phase)
|
|
220
|
+
# Timestamps the page's own milestones against the navigation that
|
|
221
|
+
# delivered it, so time-to-first-script can be measured per
|
|
222
|
+
# navigation rather than inferred from opener poll intervals.
|
|
223
|
+
navigation_key = journal.get("navigation_key")
|
|
224
|
+
if navigation_key:
|
|
225
|
+
try:
|
|
226
|
+
from arrayview._launch_trace import emit_route_launch_event
|
|
227
|
+
|
|
228
|
+
emit_route_launch_event(
|
|
229
|
+
"page.phase_recorded",
|
|
230
|
+
navigation_key=navigation_key,
|
|
231
|
+
tab_key=journal.get("tab_key"),
|
|
232
|
+
request_id=request_id,
|
|
233
|
+
navigation_attempt=journal.get("navigation_attempt"),
|
|
234
|
+
phase=phase,
|
|
235
|
+
)
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
220
238
|
if bool(body.get("release_on_disconnect")):
|
|
221
239
|
journal["disconnect_release_grace_seconds"] = 30.0
|
|
222
240
|
return {
|
|
@@ -111,6 +111,42 @@ def _render_progress_reporter(
|
|
|
111
111
|
return report
|
|
112
112
|
|
|
113
113
|
|
|
114
|
+
async def _await_session_reporting_read_progress(ws, sid):
|
|
115
|
+
"""Wait for a background read, telling the viewer how far along it is.
|
|
116
|
+
|
|
117
|
+
Nothing else can report this: the reader is a plain thread and the array
|
|
118
|
+
is not a session until it finishes, so this socket is the only thing
|
|
119
|
+
holding a line to the viewer while the wait happens.
|
|
120
|
+
|
|
121
|
+
Silent for the first stretch, so an ordinary fast open never flashes a
|
|
122
|
+
bar on its way past — the same rule the render progress reporter uses.
|
|
123
|
+
"""
|
|
124
|
+
quiet_for_s = 0.6
|
|
125
|
+
started = time.perf_counter()
|
|
126
|
+
waiter = asyncio.ensure_future(wait_for_session_ready(sid))
|
|
127
|
+
last = None
|
|
128
|
+
while True:
|
|
129
|
+
done, _pending = await asyncio.wait({waiter}, timeout=0.15)
|
|
130
|
+
if waiter in done:
|
|
131
|
+
break
|
|
132
|
+
if time.perf_counter() - started < quiet_for_s:
|
|
133
|
+
continue
|
|
134
|
+
state = _session_mod.LOAD_PROGRESS.get(sid)
|
|
135
|
+
if state and state != last:
|
|
136
|
+
last = state
|
|
137
|
+
read, total = state
|
|
138
|
+
await _send_json_quietly(ws, {
|
|
139
|
+
"type": "render_progress",
|
|
140
|
+
"done": int(read),
|
|
141
|
+
"total": int(total),
|
|
142
|
+
"label": "Reading array",
|
|
143
|
+
})
|
|
144
|
+
try:
|
|
145
|
+
return waiter.result()
|
|
146
|
+
except Exception:
|
|
147
|
+
return _session_mod.SESSIONS.get(sid)
|
|
148
|
+
|
|
149
|
+
|
|
114
150
|
async def _send_json_quietly(ws, payload):
|
|
115
151
|
try:
|
|
116
152
|
await ws.send_json(payload)
|
|
@@ -190,8 +226,28 @@ def register_websocket_routes(app) -> None:
|
|
|
190
226
|
native_request_id = ws.query_params.get("native_request_id")
|
|
191
227
|
launch_request_id = ws.query_params.get("launch_request_id")
|
|
192
228
|
launch_token = ws.query_params.get("launch_token")
|
|
193
|
-
|
|
229
|
+
# Answer the handshake before the array is ready. Holding it closed
|
|
230
|
+
# until the load finished made the viewer decide the backend was dead:
|
|
231
|
+
# it gives up after three attempts and shows "could not connect", which
|
|
232
|
+
# a large array reaches purely by being slow to load. Accepting first
|
|
233
|
+
# stops that clock. The viewer keeps its splash up until the metadata
|
|
234
|
+
# below arrives, so an early accept shows nothing half-rendered.
|
|
235
|
+
await ws.accept()
|
|
236
|
+
|
|
237
|
+
session = _session_mod.SESSIONS.get(sid)
|
|
238
|
+
if session is None:
|
|
239
|
+
session = await _await_session_reporting_read_progress(ws, sid)
|
|
194
240
|
if not session:
|
|
241
|
+
# The socket has already opened, and the viewer treats a drop after
|
|
242
|
+
# a successful open as a recoverable reconnect — it would retry
|
|
243
|
+
# forever. Say the load failed so it shows an error instead.
|
|
244
|
+
try:
|
|
245
|
+
await ws.send_json({
|
|
246
|
+
"type": "render_error",
|
|
247
|
+
"message": "the array could not be loaded",
|
|
248
|
+
})
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
195
251
|
await ws.close()
|
|
196
252
|
return
|
|
197
253
|
|
|
@@ -208,8 +264,6 @@ def register_websocket_routes(app) -> None:
|
|
|
208
264
|
await ws.close(code=1008)
|
|
209
265
|
return
|
|
210
266
|
|
|
211
|
-
await ws.accept()
|
|
212
|
-
|
|
213
267
|
# A new viewer proves that the prior disconnect was a reload, tunnel
|
|
214
268
|
# interruption, or other recoverable transport break. Invalidate and
|
|
215
269
|
# cancel its cleanup before recording this connection.
|
|
@@ -51,6 +51,10 @@ CANCELLED_PENDING_SESSIONS: set[str] = set()
|
|
|
51
51
|
# without this the opener cannot tell "still loading" from "will never load" and
|
|
52
52
|
# waits out its whole deadline on a file that failed in the first second.
|
|
53
53
|
FAILED_PENDING_SESSIONS: dict[str, str] = {}
|
|
54
|
+
# sid -> (bytes read, bytes total) for a background read still in progress.
|
|
55
|
+
# The reader is a plain thread with no socket of its own, so it leaves its
|
|
56
|
+
# progress here and the viewer's websocket picks it up while it waits.
|
|
57
|
+
LOAD_PROGRESS: dict[str, tuple[int, int]] = {}
|
|
54
58
|
|
|
55
59
|
|
|
56
60
|
def file_signature(filepath: str):
|
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
4
|
<title>ArrayView</title>
|
|
5
|
+
<script>
|
|
6
|
+
// Name the tab at parse time. Without this the tab shows the host
|
|
7
|
+
// ("localhost:8000") until metadata arrives over the WebSocket, and
|
|
8
|
+
// that wait is the array load — seconds for a large file. The name
|
|
9
|
+
// rides in on ?av_name= because the hash already carries #av-<id>.
|
|
10
|
+
// Formatting must match _setWindowIdentity(), or the title changes a
|
|
11
|
+
// second time when metadata lands.
|
|
12
|
+
(function () {
|
|
13
|
+
try {
|
|
14
|
+
var name = new URLSearchParams(location.search).get('av_name');
|
|
15
|
+
if (!name) return;
|
|
16
|
+
var clean = String(name).trim();
|
|
17
|
+
document.title = clean && clean !== 'welcome'
|
|
18
|
+
? 'ArrayView: ' + clean
|
|
19
|
+
: 'ArrayView';
|
|
20
|
+
} catch (_) {}
|
|
21
|
+
})();
|
|
22
|
+
</script>
|
|
5
23
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='2.5' fill='%230c0c0c'/%3E%3Crect x='2' y='2' width='3' height='3' fill='%233a0ca3'/%3E%3Crect x='6.5' y='2' width='3' height='3' fill='%23560bad'/%3E%3Crect x='11' y='2' width='3' height='3' fill='%23c77dff'/%3E%3Crect x='2' y='6.5' width='3' height='3' fill='%234361ee'/%3E%3Crect x='6.5' y='6.5' width='3' height='3' fill='%234cc9f0'/%3E%3Crect x='11' y='6.5' width='3' height='3' fill='%23f5c842'/%3E%3Crect x='2' y='11' width='3' height='3' fill='%234895ef'/%3E%3Crect x='6.5' y='11' width='3' height='3' fill='%2380ed99'/%3E%3Crect x='11' y='11' width='3' height='3' fill='%23f8961e'/%3E%3C/svg%3E">
|
|
6
24
|
<style>
|
|
7
25
|
/* ── CSS: Theme Variables and Base Layout ────────────────────── */
|
|
@@ -5552,8 +5570,11 @@
|
|
|
5552
5570
|
// never need to appear in the browser toolbar.
|
|
5553
5571
|
const _arrayViewLaunchSearch = __ARRAYVIEW_QUERY__ || window.location.search;
|
|
5554
5572
|
const urlParams = new URLSearchParams(_arrayViewLaunchSearch);
|
|
5555
|
-
const
|
|
5573
|
+
const _isIntegratedBrowserViewer = (
|
|
5556
5574
|
urlParams.get('_av_integrated_browser') === '1'
|
|
5575
|
+
);
|
|
5576
|
+
const _integratedBrowserDisplayHash = (
|
|
5577
|
+
_isIntegratedBrowserViewer
|
|
5557
5578
|
&& window.location.hash.startsWith('#av-')
|
|
5558
5579
|
) ? window.location.hash : '';
|
|
5559
5580
|
// isInlineEmbed: true when explicitly launched as an inline Jupyter/notebook embed
|
|
@@ -15485,7 +15506,14 @@
|
|
|
15485
15506
|
|
|
15486
15507
|
// Shell-lite: when viewer is top-level (not in shell iframe), listen for
|
|
15487
15508
|
// new-tab signals so CLI tab injection still works.
|
|
15488
|
-
|
|
15509
|
+
//
|
|
15510
|
+
// Skipped inside a VS Code browser tab. Every such tab holds its
|
|
15511
|
+
// sockets open through one shared VS Code channel that stops
|
|
15512
|
+
// carrying new page requests at ~32 of them, so a second socket per
|
|
15513
|
+
// viewer halves how many arrays can be open before opening breaks.
|
|
15514
|
+
// Nothing is lost: each launch opens its own VS Code tab, so the
|
|
15515
|
+
// window.open path this socket exists to serve never runs there.
|
|
15516
|
+
if (window === window.top && !_isIntegratedBrowserViewer) {
|
|
15489
15517
|
try {
|
|
15490
15518
|
const _shellProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
15491
15519
|
const _shellWs = new WebSocket(`${_shellProto}//${location.host}${resolveServerPath('/ws/shell')}`);
|
|
@@ -57,7 +57,7 @@ _VSCODE_EXT_FRESH_INSTALL = False # True if we just installed it this session
|
|
|
57
57
|
_VSCODE_EXT_RELOAD_REQUIRED = False # installed files are newer than the live host
|
|
58
58
|
_VSCODE_EXT_INSTALL_FAILED = False # automatic install could not complete safely
|
|
59
59
|
_VSCODE_EXT_NO_LIVE_WINDOW = False # no live host claims this terminal's window id
|
|
60
|
-
_VSCODE_EXT_VERSION = "0.15.
|
|
60
|
+
_VSCODE_EXT_VERSION = "0.15.23" # current bundled extension version
|
|
61
61
|
_VSCODE_CONFIGURED_PORTS: set[int] = set()
|
|
62
62
|
# Version-skew notices already printed, so a long-lived process (Jupyter, a
|
|
63
63
|
# script opening several arrays) states the mismatch once instead of per call.
|
|
@@ -384,6 +384,46 @@ def _find_arrayview_window_id() -> str | None:
|
|
|
384
384
|
return None
|
|
385
385
|
|
|
386
386
|
|
|
387
|
+
def _session_name_for_url(url: str) -> str | None:
|
|
388
|
+
"""Return the session name behind *url*'s ``?sid=``, or None."""
|
|
389
|
+
try:
|
|
390
|
+
import urllib.parse as _up
|
|
391
|
+
|
|
392
|
+
sid = _up.parse_qs(_up.urlparse(url).query).get("sid", [None])[0]
|
|
393
|
+
if not sid:
|
|
394
|
+
return None
|
|
395
|
+
import arrayview._session as _sm
|
|
396
|
+
|
|
397
|
+
session = _sm.SESSIONS.get(sid)
|
|
398
|
+
return getattr(session, "name", None) if session else None
|
|
399
|
+
except Exception:
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _with_viewer_name(url: str, name: str | None) -> str:
|
|
404
|
+
"""Add ``av_name`` so the viewer can title its tab before metadata lands.
|
|
405
|
+
|
|
406
|
+
The page otherwise shows the bare host until the WebSocket delivers the
|
|
407
|
+
session name, and that WebSocket waits on the array load — seconds for a
|
|
408
|
+
large file. This stays a query parameter rather than being baked into the
|
|
409
|
+
served HTML because the gzip cache in ``_server`` is keyed on the page
|
|
410
|
+
body; a per-session body would evict on every open and re-compress ~2 MB.
|
|
411
|
+
"""
|
|
412
|
+
if not name:
|
|
413
|
+
return url
|
|
414
|
+
try:
|
|
415
|
+
import urllib.parse as _up
|
|
416
|
+
|
|
417
|
+
parts = _up.urlsplit(url)
|
|
418
|
+
query = _up.parse_qsl(parts.query, keep_blank_values=True)
|
|
419
|
+
if any(key == "av_name" for key, _ in query):
|
|
420
|
+
return url
|
|
421
|
+
query.append(("av_name", str(name)))
|
|
422
|
+
return _up.urlunsplit(parts._replace(query=_up.urlencode(query)))
|
|
423
|
+
except Exception:
|
|
424
|
+
return url
|
|
425
|
+
|
|
426
|
+
|
|
387
427
|
def _open_via_signal_file(
|
|
388
428
|
url: str,
|
|
389
429
|
delay: float = 0.0,
|
|
@@ -402,19 +442,13 @@ def _open_via_signal_file(
|
|
|
402
442
|
If omitted it is auto-derived from the session name embedded in the URL's
|
|
403
443
|
``?sid=`` query parameter.
|
|
404
444
|
"""
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
_sess = _sm.SESSIONS.get(_sid)
|
|
414
|
-
if _sess:
|
|
415
|
-
title = f"ArrayView: {_sess.name}"
|
|
416
|
-
except Exception:
|
|
417
|
-
pass
|
|
445
|
+
session_name = _session_name_for_url(url)
|
|
446
|
+
if title is None and session_name:
|
|
447
|
+
title = f"ArrayView: {session_name}"
|
|
448
|
+
# The viewer titles its own tab from this, so it must be the session name
|
|
449
|
+
# the metadata will later carry — not *title*, which some callers pass
|
|
450
|
+
# pre-formatted and others pass as a bare filename.
|
|
451
|
+
url = _with_viewer_name(url, session_name)
|
|
418
452
|
|
|
419
453
|
request_id = uuid.uuid4().hex
|
|
420
454
|
if window_id is None:
|
|
@@ -454,6 +488,11 @@ def _open_via_signal_file(
|
|
|
454
488
|
payload["title"] = title
|
|
455
489
|
if handoff_path := os.environ.get("ARRAYVIEW_HANDOFF_PATH"):
|
|
456
490
|
payload["handoffPath"] = handoff_path
|
|
491
|
+
# Opt-in measurement launch: the opener observes the navigation for a long
|
|
492
|
+
# budget instead of closing and reopening a blank tab after 1.5 s. Only
|
|
493
|
+
# this launch is affected, so ordinary use continues alongside a run.
|
|
494
|
+
if os.environ.get("ARRAYVIEW_MEASURE_NAVIGATION") == "1":
|
|
495
|
+
payload["measureNavigation"] = True
|
|
457
496
|
if floating:
|
|
458
497
|
payload["floating"] = True
|
|
459
498
|
if remote_was_supplied:
|
|
Binary file
|
|
@@ -346,7 +346,8 @@ class TestLoad:
|
|
|
346
346
|
started = threading.Event()
|
|
347
347
|
finish = threading.Event()
|
|
348
348
|
|
|
349
|
-
def slow_load(filepath, key=None, select=None, *, load="lazy",
|
|
349
|
+
def slow_load(filepath, key=None, select=None, *, load="lazy",
|
|
350
|
+
stack="auto", progress=None):
|
|
350
351
|
started.set()
|
|
351
352
|
assert finish.wait(2.0)
|
|
352
353
|
return np.zeros((4, 4), dtype=np.float32), None
|
|
@@ -390,7 +391,8 @@ class TestLoad:
|
|
|
390
391
|
started = threading.Event()
|
|
391
392
|
finish = threading.Event()
|
|
392
393
|
|
|
393
|
-
def slow_load(filepath, key=None, select=None, *, load="lazy",
|
|
394
|
+
def slow_load(filepath, key=None, select=None, *, load="lazy",
|
|
395
|
+
stack="auto", progress=None):
|
|
394
396
|
started.set()
|
|
395
397
|
assert finish.wait(2.0)
|
|
396
398
|
return np.zeros((4, 4), dtype=np.float32), None
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|