Skip to content

Add mrd-viz extension and backend (initial review pass) - #87

Closed
ccapetz wants to merge 99 commits into
mainfrom
carter-mrd-viz
Closed

ccapetz wants to merge 99 commits into
mainfrom
carter-mrd-viz

Conversation

@ccapetz

@ccapetz ccapetz commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the mrd-viz VS Code extension and its Python backend for viewing MRD files, along with supporting CI workflows and devcontainer setup.

This PR is opened primarily as an initial full-repo review pass — a chance to surface issues that only show up when the work is looked at as a whole, and to serve as a large-PR code-review exercise. The remaining marketplace-release changes are intentionally not included yet.

Included

  • mrd-viz extension (webview viewer, backend resolver/runner, editor provider)
  • mrd-viz Python backend (CLI, rendering, tests) + packaging
  • CI workflows (mrd_viz.yml, mrd_viz_release.yml) and devcontainer config
  • Docs: TECHNICAL_DESIGN.md and DEVCONTAINER.md

Intentionally excluded (kept untracked for now)

The following internal/WIP docs are excluded until they are ready for review:

  • BACKEND_INSTALL_MODES.md
  • BACKEND_RESOLUTION_IMPLEMENTATION_PLAN.md
  • EXTENSION_RELEASE_RUNBOOK.md
  • OFFICIAL_EXT_DEV_RUNBOOK.md
  • PACKAGING_AND_INSTALL_RUNBOOK.md

ccapetz and others added 30 commits June 17, 2026 15:05
… backward compatibility for unknown schema versions and what error views will be supported
First official technical design outline for the mrd-viz extension. Small questions regarding error handling and backwards compatibility remain open and will be answered with further real-world testing.
Escape '<' before embedding to avoid injection/unexpected behavior for JSON payload handling

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Removed unused import of Counter from collections.
…ly break for extract_image command, clarified schema_version html_harness definitions, unified backend default values, added instructions for generating mrd test files
Add MRD Viz backend CLI

Test through `OFFICIAL_EXT_DEV_RUNBOOK.md` which has powershell and linux bash commands, guides users through generating test mrd files

Test suite also added and will be expanded upon, will run on any PRs targetting carter-mrd-viz
ccapetz and others added 18 commits August 3, 2026 09:17
viewer.js is the esbuild bundle output built by npm run build:webview and regenerated automatically on vsce package via the vscode:prepublish hook (CI runs npm ci + vsce package), so the committed copy is never consumed at package time and only produces noisy diffs. Add a .gitignore for the extension and untrack the file.
mrd-viz: stop tracking generated webview bundle (media/viewer.js)
mrd-viz CI: drop flaky darwin-x64 (Intel macos-13) build leg
Address PR #82 review (Yulia):

- removeIncompleteVenv now retries rm a few times and, if the directory still can't be deleted (e.g. a Windows file lock), renames it aside so its interpreter path no longer exists and the resolver skips the candidate; best-effort deletes the quarantined dir.

- Invalidate the backend cache on the provisioning-failure path so a torn-down venv can't stay cached.

- Export classifyProvisioningFailure and removeIncompleteVenv; add unit tests for the failure hints (confirming the mrd-viz pip name) and the venv cleanup.
mrd-viz: clean up failed backend venv + clearer setup errors
…ementation plan

Design docs backing the deterministic backend-resolution work: current-state analysis, the two-audience target architecture, levels-of-abstraction diagram, and the phased implementation plan. Base commit for the resolver rewrite (PR1).
Replace the 5-candidate search with a pure planBackendCandidates(): a configured override is the ONLY candidate (no silent fallback), otherwise the bundled binary, with the repo .venv tried first only in the F5 Development host. Adds BackendKind and getConfiguredBackendPath (backendPath, then legacy explicit pythonPath). Drops managed-venv and PATH candidates.
selectInterpreter and the guided setup now write the machine-scoped mrdViz.backendPath (no more workspace scope-clearing needed), so the managed venv becomes a first-class override rather than an implicit resolver candidate. Config invalidation watches backendPath and pythonPath.
Lead with the relevant explanation and primary action: a broken mrdViz.backendPath override vs. a bundled backend that won't run on this platform. Update references to backendPath.
Uses mrdViz.backendPath (applied as a container remote setting) instead of the deprecated pythonPath, so the container interpreter can't leak to the host.
…implemented

Unit-test planBackendCandidates (override no-fallback, dev vs prod order, binary classification, empty) and the tailored override page. Update BACKEND_INSTALL_MODES (target -> implemented) and DEVCONTAINER (machine-scoped backendPath, leak now structurally prevented).
…et/toast/doc

- Override candidate/logs and the backend-missing page now name the setting the value came from (backendPath vs legacy pythonPath) instead of always backendPath.
- Setup snippet installs editable from a checkout (mrd-viz is not on PyPI).
- Interpreter-selection toast is setting-neutral (path may be a binary).
- Reconcile BACKEND_INSTALL_MODES intro (pre-refactor, not current).
- Tests for the legacy-setting labeling.
- centralize magic timeouts/buffers/names in backendConstants.ts

- add runProcess() execFile wrapper; use it in resolver, runner, provisioning

- use PYPI package name mrd-viz and add --disable-pip-version-check --no-input to pip; TODO(publish-pypi)

- drop unused getConfiguredBackendPath export

- refresh stale mrdViz.pythonPath -> mrdViz.backendPath references in docs and postCreate

- update backend-missing test fixture to realistic post-refactor sources
The un-scoped legacy setting could leak across machines (e.g. a Linux path synced into Windows User settings) and, treated as an exclusive override, suppressed the bundled binary and forced the backend-not-found page. Only the machine-scoped mrdViz.backendPath is honored now.

- resolver reads only mrdViz.backendPath; remove legacy inspect() branch and settingKey plumbing

- remove deprecated mrdViz.pythonPath declaration and config-change watch

- simplify backend-missing intro to name mrdViz.backendPath; drop two legacy tests
…etworks

Corporate networks block public PyPI (files.pythonhosted.org SSL handshake failure) and public npm. Add select-pkg-index.sh, a candidate-ladder helper sourced by postCreate and container-setup that probes the Microsoft-internal mirror first and falls back to the public registry, exporting PIP_INDEX_URL/npm_config_registry. Candidate lists live in devcontainer.json remoteEnv (discoverable, external devs still work). Prints the exact export line on total failure; documented in DEVCONTAINER.md.
- resolveBackend now returns an explicit 'no backend available' failure when there are no candidates (no override + no bundled binary), instead of an empty attempt list the webview can't explain (yuliadub).
- Extract the hardcoded 'mrd-viz' binary name to BACKEND_BINARY_NAME so a rename touches one place (yuliadub).
- setUpBackend confirms a Reinstall when a managed backend already exists, rather than silently rebuilding it (yuliadub).
mrd-viz: deterministic two-tier backend resolution

@FeoluK FeoluK left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: Add mrd-viz extension and backend

Scope: new VS Code custom editor, webview viewer, Python analysis backend, tests, packaging, and CI/release workflows.

Severity Count Status
CRITICAL 0 pass
HIGH 3 blocking
MEDIUM 5 needs follow-up
LOW 0 pass

Verdict: REQUEST CHANGES — PR validation is not running for the target branch, release tooling is mutable, and initial inspection can fully scan large acquisition-only files. (Filed as a comment review rather than a blocking one; inline findings below.)

Highlights

  • [HIGH] Both mrd-viz workflows filter pull_request.branches to carter-mrd-viz, but that filter matches the base branch — this PR targets main, so neither workflow runs here.
  • [HIGH] The release job runs unversioned npx --yes @vscode/vsce and uses the mutable softprops/action-gh-release@v2 tag with contents: write.
  • [HIGH] Initial-open termination is driven only by the image-thumbnail limit, so acquisition-only files are read through EOF.
  • [MEDIUM] Classification retains per-image metadata; response guards only check ok; superseded image requests are not cancelled; the image cache is entry-bounded but not byte-bounded; sparse --slice axes can allocate enormous tuples.

Architecture notes

Layering is clean: the host owns the custom editor, backend resolution, process execution and panel-lifetime cancellation; the webview owns selection, mosaic presentation, viewport state and the image cache; the backend streams and classifies MRD items. Trust boundaries are mostly well handled — file: URI and extension checks, machine-scoped executable override, execFile argv arrays, timeouts, max buffer, windowsHide, CSP/nonces and textContent rendering. The two gaps are incomplete schema validation of successful payloads, and correlation-without-cancellation (stale results are dropped but their processes keep running).

This design uses short-lived subprocesses that reopen and rescan the MRD for each image request. A persistent backend session would trade lifecycle complexity for lower repeated I/O and natural request cancellation — worth considering if large raw files become a common case.

Recommendations (shared standards)

  • Define one versioned message-contract schema with complete runtime guards for host, webview and backend payloads.
  • Add malicious-workspace coverage for executable settings and Workspace Trust behavior.
  • Add adversarial and very large MRD fixtures with explicit time, item and memory budgets.
  • Add cancellation/race tests that issue overlapping image and mosaic requests.
  • Enforce a byte budget for webview caches and test eviction with large payloads.
  • Add CI checks for CSP/nonce rules, VSIX contents, pinned release tooling and cross-platform subprocess behavior.
  • Keep structured logs redacted so image data and file contents are not emitted.

Tests currently cover backend payload behavior, manifest contributions, escaping, request guards, resolver ordering and provisioning cleanup. The notable gaps are controller concurrency, webview response handling, cache budgets and very large raw files.

Comment thread .github/workflows/mrd_viz.yml Outdated
Comment thread .github/workflows/mrd_viz_release.yml Outdated
Comment thread .github/workflows/mrd_viz_release.yml Outdated
Comment thread .github/workflows/mrd_viz_release.yml Outdated
Comment on lines +69 to +81
try:
with mrd.BinaryMrdReader(str(path), skip_completed_check=not options.read_full_stream) as reader:
header = reader.read_header()
if header is None:
return _error_payload(path, "Missing MRD header")

state = _new_state(path, header)
for stream_index, item in enumerate(reader.read_data()):
should_stop = _consume_stream_item(state, item, stream_index, options)
if should_stop and not options.read_full_stream:
state["stream"]["partial"] = True
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Initial inspection fully scans acquisition-only MRD streams

The loop's stop condition is driven by the image-thumbnail limit, but acquisition handling always continues and acquisitions never contribute to that limit. An acquisition-only (raw) file is therefore read through EOF on every open, with its arrays materialized.

Impact: opening a large raw MRD can perform multi-gigabyte I/O and repeatedly exceed the extension's 30-second timeout before any metadata is shown.

Fix: apply an independent item / byte / wall-clock budget to initial inspection and mark counts and classification as partial when the budget trips. Reserve complete scans for an explicit user-invoked operation.

Confidence: high

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred until later - larger behavioral change. Thanks for the detailed writeup.

Comment on lines +46 to +56
renderSelectedTile(state.selectedTileThumb, 'Loading full-resolution image...');
const requestId = nextRequestId();
state.pendingRequestId = requestId;
state.pendingRequestIndex = state.selectedImageIndex;
state.pendingRequestCoords = state.selectedSliceCoords.slice();
postMessage({
type: 'loadImage',
requestId: requestId,
imageIndex: state.selectedImageIndex,
sliceCoords: state.selectedSliceCoords.slice()
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Superseded image requests keep expensive subprocesses running

A new requestId correctly prevents a stale response from updating the UI, but nothing cancels the in-flight backend process for the previous request — image requests share the panel-lifetime abort signal rather than a per-request controller.

Impact: rapid tile/slice navigation can leave several Python processes concurrently rescanning the same large MRD for up to 30 seconds each.

Fix: abort the previous selected-image request when it is superseded, or add an explicit cancellation message keyed by request ID.

Confidence: high

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred until later - larger behavioral change.

Comment on lines +49 to +55
options: BackendRunnerOptions,
outputChannel: vscode.OutputChannel,
signal: AbortSignal,
message: Extract<ViewerToExtensionMessage, { type: 'loadImage' }>,
): Promise<void> {
try {
const { payload, stderr } = await runImage(targetUri.fsPath, message.imageIndex, options, signal, message.sliceCoords);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Host side of the missing per-request cancellation

runImage() receives the shared panel-lifetime signal, so a superseded request has no way to be aborted independently of panel disposal.

Fix: create a per-request AbortController (linked to the panel signal), track it by request ID, and abort the prior one when a newer loadImage/mosaic request arrives.

Confidence: high

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred until later - larger behavioral change.

Comment on lines +39 to +50
const MAX_IMAGE_CACHE_ENTRIES = Number(config.maxImageCacheEntries) || 32;

export const imageCache = new Map();

export function cacheImage(key, image) {
if (imageCache.has(key)) {
imageCache.delete(key);
} else if (imageCache.size >= MAX_IMAGE_CACHE_ENTRIES) {
const oldestKey = imageCache.keys().next().value;
imageCache.delete(oldestKey);
}
imageCache.set(key, image);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] The image cache is entry-bounded but not byte-bounded

Eviction counts entries only. With BACKEND_RESPONSE_MAX_BUFFER_BYTES at 64 MiB, 32 retained base64 images is roughly 2 GiB of encoded data, plus decoded renderer memory — and hidden editors retain their webview context.

Impact: browsing perfectly valid large images can destabilize the webview or extension host.

Fix: use a conservative byte-budgeted cache based on encoded length and decoded dimensions, and clear or shrink it when the editor is hidden or disposed.

Confidence: high

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred until later - larger behavioral change.

// in-memory LRU cache. Bounds memory when browsing many tiles: recently viewed images stay
// instant to revisit while the oldest entries are evicted. 32 comfortably covers typical
// back-and-forth navigation without letting the cache grow unbounded.
const MAX_IMAGE_CACHE_ENTRIES = 32;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Cache budget is expressed in entries, not bytes

This is the host-side source of the 32-entry cache limit consumed by media/viewer/state.ts. Paired with a 64 MiB max backend response, an entry count is not a meaningful memory bound.

Fix: express the budget in bytes (and keep the entry count only as a secondary cap). See the related comment on state.ts.

Confidence: high

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred until later - larger behavioral change.

export const PROVISIONING_STEP_TIMEOUT_MS = 10 * 60 * 1_000;

/** stdout/stderr buffer for a backend payload response (base64 PNGs can be large). */
export const BACKEND_RESPONSE_MAX_BUFFER_BYTES = 64 * 1024 * 1024;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] 64 MiB per response is the multiplier behind the unbounded cache

This limit is reasonable on its own, but combined with a 32-entry webview image cache it defines the worst-case retained footprint (~2 GiB encoded). Whatever byte budget the cache adopts should be derived from or cross-referenced with this constant so the two cannot drift apart.

Confidence: high

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred until later - larger behavioral change.

ccapetz and others added 3 commits August 14, 2026 12:48
MRD Viz: Marketplace release prep + publish job
- pull_request/push filters matched the PR base branch (carter-mrd-viz), so
  neither workflow ran for PRs into main; target main instead
- pin @vscode/vsce to an exact version in the package/publish steps
- pin softprops/action-gh-release to a commit SHA (was mutable v2 tag)

Addresses review comments on PR #87.
- reject --slice AXIS above a small cap so a huge axis returns a diagnostic
  instead of attempting an enormous dense-tuple allocation
- guard the webview mosaic against a non-array thumbnails value so a
  malformed backend payload cannot throw and blank the editor

Addresses review comments on PR #87.
@ccapetz ccapetz closed this Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants