Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ In about 30 seconds, you have a typed `source → task → sink` graph that prin
first messages and records `logs/hello-copper.copper`. Start with
`copperconfig.ron`, `src/main.rs`, and `src/tasks.rs`; the generated `justfile`
also provides helpers for logs, CopperLists, topology (`just dag`), the exact
generated process schedule (`just plan`), and replay.
generated process schedule (`just plan`), post-execution timing from the default
Copper log (`just plan-log`), and replay.

## How Copper Fits Together

Expand Down
1 change: 1 addition & 0 deletions examples/cu_caterpillar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ See the crate cu29 for more information about the Copper project.
- `just resim` — rerun the logged mission from `logs/caterpillar.copper`.
- `just resim-debug` — start the replay-backed remote debug server for `logs/caterpillar.copper`.
- `just dag-logstats` — generate logstats and open an annotated DAG SVG for the current `copperconfig.ron`.
- `just plan-log` — auto-detect the default log and logreader, then append packed, proportional observed task timing to `plan.svg`.
4 changes: 4 additions & 0 deletions support/cargo_cunew/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,8 @@ mod tests {
assert!(justfile.contains("dag:"));
assert!(justfile.contains("[positional-arguments]"));
assert!(justfile.contains("plan *options:"));
assert!(justfile.contains("plan-log:"));
assert!(justfile.contains("log-stats"));
assert!(justfile.contains("--bin cu29-plan"));
}

Expand Down Expand Up @@ -648,6 +650,8 @@ mod tests {
assert!(app_manifest.contains("edition = \"2024\""));
assert!(justfile.contains("cu29-rendercfg"));
assert!(justfile.contains("cu29-plan"));
assert!(justfile.contains("plan-log:"));
assert!(justfile.contains("log-stats"));
assert!(!project.join(".git").exists());
}

Expand Down
3 changes: 2 additions & 1 deletion support/cargo_cunew/templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ The generated project includes helper commands in its `justfile`:
* `just resim`: Replay the recorded log once into a fresh replay log.
* `just resim-debug`: Start the replay-backed remote debug server manually.
* `just dag`: Render the application's execution Directed Acyclic Graph (DAG). Local-checkout templates run the tool from the Copper repo; crates.io and git templates use `cu29-rendercfg` from `PATH` or install the matching `cu29-runtime` on demand. `just rcfg` remains as a compatibility alias.
* `just plan`: Render the exact generated per-CopperList process order as `plan.svg`, with serial and `parallel-rt` projections. Pass `mission=<id>` or `features=<a,b>` when needed. Templates use the local `cu29-plan` binary or install the matching `cu29-runtime` on demand.
* `just plan`: Render the exact generated per-CopperList process order as `plan.svg`, with serial and `parallel-rt` projections. Pass `log=logs/<app>.copper` to append packed typical/slowest timelines whose back-to-back segments remain proportional to recorded task duration, or `mission=<id>` / `features=<a,b>` when needed. Templates use the local `cu29-plan` binary or install the matching `cu29-runtime` on demand.
* `just plan-log`: Render the same SVG with observed timing from the project's default Copper log.

Set `APP_NAME=test-workspace-demo APP_DIR=test_workspace-demo` to target the demo app (for example, `APP_NAME=test-workspace-demo APP_DIR=test_workspace-demo just log`).

Expand Down
2 changes: 1 addition & 1 deletion support/cargo_cunew/templates/cu_full/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This template bootstraps a Copper workspace with an app crate and a shared-compo
- `components/bridges/cu_example_shared_bridge/`: shared bridge example crate.
- `components/`: shared components by category (placeholders to extend).
- `doc/`: design notes and project docs.
- `justfile`: automation helpers like `just dag` for topology, `just plan` for generated process order, `just log`, `just cl`, `just resim`, and `just resim-debug` (`just rcfg` remains as a compatibility alias).
- `justfile`: automation helpers like `just dag` for topology, `just plan` for generated or log-observed process timing, `just log`, `just cl`, `just resim`, and `just resim-debug` (`just rcfg` remains as a compatibility alias).

## Quick start

Expand Down
21 changes: 20 additions & 1 deletion support/cargo_cunew/templates/cu_full/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,30 @@ plan *options:
args=("$config" --output "$output" --open)
mission_value=""
features_value=""
log_value=""
for option in "$@"; do
case "$option" in
mission=*) mission_value="${option#mission=}" ;;
features=*) features_value="${option#features=}" ;;
log=*) log_value="${option#log=}" ;;
*)
echo "Unknown plan option '$option'; expected mission=<id> or features=<a,b>." >&2
echo "Unknown plan option '$option'; expected mission=<id>, features=<a,b>, or log=<file>." >&2
exit 2
;;
esac
done
[[ -z "$mission_value" ]] || args+=(--mission "$mission_value")
[[ -z "$features_value" ]] || args+=(--features "$features_value")
if [[ -n "$log_value" ]]; then
APP_NAME="${APP_NAME:-${APP_DIR}}"
[[ "$log_value" != *_0.copper ]] || log_value="${log_value%_0.copper}.copper"
stats="apps/${APP_DIR}/target/cu29_plan_logstats.json"
mkdir -p "$(dirname "$stats")"
logreader_args=(-p "$APP_NAME" --features=logreader --bin "${APP_NAME}-logreader" -- "$log_value" log-stats --config "$config" --output "$stats" --features logreader)
[[ -z "$mission_value" ]] || logreader_args+=(--mission "$mission_value")
cargo run "${logreader_args[@]}"
args+=(--logstats "$stats")
fi
{% if copper_source == "local" %}
cargo run --manifest-path "{{copper_root_path}}/Cargo.toml" -p cu29-runtime --bin cu29-plan -- "${args[@]}"
{% else %}
Expand All @@ -70,6 +82,13 @@ plan *options:
"$planner" "${args[@]}"
{% endif %}

# Render observed timing from the selected app's default Copper log.
plan-log:
#!/usr/bin/env bash
APP_DIR="${APP_DIR:-cu_example_app}"
APP_NAME="${APP_NAME:-${APP_DIR}}"
just plan "log=apps/${APP_DIR}/logs/${APP_NAME}.copper" features=logreader

# Extract the structured log via the log reader.
log:
#!/usr/bin/env bash
Expand Down
2 changes: 1 addition & 1 deletion support/cargo_cunew/templates/cu_project/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This template bootstraps a single-crate Copper project for quick experiments.
- `src/resim.rs`: replay binary with one-shot replay and remote-debug server modes.
- `src/tasks.rs`: sample tasks.
- `copperconfig.ron`: runtime configuration.
- `justfile`: automation helpers like `just log`, `just cl`, `just resim`, `just resim-debug`, `just dag` for topology, and `just plan` for generated process order (`just rcfg` remains as a compatibility alias).
- `justfile`: automation helpers like `just log`, `just cl`, `just resim`, `just resim-debug`, `just dag` for topology, and `just plan` for generated or log-observed process timing (`just rcfg` remains as a compatibility alias).

## Quick start

Expand Down
16 changes: 15 additions & 1 deletion support/cargo_cunew/templates/cu_project/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,28 @@ plan *options:
args=(copperconfig.ron --output plan.svg --open)
mission_value=""
features_value=""
log_value=""
for option in "$@"; do
case "$option" in
mission=*) mission_value="${option#mission=}" ;;
features=*) features_value="${option#features=}" ;;
log=*) log_value="${option#log=}" ;;
*)
echo "Unknown plan option '$option'; expected mission=<id> or features=<a,b>." >&2
echo "Unknown plan option '$option'; expected mission=<id>, features=<a,b>, or log=<file>." >&2
exit 2
;;
esac
done
[[ -z "$mission_value" ]] || args+=(--mission "$mission_value")
[[ -z "$features_value" ]] || args+=(--features "$features_value")
if [[ -n "$log_value" ]]; then
mkdir -p target
[[ "$log_value" != *_0.copper ]] || log_value="${log_value%_0.copper}.copper"
logreader_args=(--features=logreader --bin {{project-name|kebab_case}}-logreader -- "$log_value" log-stats --config copperconfig.ron --output target/cu29_plan_logstats.json --features logreader)
[[ -z "$mission_value" ]] || logreader_args+=(--mission "$mission_value")
cargo run "${logreader_args[@]}"
args+=(--logstats target/cu29_plan_logstats.json)
fi
{% if copper_source == "local" %}
cargo run --manifest-path "{{copper_root_path}}/Cargo.toml" -p cu29-runtime --bin cu29-plan -- "${args[@]}"
{% else %}
Expand All @@ -100,3 +110,7 @@ plan *options:
[[ -x "$planner" ]] || { echo "Failed to find cu29-plan after installation." >&2; exit 1; }
"$planner" "${args[@]}"
{% endif %}

# Render observed timing from the project's default Copper log.
plan-log:
just plan log=logs/{{project-name|kebab_case}}.copper features=logreader
97 changes: 97 additions & 0 deletions support/ci/plan_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env python3

"""Discover the local Copper log and typed logreader used by `just plan-log`."""

from __future__ import annotations

import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Any


def fail(message: str) -> None:
raise SystemExit(f"plan-log: {message}")


def copper_log_bases(directory: Path) -> list[Path]:
logs = directory / "logs"
bases: set[Path] = set()
for path in logs.glob("*.copper"):
name = re.sub(r"_\d+\.copper$", ".copper", path.name)
bases.add(logs / name)
return sorted(bases)


def choose_log(candidates: list[Path]) -> Path:
if not candidates:
fail("no logs/*.copper slabs found; run the example first")
preferred = [
path
for path in candidates
if "resim" not in path.stem and "replay" not in path.stem
]
if preferred:
candidates = preferred
non_compute = [path for path in candidates if "compute" not in path.stem]
if non_compute:
candidates = non_compute
if len(candidates) != 1:
names = ", ".join(path.name for path in candidates)
fail(f"multiple plausible logs ({names}); use `just plan log=... bin=...`")
return candidates[0]


def choose_logreader(targets: list[dict[str, Any]], log: Path) -> dict[str, Any]:
candidates = [
target
for target in targets
if "bin" in target.get("kind", []) and "logreader" in target.get("name", "")
]
if not candidates:
fail("this example has no typed logreader binary")
wants_compute = "compute" in log.stem
matched = [
target
for target in candidates
if ("compute" in target["name"]) == wants_compute
]
if matched:
candidates = matched
if len(candidates) != 1:
names = ", ".join(target["name"] for target in candidates)
fail(f"multiple plausible logreaders ({names}); use `just plan log=... bin=...`")
return candidates[0]


def main() -> None:
if len(sys.argv) != 2:
fail("expected the example directory")
directory = Path(sys.argv[1]).resolve()
metadata = json.loads(
subprocess.check_output(
["cargo", "metadata", "--format-version", "1", "--no-deps"],
cwd=directory,
text=True,
encoding="utf-8",
)
)
packages = [
package
for package in metadata.get("packages", [])
if Path(package["manifest_path"]).resolve().parent == directory
]
if len(packages) != 1:
fail(f"could not identify one Cargo package in {directory}")
log = choose_log(copper_log_bases(directory))
target = choose_logreader(packages[0].get("targets", []), log)
required_features = target.get("required-features", [])
print(target["name"])
print(",".join(required_features))
print(log)


if __name__ == "__main__":
main()
54 changes: 53 additions & 1 deletion support/just/plan.just
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@ plan *options:
mission_value=""
features_value=""
config_value=""
log_value=""
bin_value=""
log_features_value=""
for option in "$@"; do
case "$option" in
mission=*) mission_value="${option#mission=}" ;;
features=*) features_value="${option#features=}" ;;
config=*) config_value="${option#config=}" ;;
log=*) log_value="${option#log=}" ;;
bin=*) bin_value="${option#bin=}" ;;
logfeatures=*) log_features_value="${option#logfeatures=}" ;;
*)
echo "Unknown plan option '$option'; expected mission=<id>, features=<a,b>, or config=<file>." >&2
echo "Unknown plan option '$option'; expected mission=<id>, features=<a,b>, config=<file>, log=<file>, bin=<logreader>, or logfeatures=<a,b>." >&2
exit 2
;;
esac
Expand Down Expand Up @@ -64,5 +70,51 @@ plan *options:
args+=(--features "$features_value")
fi

if [[ -n "$log_value" ]]; then
[[ -n "$bin_value" ]] || { echo "bin=<logreader> is required with log=<file>." >&2; exit 2; }
if [[ "$log_value" = /* ]]; then
log_path="$log_value"
else
log_path="${invocation_dir}/${log_value}"
fi
[[ "$log_path" != *_0.copper ]] || log_path="${log_path%_0.copper}.copper"
log_slab="${log_path%.copper}_0.copper"
[[ -f "$log_path" || -f "$log_slab" ]] || { echo "Copper log not found: $log_path" >&2; exit 1; }
stats_path="${output_dir}/target/cu29_plan_logstats.json"
mkdir -p "$(dirname "$stats_path")"
logreader_args=(run --bin "$bin_value")
[[ -z "$log_features_value" ]] || logreader_args+=(--features "$log_features_value")
logreader_args+=(-- "$log_path" log-stats --config "$cfg_path" --output "$stats_path")
[[ -z "$mission_value" ]] || logreader_args+=(--mission "$mission_value")
config_features_value="$features_value"
[[ -n "$config_features_value" ]] || config_features_value="$log_features_value"
[[ -z "$config_features_value" ]] || logreader_args+=(--features "$config_features_value")
(cd "$invocation_dir" && cargo "${logreader_args[@]}")
args+=(--logstats "$stats_path")
fi

cd "{{PLAN_WORKSPACE_ROOT}}"
cargo run -p cu29-runtime --bin cu29-plan -- "${args[@]}"

# Render observed timing from this example's default local Copper log.
[positional-arguments]
plan-log *options:
#!/usr/bin/env bash
set -euo pipefail

invocation_dir="{{invocation_directory()}}"
defaults="$(python3 "{{PLAN_WORKSPACE_ROOT}}/support/ci/plan_defaults.py" "$invocation_dir")"
bin_default="$(printf '%s\n' "$defaults" | sed -n '1p')"
features_default="$(printf '%s\n' "$defaults" | sed -n '2p')"
log_default="$(printf '%s\n' "$defaults" | sed -n '3p')"
if [[ -z "$bin_default" || -z "$log_default" ]]; then
echo "Could not discover plan-log defaults for ${invocation_dir}." >&2
exit 1
fi

args=("log=${log_default}" "bin=${bin_default}")
if [[ -n "$features_default" ]]; then
args+=("features=${features_default}" "logfeatures=${features_default}")
fi
args+=("$@")
(cd "$invocation_dir" && just plan "${args[@]}")
Loading