Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/_static/visualization/Expanded.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/_static/visualization/overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 14 additions & 3 deletions docs/source/visualization.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ This is very helpful during development, but currently does not provide a hot re
## Usage

The visualizer provides plenty information about the graphs in the system.
![visualizer overview](./_static/visualization/overview.png)

### General Functionality

Expand All @@ -54,6 +55,8 @@ At the bottom left of the main view there are three control buttons from top to
- Centering the view
- Hiding the navigation bar
- Recalculation the graph layout
- Reset Local storage, sometimes graph changes are not update correctly, clearing the storage and reloading resets the view
- Showing/Hiding all node ports

### Available Graphs

Expand All @@ -62,22 +65,25 @@ Available graphs are listed in the sidebar by their name.
### Node Types

The visualizer will show the different node types with their heading.
Tasks will be displayed by their function names and constant values have their values attached.
Tasks will be displayed by their function names and constant values, inputs and outputs have their values attached.

### Node Status

The node status is indicated by the border color of the nodes:

- White/No border: Node has not been started yet
![node state example](./_static/visualization/node_states.png)

- Yellow: Node is currently running
- Green: Node is finished
- Red: An error has occurred in the node (or one of its nested nodes)
- White/No border: Node has not been started yet

### Ports & Values

Inputs and outputs of nodes are indicated by black circles (ports) on the nodes border.
Inputs and outputs of nodes are indicated by black circles (handles) on the nodes border.
Inputs are at the top, outputs at the bottom.
Hovering a port will show the port name.
By using the control button, all ports can be shown simultaneously.
The values in a graph are only visible once the graph has run, indicated on the edges connecting a port.
Small values will be displayed, larger values are truncated with a `{}` symbol.
Hovering the symbol shows the entire value in json format.
Expand All @@ -87,12 +93,17 @@ The graph symbol indicates a value is a constant subgraph supplied as a nested g

The higher order nodes `eval`, `map`, and `loop` can be expanded by pressing the `+` button.
This will show their nested structure.

![node state example](./_static/visualization/Expanded.png)
For `eval` nodes this will immediately be the nested graph;
For `map`/`loop` nodes this will show the individual elements/iterations which each contain their own subgraph.
For unevaluated graphs, this will only show a placeholder evaluation.
To hide the graph again use the `-` button at the top right.

### Logs & Errors

Logs can be accessed by double-clicking a node.
If an error has occurred on a node, it will have a `!` button.
Pressing it will show the error information.

![error logs](./_static/visualization/Debugging.png)
25 changes: 24 additions & 1 deletion tierkreis/tierkreis/controller/storage/filestorage.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,18 @@ def read_output(self, node_location: Loc, output_name: PortID) -> bytes:

def read_errors(self, node_location: Loc) -> str:
if not self._error_logs_path(node_location).exists():
if self._error_path(node_location).exists():
print(self._error_path(node_location))
with open(self._error_path(node_location), "r") as fh:
Copy link
Copy Markdown
Collaborator

@mwpb mwpb Sep 16, 2025

Choose a reason for hiding this comment

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

Okay with this in the short-term, although I'd prefer in future that _error_path be the indicator of whether an error has occurred (and never have contents). In this direction, we should probably change to something like the following in future:-

  • the executor handles error (and logs) redirection
  • if the exit code is non-zero then it touches _error_path
  • it pipes stderr to _error_logs_path

(Also there is an extra print on the line above...)

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.

Adgreed, I just noticed that some nodes didn't have their correct error message attached. I opened #201 to keep track of this

return fh.read()
return ""
with open(self._error_logs_path(node_location), "r") as fh:
return fh.read()
errors = fh.read()
if errors == "":
if self._error_path(node_location).exists():
with open(self._error_path(node_location), "r") as fh:
return fh.read()
return errors

def write_node_errors(self, node_location: Loc, error_logs: str) -> None:
with open(self._error_logs_path(node_location), "w+") as fh:
Expand Down Expand Up @@ -208,3 +217,17 @@ def write_metadata(self, node_location: Loc) -> None:
def read_metadata(self, node_location: Loc) -> dict[str, Any]:
with open(self._metadata_path(node_location)) as fh:
return json.load(fh)

def read_started_time(self, node_location: Loc) -> str | None:
node_def = Path(self._nodedef_path(node_location))
if not node_def.exists():
return None
since_epoch = node_def.stat().st_mtime
return datetime.fromtimestamp(since_epoch).strftime("%Y-%m-%d %H:%M:%S")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Slight preference for isoformat() here but not a big thing.


def read_finished_time(self, node_location: Loc) -> str | None:
done = Path(self._done_path(node_location))
if not done.exists():
return None
since_epoch = done.stat().st_mtime
return datetime.fromtimestamp(since_epoch).strftime("%Y-%m-%d %H:%M:%S")
6 changes: 6 additions & 0 deletions tierkreis/tierkreis/controller/storage/graphdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ def read_metadata(self, node_location: Loc) -> dict[str, Any]:
def write_metadata(self, node_location: Loc) -> None:
raise NotImplementedError("GraphDataStorage is read only storage.")

def read_started_time(self, node_location: Loc) -> str | None:
return None

def read_finished_time(self, node_location: Loc) -> str | None:
return None


def _build_node_outputs(node: NodeDef) -> dict[PortID, None | bytes]:
outputs: dict[PortID, None | bytes] = {val: None for val in node.outputs}
Expand Down
14 changes: 14 additions & 0 deletions tierkreis/tierkreis/controller/storage/in_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class NodeData(BaseModel):
metadata: dict[str, Any] = Field(default_factory=dict)
error_logs: str = ""
outputs: dict[PortID, bytes | None] = Field(default_factory=dict)
started: str | None = None
finished: str | None = None


class ControllerInMemoryStorage:
Expand Down Expand Up @@ -72,6 +74,9 @@ def path_to_loc(self, path: Path) -> tuple[Loc, PortID | None]:

def write_node_def(self, node_location: Loc, node: NodeDef) -> None:
self.nodes[self.loc_to_path(node_location)].definition = node
self.nodes[self.loc_to_path(node_location)].started = datetime.now().strftime(
"%Y-%m-%d %H:%M:%S"
)

def read_node_def(self, node_location: Loc) -> NodeDef:
if result := self.nodes[self.loc_to_path(node_location)].definition:
Expand Down Expand Up @@ -126,6 +131,9 @@ def write_node_errors(self, node_location: Loc, error_logs: str) -> None:

def mark_node_finished(self, node_location: Loc) -> None:
self.nodes[self.loc_to_path(node_location)].is_done = True
self.nodes[self.loc_to_path(node_location)].finished = datetime.now().strftime(
"%Y-%m-%d %H:%M:%S"
)

def is_node_finished(self, node_location: Loc) -> bool:
return self.nodes[self.loc_to_path(node_location)].is_done
Expand Down Expand Up @@ -176,6 +184,12 @@ def write_metadata(self, node_location: Loc) -> None:
"start_time": datetime.now().isoformat(),
}

def read_started_time(self, node_location: Loc) -> str | None:
return self.nodes[self.loc_to_path(node_location)].started

def read_finished_time(self, node_location: Loc) -> str | None:
return self.nodes[self.loc_to_path(node_location)].finished

def clean_graph_files(self) -> None:
uid = os.getuid()
tmp_dir = Path(f"/tmp/{uid}/tierkreis/archive/{time_ns()}")
Expand Down
20 changes: 20 additions & 0 deletions tierkreis/tierkreis/controller/storage/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,23 @@ def write_metadata(self, node_location: Loc) -> None:
:type node_location: Loc
"""
...

def read_started_time(self, node_location: Loc) -> str | None:
"""Reads the start time of a node

:param node_location: The location of the node
:type node_location: Loc
:return: A time string when node has started, else None.
:rtype: str | None
"""
...

def read_finished_time(self, node_location: Loc) -> str | None:
"""Reads the finish time of a node

:param node_location: The location of the node
:type node_location: Loc
:return: A time string when node is completed, else None.
:rtype: str | None
"""
...
Loading