arrayview 0.36.0__tar.gz → 0.38.1__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.36.0 → arrayview-0.38.1}/PKG-INFO +1 -1
- {arrayview-0.36.0 → arrayview-0.38.1}/pyproject.toml +1 -2
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_io.py +84 -9
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_launch_plan.py +0 -1
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_launcher.py +33 -30
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_platform.py +51 -2
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_routes_loading.py +2 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_session.py +43 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_vscode_browser.py +6 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_vscode_extension.py +31 -15
- arrayview-0.38.1/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_api.py +132 -2
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_cli.py +1 -1
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_doctor_commands.py +30 -13
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_invocation_contract_matrix.py +0 -4
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_launch_plan.py +0 -1
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_lifecycle_contract.py +14 -0
- arrayview-0.36.0/src/arrayview/_codex_open.py +0 -137
- arrayview-0.36.0/src/arrayview/arrayview-opener.vsix +0 -0
- arrayview-0.36.0/tests/test_codex_open.py +0 -160
- {arrayview-0.36.0 → arrayview-0.38.1}/.gitignore +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/LICENSE +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/README.md +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/__init__.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/__main__.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_analysis.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_app.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_config.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_dicom.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_diff.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_icon.png +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_instance_registry.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_launch_trace.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_lifecycle.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_render.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_routes_drop.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_routes_preferences.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_routes_query.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_routes_rendering.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_routes_websocket.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_server.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_session_spec.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_shell.html +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_torch.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_viewer.html +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/_vscode_signal.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/src/arrayview/todo.md +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/capture_patient_loading.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/capture_v_animation.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/conftest.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/lifecycle_matrix.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_backend_shared.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_browser.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_colorbar_hover_highlight.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_command_reachability.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_config.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_cross_mode_parametrized.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_dicom.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_display_opener_results.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_drop_import.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_instance_registry.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_interactions.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_io.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_large_arrays.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_launch_blackbox.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_launch_diagnostics.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_launch_execution_ownership.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_launch_trace.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_loading_server.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_nifti_series.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_preferences_ui.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_public_invocation_regressions.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_safe_server_stop.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_server_identity_status.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_session_spec.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_spawn_ownership.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_torch.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_value_formatting.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_view_component_integration.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_view_component_unit.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_view_handle_lifecycle.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_view_launch_execution.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/test_vscode_ack_protocol.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/ui_audit.py +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/v_anim_frames/.gitkeep +0 -0
- {arrayview-0.36.0 → arrayview-0.38.1}/tests/visual_smoke.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "arrayview"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.38.1"
|
|
8
8
|
description = "Fast multi-dimensional array viewer"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -43,7 +43,6 @@ dependencies = [
|
|
|
43
43
|
|
|
44
44
|
[project.scripts]
|
|
45
45
|
arrayview = "arrayview._launcher:arrayview"
|
|
46
|
-
arrayview-codex = "arrayview._codex_open:main"
|
|
47
46
|
|
|
48
47
|
[tool.hatch.build.targets.wheel]
|
|
49
48
|
packages = ["src/arrayview"]
|
|
@@ -1397,6 +1397,76 @@ def _load_nifti_series(path, *, load="lazy", stack="auto"):
|
|
|
1397
1397
|
return _series_from_file_matrix(file_matrix, load=load, stack=stack)
|
|
1398
1398
|
|
|
1399
1399
|
|
|
1400
|
+
_MMAP_PAGE_BYTES = 4096
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
def npy_eager_limit() -> int:
|
|
1404
|
+
"""Largest .npy that may be read fully into RAM."""
|
|
1405
|
+
return int(os.environ.get("AV_NPY_EAGER_BYTES", 2 * 1024**3))
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
def first_frame_fault_cost(shape, itemsize, strides, dims, total_bytes):
|
|
1409
|
+
"""Estimate what a memmap must fetch to render one 2D slice.
|
|
1410
|
+
|
|
1411
|
+
Returns ``(runs, bytes)``. Both matter, and on network storage the run
|
|
1412
|
+
count matters more: a frame of 210 single-page runs measured 1.34 s off an
|
|
1413
|
+
SMB share while transferring under a megabyte, because the cost was 210
|
|
1414
|
+
round-trips rather than the bytes.
|
|
1415
|
+
|
|
1416
|
+
Only the layout is consulted, so this touches no data. The displayed axes
|
|
1417
|
+
are ordered by stride; the inner one decides whether the frame arrives as a
|
|
1418
|
+
few long runs or as one page per element.
|
|
1419
|
+
"""
|
|
1420
|
+
dx, dy = dims
|
|
1421
|
+
inner, outer = sorted(
|
|
1422
|
+
((abs(int(strides[d])), int(shape[d])) for d in (dx, dy)),
|
|
1423
|
+
key=lambda pair: pair[0],
|
|
1424
|
+
)
|
|
1425
|
+
inner_stride, inner_len = inner
|
|
1426
|
+
outer_stride, outer_len = outer
|
|
1427
|
+
|
|
1428
|
+
row_span = inner_len * inner_stride
|
|
1429
|
+
if inner_stride == itemsize and outer_stride == row_span:
|
|
1430
|
+
# The displayed pair is itself one contiguous block (e.g. axes (0, 1) of
|
|
1431
|
+
# a Fortran-order array, or the trailing pair of a C-order one).
|
|
1432
|
+
return 1, min(row_span * outer_len, int(total_bytes))
|
|
1433
|
+
if inner_stride >= _MMAP_PAGE_BYTES:
|
|
1434
|
+
# Nothing shares a page: one fetch per element, the pathological case.
|
|
1435
|
+
runs = inner_len * outer_len
|
|
1436
|
+
return runs, min(runs * _MMAP_PAGE_BYTES, int(total_bytes))
|
|
1437
|
+
# Each row is one near-contiguous span; rows are scattered across the file.
|
|
1438
|
+
row_bytes = max(row_span, _MMAP_PAGE_BYTES)
|
|
1439
|
+
return outer_len, min(outer_len * row_bytes, int(total_bytes))
|
|
1440
|
+
|
|
1441
|
+
|
|
1442
|
+
def mmap_first_frame_is_cheap(arr, total_bytes) -> bool:
|
|
1443
|
+
"""Whether serving the viewer's first frame off *arr* beats reading it whole.
|
|
1444
|
+
|
|
1445
|
+
A memmap wins when the axes the viewer starts on are the fastest-varying
|
|
1446
|
+
ones in the file layout, and loses badly when they are the slowest: the same
|
|
1447
|
+
4D array costs a single contiguous block in Fortran order and roughly one
|
|
1448
|
+
page per element in C order. Both regimes are common, so decide per array
|
|
1449
|
+
rather than assuming either.
|
|
1450
|
+
"""
|
|
1451
|
+
if os.environ.get("AV_MMAP_FIRST_FRAME", "1") == "0":
|
|
1452
|
+
return False
|
|
1453
|
+
from ._session import _viewer_start_dims_for_shape
|
|
1454
|
+
|
|
1455
|
+
shape = tuple(int(s) for s in getattr(arr, "shape", ()))
|
|
1456
|
+
strides = tuple(int(s) for s in getattr(arr, "strides", ()))
|
|
1457
|
+
if len(shape) < 2 or len(strides) != len(shape) or not total_bytes:
|
|
1458
|
+
return False
|
|
1459
|
+
dims = _viewer_start_dims_for_shape(shape)
|
|
1460
|
+
if dims is None:
|
|
1461
|
+
return False
|
|
1462
|
+
runs, faulted = first_frame_fault_cost(
|
|
1463
|
+
shape, arr.dtype.itemsize, strides, dims, total_bytes
|
|
1464
|
+
)
|
|
1465
|
+
max_runs = int(os.environ.get("AV_MMAP_FIRST_FRAME_MAX_RUNS", "1024"))
|
|
1466
|
+
fraction = float(os.environ.get("AV_MMAP_FIRST_FRAME_FRACTION", "0.25"))
|
|
1467
|
+
return runs <= max_runs and faulted <= fraction * total_bytes
|
|
1468
|
+
|
|
1469
|
+
|
|
1400
1470
|
def load_data_with_meta(filepath, key=None, *, load="lazy", stack="auto", select=None):
|
|
1401
1471
|
"""Like load_data but also returns spatial metadata for NIfTI files.
|
|
1402
1472
|
|
|
@@ -1430,15 +1500,20 @@ def load_data(filepath, key=None):
|
|
|
1430
1500
|
series, _meta = _load_file_series(filepath)
|
|
1431
1501
|
return series
|
|
1432
1502
|
if filepath.endswith(".npy"):
|
|
1433
|
-
#
|
|
1434
|
-
#
|
|
1435
|
-
#
|
|
1436
|
-
#
|
|
1437
|
-
#
|
|
1438
|
-
|
|
1439
|
-
if
|
|
1440
|
-
return np.load(filepath)
|
|
1441
|
-
|
|
1503
|
+
# Files that do not fit in RAM must be mapped whatever the access cost.
|
|
1504
|
+
# Below that limit the array ends up in RAM either way; the only question
|
|
1505
|
+
# is whether the first frame is served from the map while the sequential
|
|
1506
|
+
# read runs behind it (see mmap_first_frame_is_cheap) or the viewer shows
|
|
1507
|
+
# a spinner until the read completes.
|
|
1508
|
+
size = os.path.getsize(filepath)
|
|
1509
|
+
if size >= npy_eager_limit():
|
|
1510
|
+
return np.load(filepath, mmap_mode="r")
|
|
1511
|
+
mapped = np.load(filepath, mmap_mode="r")
|
|
1512
|
+
if mmap_first_frame_is_cheap(mapped, size):
|
|
1513
|
+
# Session creation upgrades this to an in-RAM array in the background.
|
|
1514
|
+
return mapped
|
|
1515
|
+
del mapped
|
|
1516
|
+
return np.load(filepath)
|
|
1442
1517
|
elif filepath.endswith(".npz"):
|
|
1443
1518
|
npz = np.load(filepath)
|
|
1444
1519
|
try:
|
|
@@ -1672,7 +1672,7 @@ def _register_cli_session_with_existing_server_impl(
|
|
|
1672
1672
|
error += (
|
|
1673
1673
|
"\nThe server on this port is an older ArrayView process that "
|
|
1674
1674
|
"does not support automatic --overlay-dir case inference yet. "
|
|
1675
|
-
|
|
1675
|
+
"Restart it with: arrayview stop"
|
|
1676
1676
|
)
|
|
1677
1677
|
raise RuntimeError(f"Error from server: {error}")
|
|
1678
1678
|
_loaded_sids.append(str(result["sid"]))
|
|
@@ -1860,7 +1860,7 @@ def _handle_cli_existing_server(
|
|
|
1860
1860
|
print(
|
|
1861
1861
|
f"Error: existing ArrayView server on port {port} does not support "
|
|
1862
1862
|
"directory stacking. Restart it with "
|
|
1863
|
-
|
|
1863
|
+
"`arrayview stop` or choose a free port with "
|
|
1864
1864
|
f"`--port`. ({e})"
|
|
1865
1865
|
)
|
|
1866
1866
|
sys.exit(1)
|
|
@@ -4307,6 +4307,7 @@ def _serve_daemon(
|
|
|
4307
4307
|
f"[ArrayView] Warning: failed to load vector field {vfield_filepath}: {e}",
|
|
4308
4308
|
flush=True,
|
|
4309
4309
|
)
|
|
4310
|
+
_session_mod.upgrade_memmap_in_background(session)
|
|
4310
4311
|
_session_mod.SESSIONS[session.sid] = session
|
|
4311
4312
|
if dir_overlay_items is not None:
|
|
4312
4313
|
for item, ov_sid in zip(dir_overlay_items, overlay_sids or []):
|
|
@@ -4815,9 +4816,29 @@ def _instance_public_dict(record) -> dict[str, object]:
|
|
|
4815
4816
|
return value
|
|
4816
4817
|
|
|
4817
4818
|
|
|
4819
|
+
def _run_stop_command() -> None:
|
|
4820
|
+
"""Stop every running ArrayView instance and clean up stale state."""
|
|
4821
|
+
from arrayview._instance_registry import InstanceRegistry # noqa: PLC0415
|
|
4822
|
+
|
|
4823
|
+
records = InstanceRegistry().discover(clean_stale=True)
|
|
4824
|
+
if not records:
|
|
4825
|
+
print("[ArrayView] No running ArrayView instances.")
|
|
4826
|
+
for record in records:
|
|
4827
|
+
message, _pid = _stop_verified_server(record.port)
|
|
4828
|
+
print(f"[ArrayView] {record.instance_id}: {message}")
|
|
4829
|
+
|
|
4830
|
+
from arrayview._vscode_signal import ( # noqa: PLC0415
|
|
4831
|
+
_cleanup_zombie_registrations,
|
|
4832
|
+
)
|
|
4833
|
+
|
|
4834
|
+
cleaned = _cleanup_zombie_registrations(verbose=True)
|
|
4835
|
+
if cleaned:
|
|
4836
|
+
print(f"[ArrayView] Cleaned {cleaned} zombie window registration(s)")
|
|
4837
|
+
|
|
4838
|
+
|
|
4818
4839
|
def _handle_management_command(argv: list[str]) -> bool:
|
|
4819
4840
|
"""Handle dependency-light management subcommands before the file CLI."""
|
|
4820
|
-
if not argv or argv[0] not in {"doctor", "instances", "stop"}:
|
|
4841
|
+
if not argv or argv[0] not in {"doctor", "instances", "stop", "kill"}:
|
|
4821
4842
|
return False
|
|
4822
4843
|
command = argv[0]
|
|
4823
4844
|
parser = argparse.ArgumentParser(prog=f"arrayview {command}")
|
|
@@ -4843,22 +4864,10 @@ def _handle_management_command(argv: list[str]) -> bool:
|
|
|
4843
4864
|
)
|
|
4844
4865
|
return True
|
|
4845
4866
|
|
|
4846
|
-
if command
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
args = parser.parse_args(argv[1:])
|
|
4851
|
-
records = InstanceRegistry().discover(clean_stale=True)
|
|
4852
|
-
selected = (
|
|
4853
|
-
records
|
|
4854
|
-
if args.all
|
|
4855
|
-
else [record for record in records if record.instance_id == args.instance_id]
|
|
4856
|
-
)
|
|
4857
|
-
if not selected:
|
|
4858
|
-
parser.error("no matching running ArrayView instance")
|
|
4859
|
-
for record in selected:
|
|
4860
|
-
message, _pid = _stop_verified_server(record.port)
|
|
4861
|
-
print(f"[ArrayView] {record.instance_id}: {message}")
|
|
4867
|
+
if command in {"stop", "kill"}:
|
|
4868
|
+
parser.description = "Stop every running ArrayView server."
|
|
4869
|
+
parser.parse_args(argv[1:])
|
|
4870
|
+
_run_stop_command()
|
|
4862
4871
|
return True
|
|
4863
4872
|
|
|
4864
4873
|
parser.add_argument("--json", action="store_true")
|
|
@@ -5002,7 +5011,7 @@ def arrayview():
|
|
|
5002
5011
|
parser.add_argument(
|
|
5003
5012
|
"--kill",
|
|
5004
5013
|
action="store_true",
|
|
5005
|
-
help="
|
|
5014
|
+
help="Deprecated: use `arrayview stop`",
|
|
5006
5015
|
)
|
|
5007
5016
|
parser.add_argument(
|
|
5008
5017
|
"--overlay",
|
|
@@ -5350,15 +5359,9 @@ def arrayview():
|
|
|
5350
5359
|
sys.exit(1)
|
|
5351
5360
|
return
|
|
5352
5361
|
|
|
5353
|
-
# -- --kill:
|
|
5362
|
+
# -- --kill: deprecated alias for `arrayview stop` --
|
|
5354
5363
|
if args.kill:
|
|
5355
|
-
|
|
5356
|
-
print(f"[ArrayView] {message}")
|
|
5357
|
-
|
|
5358
|
-
from arrayview._vscode_signal import _cleanup_zombie_registrations
|
|
5359
|
-
cleaned = _cleanup_zombie_registrations(verbose=True)
|
|
5360
|
-
if cleaned:
|
|
5361
|
-
print(f"[ArrayView] Cleaned {cleaned} zombie window registration(s)")
|
|
5364
|
+
_run_stop_command()
|
|
5362
5365
|
return
|
|
5363
5366
|
|
|
5364
5367
|
# -- --serve: start a persistent empty server and exit --
|
|
@@ -5403,7 +5406,7 @@ def arrayview():
|
|
|
5403
5406
|
if serve_context.evidence.is_vscode_remote:
|
|
5404
5407
|
print(
|
|
5405
5408
|
f"[ArrayView] Port {args.port} was claimed while starting.\n"
|
|
5406
|
-
|
|
5409
|
+
" Run 'arrayview stop' to free it, "
|
|
5407
5410
|
"or use --port to specify a different port.",
|
|
5408
5411
|
flush=True,
|
|
5409
5412
|
)
|
|
@@ -5675,7 +5678,7 @@ def arrayview():
|
|
|
5675
5678
|
if launch_plan.failure is LaunchFailure.REMOTE_PORT_CONFLICT:
|
|
5676
5679
|
print(
|
|
5677
5680
|
f"[ArrayView] Port {args.port} is in use by another process.\n"
|
|
5678
|
-
|
|
5681
|
+
" Run 'arrayview stop' to free it, "
|
|
5679
5682
|
f"or use --port to specify a different port.",
|
|
5680
5683
|
flush=True,
|
|
5681
5684
|
)
|
|
@@ -6,6 +6,7 @@ import importlib.util
|
|
|
6
6
|
import hashlib
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
import subprocess
|
|
10
11
|
import sys
|
|
11
12
|
|
|
@@ -450,6 +451,42 @@ def _process_is_alive(pid: object) -> bool:
|
|
|
450
451
|
return False
|
|
451
452
|
|
|
452
453
|
|
|
454
|
+
def _superseding_window_registration(
|
|
455
|
+
claimed_ids: list[str],
|
|
456
|
+
signal_dir: str,
|
|
457
|
+
read: Callable[[str], tuple[str, dict] | None],
|
|
458
|
+
) -> tuple[str, dict] | None:
|
|
459
|
+
"""Return the live window that replaced one of ``claimed_ids``, if unique.
|
|
460
|
+
|
|
461
|
+
Each registration lists the window ids it took over from, so a terminal left
|
|
462
|
+
holding a pre-reload id resolves to its own window rather than to a sibling
|
|
463
|
+
that merely shares a server root. Two live windows claiming one id would be a
|
|
464
|
+
contradiction, so an ambiguous answer is refused instead of guessed.
|
|
465
|
+
"""
|
|
466
|
+
if not claimed_ids:
|
|
467
|
+
return None
|
|
468
|
+
wanted = set(claimed_ids)
|
|
469
|
+
try:
|
|
470
|
+
filenames = os.listdir(signal_dir)
|
|
471
|
+
except OSError:
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
matches: list[tuple[str, dict]] = []
|
|
475
|
+
for filename in filenames:
|
|
476
|
+
if not filename.startswith("window-") or not filename.endswith(".json"):
|
|
477
|
+
continue
|
|
478
|
+
candidate = read(filename[len("window-") : -len(".json")])
|
|
479
|
+
if candidate is None:
|
|
480
|
+
continue
|
|
481
|
+
superseded = candidate[1].get("supersedes")
|
|
482
|
+
if not isinstance(superseded, list):
|
|
483
|
+
continue
|
|
484
|
+
if wanted.intersection(item for item in superseded if isinstance(item, str)):
|
|
485
|
+
matches.append(candidate)
|
|
486
|
+
|
|
487
|
+
return matches[0] if len(matches) == 1 else None
|
|
488
|
+
|
|
489
|
+
|
|
453
490
|
def _exact_vscode_window_registration(
|
|
454
491
|
ipc: str | None = None,
|
|
455
492
|
) -> tuple[str, dict] | None:
|
|
@@ -461,6 +498,11 @@ def _exact_vscode_window_registration(
|
|
|
461
498
|
remote registration is unsafe. When direct window evidence is unavailable,
|
|
462
499
|
match the live extension-host executable to the exact server root named by
|
|
463
500
|
the terminal's NLS configuration, and only accept a unique match.
|
|
501
|
+
|
|
502
|
+
Reloading a window rotates its IPC socket and re-injects ``ARRAYVIEW_WINDOW_ID``
|
|
503
|
+
into new terminals only, so a terminal that predates the reload keeps naming a
|
|
504
|
+
window id that no longer registers. The extension records the ids it replaced,
|
|
505
|
+
which turns that stale name back into an exact match on the window that owns it.
|
|
464
506
|
"""
|
|
465
507
|
signal_dir = os.path.expanduser("~/.arrayview")
|
|
466
508
|
|
|
@@ -479,16 +521,23 @@ def _exact_vscode_window_registration(
|
|
|
479
521
|
return None
|
|
480
522
|
return window_id, registration
|
|
481
523
|
|
|
524
|
+
claimed_ids: list[str] = []
|
|
482
525
|
registration = None
|
|
483
526
|
if ipc:
|
|
484
527
|
window_id = hashlib.sha256(ipc.encode()).hexdigest()[:16]
|
|
485
528
|
if reg := _read(window_id):
|
|
486
529
|
return reg
|
|
530
|
+
claimed_ids.append(window_id)
|
|
487
531
|
registration = reg
|
|
488
532
|
if registration is None:
|
|
489
533
|
window_id = os.environ.get("ARRAYVIEW_WINDOW_ID")
|
|
490
|
-
if window_id
|
|
491
|
-
|
|
534
|
+
if window_id:
|
|
535
|
+
if reg := _read(window_id):
|
|
536
|
+
return reg
|
|
537
|
+
claimed_ids.append(window_id)
|
|
538
|
+
|
|
539
|
+
if reg := _superseding_window_registration(claimed_ids, signal_dir, _read):
|
|
540
|
+
return reg
|
|
492
541
|
|
|
493
542
|
exact_cli = _current_vscode_remote_cli()
|
|
494
543
|
if exact_cli is None:
|
|
@@ -24,6 +24,7 @@ from arrayview._session import (
|
|
|
24
24
|
SESSIONS,
|
|
25
25
|
Session,
|
|
26
26
|
file_signature,
|
|
27
|
+
upgrade_memmap_in_background,
|
|
27
28
|
wait_for_session_ready,
|
|
28
29
|
)
|
|
29
30
|
|
|
@@ -293,6 +294,7 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
293
294
|
session.related_release_sids = [
|
|
294
295
|
str(value) for value in body.get("related_sids", [])
|
|
295
296
|
]
|
|
297
|
+
upgrade_memmap_in_background(session)
|
|
296
298
|
commit_pending_session(sid, session)
|
|
297
299
|
except Exception as exc:
|
|
298
300
|
# Keep the server alive, and record why this sid will never
|
|
@@ -458,6 +458,49 @@ def _startup_dims_for_data(data, shape=None) -> tuple[int, int] | None:
|
|
|
458
458
|
return _viewer_start_dims_for_shape(target_shape)
|
|
459
459
|
|
|
460
460
|
|
|
461
|
+
def upgrade_memmap_in_background(session) -> bool:
|
|
462
|
+
"""Read a memmap-backed session into RAM behind the viewer.
|
|
463
|
+
|
|
464
|
+
``load_data`` hands back a memmap only when the first frame is cheap to
|
|
465
|
+
fault in, which buys an immediate picture but leaves later orientations
|
|
466
|
+
paying scattered reads. The sequential read still has to happen, so run it
|
|
467
|
+
off the hot path and swap the array in when it lands.
|
|
468
|
+
|
|
469
|
+
Rebinding ``session.data`` is atomic under the GIL and the values are
|
|
470
|
+
identical, so a render already holding the memmap stays correct and the
|
|
471
|
+
slice caches stay valid.
|
|
472
|
+
"""
|
|
473
|
+
data = getattr(session, "data", None)
|
|
474
|
+
filepath = getattr(session, "filepath", None)
|
|
475
|
+
if not isinstance(data, np.memmap) or not filepath:
|
|
476
|
+
return False
|
|
477
|
+
if not str(filepath).endswith(".npy"):
|
|
478
|
+
return False
|
|
479
|
+
try:
|
|
480
|
+
from ._io import npy_eager_limit
|
|
481
|
+
|
|
482
|
+
if os.path.getsize(filepath) >= npy_eager_limit():
|
|
483
|
+
return False # does not fit in RAM — the map is the only option
|
|
484
|
+
except OSError:
|
|
485
|
+
return False
|
|
486
|
+
|
|
487
|
+
expected = data
|
|
488
|
+
version = getattr(session, "data_version", 0)
|
|
489
|
+
|
|
490
|
+
def _upgrade():
|
|
491
|
+
try:
|
|
492
|
+
loaded = np.load(filepath)
|
|
493
|
+
except Exception:
|
|
494
|
+
return # keep serving from the map
|
|
495
|
+
# A /reload or a released session must not be clobbered by a read that
|
|
496
|
+
# started against the previous array.
|
|
497
|
+
if session.data is expected and getattr(session, "data_version", 0) == version:
|
|
498
|
+
session.data = loaded
|
|
499
|
+
|
|
500
|
+
threading.Thread(target=_upgrade, daemon=True).start()
|
|
501
|
+
return True
|
|
502
|
+
|
|
503
|
+
|
|
461
504
|
SESSIONS = {}
|
|
462
505
|
|
|
463
506
|
COLORMAPS = [
|
|
@@ -275,6 +275,12 @@ def _open_browser(
|
|
|
275
275
|
"ArrayView could not safely install its VS Code opener in "
|
|
276
276
|
"this exact remote window; no viewer request was sent."
|
|
277
277
|
)
|
|
278
|
+
elif _extension_state._VSCODE_EXT_NO_LIVE_WINDOW:
|
|
279
|
+
detail = (
|
|
280
|
+
"This terminal was opened before its VS Code window reloaded, "
|
|
281
|
+
"so it still points at a window that no longer exists. Open a "
|
|
282
|
+
"new terminal in the target window and retry."
|
|
283
|
+
)
|
|
278
284
|
elif _extension_state._VSCODE_EXT_RELOAD_REQUIRED:
|
|
279
285
|
detail = (
|
|
280
286
|
"ArrayView's VS Code opener was installed; reload this exact "
|
|
@@ -54,7 +54,8 @@ _VSCODE_EXT_INSTALLED = False # cached so we only check once per process
|
|
|
54
54
|
_VSCODE_EXT_FRESH_INSTALL = False # True if we just installed it this session
|
|
55
55
|
_VSCODE_EXT_RELOAD_REQUIRED = False # installed files are newer than the live host
|
|
56
56
|
_VSCODE_EXT_INSTALL_FAILED = False # automatic install could not complete safely
|
|
57
|
-
|
|
57
|
+
_VSCODE_EXT_NO_LIVE_WINDOW = False # no live host claims this terminal's window id
|
|
58
|
+
_VSCODE_EXT_VERSION = "0.14.89" # current bundled extension version
|
|
58
59
|
_VSCODE_CONFIGURED_PORTS: set[int] = set()
|
|
59
60
|
|
|
60
61
|
def _bundled_vscode_vsix_version(vsix_path: str) -> str | None:
|
|
@@ -471,9 +472,11 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
471
472
|
"""
|
|
472
473
|
global _VSCODE_EXT_INSTALLED, _VSCODE_EXT_FRESH_INSTALL
|
|
473
474
|
global _VSCODE_EXT_RELOAD_REQUIRED, _VSCODE_EXT_INSTALL_FAILED
|
|
475
|
+
global _VSCODE_EXT_NO_LIVE_WINDOW
|
|
474
476
|
_VSCODE_EXT_FRESH_INSTALL = False
|
|
475
477
|
_VSCODE_EXT_RELOAD_REQUIRED = False
|
|
476
478
|
_VSCODE_EXT_INSTALL_FAILED = False
|
|
479
|
+
_VSCODE_EXT_NO_LIVE_WINDOW = False
|
|
477
480
|
|
|
478
481
|
vsix_path = str(_pkg_files("arrayview").joinpath("arrayview-opener.vsix"))
|
|
479
482
|
if not os.path.isfile(vsix_path):
|
|
@@ -496,6 +499,31 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
496
499
|
and active_registration.get("extensionVersion") == active_version
|
|
497
500
|
else None
|
|
498
501
|
)
|
|
502
|
+
def _reject_unmatched_host(installed_version: str) -> bool:
|
|
503
|
+
"""Record why the live host was rejected, so callers can advise correctly.
|
|
504
|
+
|
|
505
|
+
A version disagreement is fixed by reloading; an unidentifiable window is
|
|
506
|
+
not. Conflating them tells the user to reload when the reload is what
|
|
507
|
+
stranded their terminal in the first place.
|
|
508
|
+
"""
|
|
509
|
+
global _VSCODE_EXT_RELOAD_REQUIRED, _VSCODE_EXT_NO_LIVE_WINDOW
|
|
510
|
+
if active_version is None:
|
|
511
|
+
_VSCODE_EXT_NO_LIVE_WINDOW = True
|
|
512
|
+
_vprint(
|
|
513
|
+
f"[ArrayView] opener v{installed_version} is installed, but no live "
|
|
514
|
+
"VS Code window claims this terminal; open a new terminal in the "
|
|
515
|
+
"target window",
|
|
516
|
+
flush=True,
|
|
517
|
+
)
|
|
518
|
+
return False
|
|
519
|
+
_VSCODE_EXT_RELOAD_REQUIRED = True
|
|
520
|
+
_vprint(
|
|
521
|
+
f"[ArrayView] opener v{installed_version} is installed, but this VS Code "
|
|
522
|
+
"window is still running an older extension host; reload this window",
|
|
523
|
+
flush=True,
|
|
524
|
+
)
|
|
525
|
+
return False
|
|
526
|
+
|
|
499
527
|
if _extension_on_disk(ext_version, vsix_path, remote=is_remote):
|
|
500
528
|
active_matches = (
|
|
501
529
|
active_version == ext_version
|
|
@@ -503,13 +531,7 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
503
531
|
else active_version in (None, ext_version)
|
|
504
532
|
)
|
|
505
533
|
if not active_matches:
|
|
506
|
-
|
|
507
|
-
_vprint(
|
|
508
|
-
f"[ArrayView] opener v{ext_version} is installed, but this VS Code "
|
|
509
|
-
"window is still running an older extension host; reload this window",
|
|
510
|
-
flush=True,
|
|
511
|
-
)
|
|
512
|
-
return False
|
|
534
|
+
return _reject_unmatched_host(ext_version)
|
|
513
535
|
if not is_remote:
|
|
514
536
|
_remove_old_extension_versions(ext_version, remote=False)
|
|
515
537
|
_VSCODE_EXT_INSTALLED = True
|
|
@@ -527,13 +549,7 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
527
549
|
else active_version in (None, newer_version)
|
|
528
550
|
)
|
|
529
551
|
if not active_matches:
|
|
530
|
-
|
|
531
|
-
_vprint(
|
|
532
|
-
f"[ArrayView] newer opener v{newer_version} is installed, but this "
|
|
533
|
-
"VS Code window is still running an older extension host; reload this window",
|
|
534
|
-
flush=True,
|
|
535
|
-
)
|
|
536
|
-
return False
|
|
552
|
+
return _reject_unmatched_host(newer_version)
|
|
537
553
|
_VSCODE_EXT_INSTALLED = True
|
|
538
554
|
_vprint(
|
|
539
555
|
f"[ArrayView] newer extension v{newer_version} is installed — keeping it",
|
|
Binary file
|