Skip to content

Plot origin lost on the source() / run-file path (no code_location fallback) #1334

Description

@midleman

Running an R file with source() (Positron's "run file" / r.sourceCurrentFile) intermittently produces a plot with no origin attribution, so Positron never shows the source-file link on the plot.

Found while chasing a flaky Positron e2e test (plot-file-attribution.test.ts › R - Plot origin shows source file after run file command). It fails as flaky (passes on retry), which is the signature of a timing race rather than a hard bug.

Flake rates

Last 14 days on main, 121 runs each (see metrics):

Test Path Flaky
R - run file command source() 21%
R - line execution line-by-line 1%
Python - line execution line-by-line 0%
Python - run file command python.execInConsole 5%

The R source() path is 21% flaky vs. 1% for R line-execution. The line-execution path attaches a code_location that the source() path lacks, so it isn't exposed to this race.

What the R Kernel log shows

The source() execute request arrives with positron: None (no code_location):

ExecuteRequest { code: "source(\"/tmp/.../plot-attribution-test.R\")", ... positron: None }

The line-by-line variant carries code_location: Some(...). Both plots then go through the same deferred path: hook_holdflush -> process_changes -> store_plot_context -> take_pending_origin.

Why the source() path is exposed

capture_plot_origin checks the source-context stack first, then falls back to ctx.code_location. Line execution always has code_location, so even if the stack timing is off, origin still resolves. The source() path has no such fallback: origin depends entirely on the source-context stack (pushed/popped by the source() hook in hooks_source.R via defer()) being populated at capture time.

When the eager pending_origin snapshot in hook_mode is skipped (it's gated on !old_has_changes && is_drawing), take_pending_origin re-derives origin at process_changes time. If defer() has already popped the stack by then, origin comes back None for good, with no signal to the frontend.

Expected vs. actual

  • Expected: a plot from source("file.R") is attributed to file.R every time.
  • Actual: attribution is empty ~21% of the time on the source() path.

Reproduction

No reliable one-shot repro - it's a timing race, reproducing intermittently under load.

Manual (occasional):

  1. Start a fresh R console in Positron.
  2. Open a .R file containing just plot(1:10).
  3. Run the whole file ("Source" / r.sourceCurrentFile).
  4. Check the Plots pane. Usually the origin link appears; occasionally it's missing.

Via the Positron e2e test: tests/plots/plot-file-attribution.test.ts › "R - Plot origin shows source file after run file command". Fails ~21% in CI (Linux x86_64 electron), which runs parallel workers under load.

Existing ark coverage: test_plot_source_context_stacking and test_plot_from_source_dynamic in crates/ark/tests/integration/plots.rs exercise source() + origin and pass deterministically. The DummyArkFrontend harness is single-threaded, so the defer() pop never races the deferred process_changes - which is why the existing suite doesn't catch this.

Fix direction (per Claude)

Make source-path capture independent of the pop timing, e.g. capture origin eagerly when the source context is pushed, hold the pop until pending plots' process_changes runs, or drop the !old_has_changes gate while a source context is active.

Notes

  • I couldn't confirm the exact stack state from logs alone - the push/pop and origin-capture functions have no trace logging, so the empty-stack moment is inferred from the surrounding sequence. Trace logging there (or a repro under load) would pin down the precise sub-mechanism.
  • The Python run-file case is also elevated (5% flaky vs. 0% for Python line-execution). Python doesn't use source(), so this is likely a separate, milder origin-attribution gap on the Python side rather than the same bug - worth a look independently.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions