Repository: iterative/dvc

Issue:
`exp show`: provide executor information for finished experiments
The extension has a "Show Logs" command that user's can use to show the logs of experiments running in the queue. This uses `dvc queue logs <id> -f` under the hood. I would like to extend this functionality to show logs of completed/failed experiments. In order to do this I need some indicator that the experiment has available logs. 

To me it makes sense for the API to provide executor information for finished experiments but I am open to suggestions. I would like to avoid introducing calls to another CLI command if possible.

### Demo of show logs

https://user-images.githubusercontent.com/37993418/237017012-ba6f9a62-e24e-448e-959f-9e425ca61310.mov




Discussion:
@pmrowla What are your thoughts?
This is doable, but it's not straightforward since we currently don't associate the task/executor information with finished experiments at all. It's probably a couple of days worth of work to fix in DVC
@mattseddon I think it makes sense as part of a bigger epic on improving the logs. Added an epic in #9442 to discuss, but let me know if you think it's important to prioritize this as a standalone improvement.
https://github.com/iterative/vscode-dvc/issues/3178#issuecomment-1429990393 is related. This is the relevant part:

> I think it's important but complicated, and for now let's start with exposing the logs so at least people know what happened.

☝🏻 this is currently not possible. Does this bump the priority?
@mattseddon Could you expand? Why isn't it possible?
> @mattseddon Could you expand? Why isn't it possible?

Without anything to indicate that the experiment was run in a temporary directory we don't know which experiments have "saved logs"and which don't. 
@mattseddon So if we save logs for workspace experiments, does it mean this issue becomes irrelevant? Trying to figure out if our time is better spent there.
> @mattseddon So if we save logs for workspace experiments, does it mean this issue becomes irrelevant? Trying to figure out if our time is better spent there.

Yep as long as there is a simple way to get to them. `dvc queue logs` doesn't seem appropriate (or like it would work) under those circumstances.
> `dvc queue logs` doesn't seem appropriate

No it doesn't 😄 .

Now that we have dropped checkpoints, it might be worth revisiting [this discussion](https://github.com/iterative/dvc/discussions/8123) about whether we need a separate `dvc queue` command or if it can all be part of `dvc exp`. I think a lot of the need for it was about checkpoints, which are no longer supported.
I guess we will never have logs for dvclive-only experiments, so there's probably no getting around having some way to know which experiments have logs.
@mattseddon Sorry if we already discussed this and I forgot, but is it possible to use `dvc queue status` for this?
> @mattseddon Sorry if we already discussed this and I forgot, but is it possible to use `dvc queue status` for this?

We have talked about this a few times (but I couldn't find anything quickly on GH). Up until this point we've avoided adding that call. The main reasons are performance and not wanting to add to the Git locks issues.
@pmrowla Does calling `dvc queue status` do any git operations or use the git lock?
No, it just queries the celery worker processes, it shouldn't be affecting git locks in either the workspace or the queued exp tempdirs
Discussed this in the cross-team meeting. Requirements to use `dvc queue status` would be that it would need to 

- have the same performance as `exp show` / ability to pass `--rev` flag for each commit we are currently showing
- ability to provide JSON output
- not take any of the DVC locks
- not interfere with the queue if it is called repeatedly



Resolving patch:
diff --git a/dvc/repo/experiments/collect.py b/dvc/repo/experiments/collect.py
--- a/dvc/repo/experiments/collect.py
+++ b/dvc/repo/experiments/collect.py
@@ -261,6 +261,7 @@ def _collect_baseline(
         ref_it = (ref for ref in iter(refs) if ref.startswith(str(ref_info)))
     else:
         ref_it = repo.scm.iter_refs(base=str(ref_info))
+    executors = repo.experiments.celery_queue.collect_success_executors([baseline_rev])
     for ref in ref_it:
         try:
             ref_info = ExpRefInfo.from_ref(ref)
@@ -272,7 +273,11 @@ def _collect_baseline(
         exps = list(collect_branch(repo, exp_rev, baseline_rev, **kwargs))
         if exps:
             exps[0].name = ref_info.name
-            yield ExpRange(exps, name=ref_info.name)
+            yield ExpRange(
+                exps,
+                name=ref_info.name,
+                executor=executors.get(str(ref_info)),
+            )
 
 
 def collect(
diff --git a/dvc/repo/experiments/queue/celery.py b/dvc/repo/experiments/queue/celery.py
--- a/dvc/repo/experiments/queue/celery.py
+++ b/dvc/repo/experiments/queue/celery.py
@@ -42,7 +42,7 @@
 
     from dvc.repo.experiments.executor.base import ExecutorResult
     from dvc.repo.experiments.refs import ExpRefInfo
-    from dvc.repo.experiments.serialize import ExpRange
+    from dvc.repo.experiments.serialize import ExpExecutor, ExpRange
     from dvc_task.app import FSApp
     from dvc_task.proc.manager import ProcessManager
     from dvc_task.worker import TemporaryWorker
@@ -632,3 +632,32 @@ def collect_failed_data(
                 )
             )
         return result
+
+    def collect_success_executors(
+        self,
+        baseline_revs: Optional[Collection[str]],
+        **kwargs,
+    ) -> Dict[str, "ExpExecutor"]:
+        """Map exp refs to any available successful executors."""
+        from dvc.repo.experiments.serialize import ExpExecutor, LocalExpExecutor
+
+        result: Dict[str, "ExpExecutor"] = {}
+        for entry, exec_result in self.iter_success():
+            if baseline_revs and entry.baseline_rev not in baseline_revs:
+                continue
+            if not (exec_result and exec_result.ref_info):
+                continue
+            proc_info = self.proc.get(entry.stash_rev)
+            if proc_info:
+                local_exec: Optional[LocalExpExecutor] = LocalExpExecutor(
+                    log=proc_info.stdout,
+                    pid=proc_info.pid,
+                    returncode=proc_info.returncode,
+                    task_id=entry.stash_rev,
+                )
+            else:
+                local_exec = None
+            result[str(exec_result.ref_info)] = ExpExecutor(
+                "success", name="dvc-task", local=local_exec
+            )
+        return result


Test patch: