diff --git a/README.md b/README.md index 81cc453..764e683 100644 --- a/README.md +++ b/README.md @@ -263,3 +263,17 @@ node test.mjs # run WASM tests | `getrandom` | yes | System RNG for actor addresses | | `no_random` | no | Deterministic counter (for WASM / reproducible tests) | | `python` | no | PyO3 bindings, builds cdylib wheel | +## Connectome analysis + +Spectral analysis of the internal dependency graph, producing a Connectome Complexity Index (CCI) and visual dashboards. + +```sh +# Generate the dependency DAG +cargo run --manifest-path tools/depgraph/Cargo.toml -- --src-dir src/ --output deps + +# Run spectral analysis (outputs to docs/connectome/) +source .venv/bin/activate +python tools/spectral/spectral_analysis.py deps.dot +``` + +This produces a text report, an interactive HTML dashboard, and a static PNG dashboard in `docs/connectome/`. See [docs/connectome.md](docs/connectome.md) for details on the metrics and interpretation. diff --git a/docs/connectome.md b/docs/connectome.md new file mode 100644 index 0000000..db223c0 --- /dev/null +++ b/docs/connectome.md @@ -0,0 +1,76 @@ +# Connectome Analysis + +The connectome analysis applies spectral graph theory to the codebase's internal dependency DAG, producing quantitative coupling metrics and visual dashboards. + +## What it measures + +The tool parses `deps.dot` (a GraphViz DOT file describing struct/trait dependencies between modules) and computes: + +- **Laplacian eigenvalue spectrum** -- encodes the graph's overall connectivity structure +- **Fiedler vector** -- the optimal spectral bisection of the dependency graph, revealing natural module clusters +- **Module coupling matrix** -- directed edge counts between every pair of modules +- **Connectome Complexity Index (CCI)** -- a single 0-1 score combining five sub-metrics: + +| Sub-metric | Weight | What it captures | +|---|---|---| +| Algebraic connectivity (lambda_2/n) | 25% | How tightly connected the graph is | +| Spectral entropy (H/log2(k)) | 25% | How uniformly distributed coupling is across eigenvalues | +| Edge density (\|E\|/n(n-1)) | 15% | Raw ratio of edges to possible edges | +| Cross-module coupling ratio | 20% | Fraction of edges that cross module boundaries | +| Spectral radius (rho/(n-1)) | 15% | Maximum hub concentration | + +### Interpreting CCI + +| CCI range | Label | Meaning | +|---|---|---| +| < 0.30 | LOW | Well-decomposed architecture | +| 0.30 - 0.60 | MODERATE | Typical well-structured codebase | +| > 0.60 | HIGH | Consider reviewing module boundaries | + +## Running + +From the project root: + +```sh +# Default: outputs to docs/connectome/ +python tools/spectral/spectral_analysis.py deps.dot + +# Custom output directory +python tools/spectral/spectral_analysis.py deps.dot -o path/to/output + +# Also emit JSON metrics +python tools/spectral/spectral_analysis.py deps.dot --json + +# Text report only (skip matplotlib PNG) +python tools/spectral/spectral_analysis.py deps.dot --no-plots +``` + +### Prerequisites + +The script requires numpy, scipy, and matplotlib (for the PNG dashboard). These are available in the project's `.venv`: + +```sh +source .venv/bin/activate +python tools/spectral/spectral_analysis.py deps.dot +``` + +## Output files + +All output goes to `docs/connectome/` by default: + +| File | Description | +|---|---| +| `connectome_report.txt` | Full text report with eigenvalues, Fiedler bisection, coupling matrix, and CCI breakdown | +| `connectome_dashboard.html` | Interactive HTML dashboard with zoomable DAG, eigenvalue plot, Fiedler bar chart, and coupling heatmap | +| `connectome_dashboard.png` | Static PNG snapshot of the spectral dashboard (dark theme, 16x12 @ 150 DPI) | +| `connectome_metrics.json` | Machine-readable metrics (only with `--json` flag) | + +## Regenerating deps.dot + +The DOT file is the input to the spectral analysis. To regenerate it from source: + +```sh +cargo run --manifest-path tools/depgraph/Cargo.toml -- --src-dir src/ --output deps +``` + +Then re-run the spectral analysis to update the connectome report. diff --git a/docs/connectome/connectome_dashboard.html b/docs/connectome/connectome_dashboard.html new file mode 100644 index 0000000..342b312 --- /dev/null +++ b/docs/connectome/connectome_dashboard.html @@ -0,0 +1,738 @@ + + + +swactor — dependency analysis + + + + +
+
swactor — dependency analysis
+ + +
+ +
+
+ + + + scroll to zoom · drag to pan · click node to focus +
+
+
Loading Graphviz…
+
+ +
+
+
+

λ Laplacian Eigenvalue Spectrum

+ +
+ +
+

✂ Fiedler Vector — Spectral Bisection

+ +
+ +
+

▦ Module Coupling (directed edge counts)

+ +
+ +
+

∑ Complexity Metrics

+
+
+
+
+ +
+ + + + + + + diff --git a/docs/connectome/connectome_dashboard.png b/docs/connectome/connectome_dashboard.png new file mode 100644 index 0000000..b971549 Binary files /dev/null and b/docs/connectome/connectome_dashboard.png differ diff --git a/docs/connectome/connectome_metrics.json b/docs/connectome/connectome_metrics.json new file mode 100644 index 0000000..db8b631 --- /dev/null +++ b/docs/connectome/connectome_metrics.json @@ -0,0 +1,280 @@ +{ + "graph": { + "n_nodes": 36, + "n_edges": 78, + "n_modules": 8, + "connected_components": 2, + "modules": [ + "error", + "config", + "channel", + "actor", + "address_map", + "runtime", + "worker", + "python" + ] + }, + "spectral": { + "eigenvalues": [ + 0.0, + 0.0, + 0.18637427422819514, + 0.4813940269111958, + 0.6123548189907484, + 0.7985629750697533, + 0.8319091149970231, + 1.004600219615323, + 1.2394224070963267, + 1.3689639255261323, + 1.4526860286383532, + 1.626080007307936, + 2.321279039207482, + 2.3935870074779477, + 2.909249108581605, + 3.1569529438124246, + 3.219980753498557, + 3.3901681819448264, + 3.4799333923457128, + 3.605153966968332, + 3.8847634489335645, + 4.186333826694949, + 4.707024553452379, + 5.173220891347629, + 5.586454240023603, + 5.795938099946378, + 5.8549806331718415, + 6.1828765255391644, + 6.461944112192484, + 6.898584006266002, + 7.3807011063714905, + 7.896480708195232, + 9.160238969430825, + 11.171010263156152, + 14.04747425561517, + 15.533322167445291 + ], + "fiedler_value": 0.0, + "fiedler_vector": [ + 0.0, + 1.6667674979754847e-17, + -4.4166826078552935e-16, + -5.256955919501151e-16, + -1.6422080940489055e-18, + -7.037238109196825e-17, + 8.390622125197347e-16, + 2.3690827037115515e-17, + 1.4176669953736474e-16, + 1.4226827878099615e-16, + 3.1675939003075104e-17, + 2.7236604915425953e-18, + -1.744993274089968e-16, + 2.918795638720409e-17, + -2.1047785816801073e-16, + 1.6100142369066343e-16, + -1.1048855416219909e-16, + 2.623380592723269e-16, + -6.257340472605819e-17, + -2.7901019807352287e-17, + 7.954130131218555e-17, + -2.8145783605573126e-16, + 5.097927800469914e-17, + 1.0000000000000002, + -7.635525673846673e-17, + 4.0203070989124624e-17, + 5.607482503879278e-17, + 1.4848475991077948e-17, + -8.451175680174382e-17, + -1.3333327282927672e-16, + 2.6566833162138994e-16, + 1.0987812721413363e-16, + 4.959951093541129e-16, + -1.2067067461630528e-16, + -2.172546179303562e-16, + -2.3212297109883297e-16 + ], + "node_names": [ + "Error", + "BackoffPolicy", + "RuntimeConfig", + "HybridChannel", + "Receiver", + "Sender", + "Actor", + "ActorAddress", + "ActorInterface", + "AnyActor", + "ContextInner", + "Ctx", + "Message", + "AddressMap", + "Placement", + "WorkerId", + "Envelope", + "Inbox", + "InboxRegistry", + "Runtime", + "RuntimeHandle", + "SenderT", + "ActorPool", + "Mailbox", + "TickContext", + "Worker", + "WorkerContext", + "Effect", + "PyActor", + "PyActorAddress", + "PyCtx", + "PyInbox", + "PyMsg", + "PyRuntime", + "PyRuntimeConfig", + "PyRuntimeHandle" + ], + "node_modules": [ + "error", + "config", + "config", + "channel", + "channel", + "channel", + "actor", + "actor", + "actor", + "actor", + "actor", + "actor", + "actor", + "address_map", + "address_map", + "address_map", + "runtime", + "runtime", + "runtime", + "runtime", + "runtime", + "runtime", + "worker", + "worker", + "worker", + "worker", + "worker", + "python", + "python", + "python", + "python", + "python", + "python", + "python", + "python", + "python" + ] + }, + "module_coupling": { + "module_names": [ + "error", + "config", + "channel", + "actor", + "address_map", + "runtime", + "worker", + "python" + ], + "coupling_matrix": [ + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 3.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0 + ], + [ + 2.0, + 0.0, + 0.0, + 7.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0, + 2.0, + 0.0, + 0.0, + 0.0 + ], + [ + 2.0, + 1.0, + 2.0, + 6.0, + 2.0, + 6.0, + 1.0, + 0.0 + ], + [ + 1.0, + 1.0, + 2.0, + 9.0, + 4.0, + 3.0, + 3.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 5.0, + 0.0, + 3.0, + 0.0, + 10.0 + ] + ], + "cross_module_edges": 46, + "total_edges": 78 + }, + "metrics": { + "algebraic_connectivity": 0.0, + "normalized_algebraic_connectivity": 0.0, + "spectral_entropy": 4.641128070102523, + "normalized_spectral_entropy": 0.9122677088609219, + "edge_density": 0.06190476190476191, + "cross_module_ratio": 0.5897435897435898, + "spectral_radius": 6.676215667817795, + "normalized_spectral_radius": 0.19074901908050843, + "cci": 0.383913712311739 + } +} \ No newline at end of file diff --git a/docs/connectome/connectome_report.txt b/docs/connectome/connectome_report.txt new file mode 100644 index 0000000..a76d543 --- /dev/null +++ b/docs/connectome/connectome_report.txt @@ -0,0 +1,125 @@ +======================================================================== + SPECTRAL ANALYSIS REPORT — Dependency DAG +======================================================================== + +GRAPH SUMMARY +---------------------------------------- + Nodes: 36 + Directed edges: 78 + Modules: 8 + Connected components: 2 + Modules: error, config, channel, actor, address_map, runtime, worker, python + +LAPLACIAN EIGENVALUE SPECTRUM +---------------------------------------- + lambda_ 0 = 0.0000 + lambda_ 1 = 0.0000 <-- Fiedler value (lambda_2) + lambda_ 2 = 0.1864 + lambda_ 3 = 0.4814 + lambda_ 4 = 0.6124 + lambda_ 5 = 0.7986 + lambda_ 6 = 0.8319 + lambda_ 7 = 1.0046 + lambda_ 8 = 1.2394 + lambda_ 9 = 1.3690 + lambda_10 = 1.4527 + lambda_11 = 1.6261 + lambda_12 = 2.3213 + lambda_13 = 2.3936 + lambda_14 = 2.9092 + lambda_15 = 3.1570 + lambda_16 = 3.2200 + lambda_17 = 3.3902 + lambda_18 = 3.4799 + lambda_19 = 3.6052 + lambda_20 = 3.8848 + lambda_21 = 4.1863 + lambda_22 = 4.7070 + lambda_23 = 5.1732 + lambda_24 = 5.5865 + lambda_25 = 5.7959 + lambda_26 = 5.8550 + lambda_27 = 6.1829 + lambda_28 = 6.4619 + lambda_29 = 6.8986 + lambda_30 = 7.3807 + lambda_31 = 7.8965 + lambda_32 = 9.1602 + lambda_33 = 11.1710 + lambda_34 = 14.0475 + lambda_35 = 15.5333 + + Spectral gap (lambda_max - lambda_2): 15.5333 + Fiedler value (algebraic connectivity): 0.0000 + +FIEDLER VECTOR — SPECTRAL BISECTION +---------------------------------------- + Partition A (Fiedler < 0): + HybridChannel [channel ] f = -0.0000 + RuntimeConfig [config ] f = -0.0000 + SenderT [runtime ] f = -0.0000 + PyRuntimeHandle [python ] f = -0.0000 + PyRuntimeConfig [python ] f = -0.0000 + Placement [address_map ] f = -0.0000 + Message [actor ] f = -0.0000 + PyActorAddress [python ] f = -0.0000 + PyRuntime [python ] f = -0.0000 + Envelope [runtime ] f = -0.0000 + PyActor [python ] f = -0.0000 + TickContext [worker ] f = -0.0000 + Sender [channel ] f = -0.0000 + InboxRegistry [runtime ] f = -0.0000 + Runtime [runtime ] f = -0.0000 + Receiver [channel ] f = -0.0000 + ──────────────────────────────────── + Partition B (Fiedler >= 0): + Error [error ] f = +0.0000 + Ctx [actor ] f = +0.0000 + Effect [python ] f = +0.0000 + BackoffPolicy [config ] f = +0.0000 + ActorAddress [actor ] f = +0.0000 + AddressMap [address_map ] f = +0.0000 + ContextInner [actor ] f = +0.0000 + Worker [worker ] f = +0.0000 + ActorPool [worker ] f = +0.0000 + WorkerContext [worker ] f = +0.0000 + RuntimeHandle [runtime ] f = +0.0000 + PyInbox [python ] f = +0.0000 + ActorInterface [actor ] f = +0.0000 + AnyActor [actor ] f = +0.0000 + WorkerId [address_map ] f = +0.0000 + Inbox [runtime ] f = +0.0000 + PyCtx [python ] f = +0.0000 + PyMsg [python ] f = +0.0000 + Actor [actor ] f = +0.0000 + Mailbox [worker ] f = +1.0000 + +MODULE COUPLING MATRIX (directed edge counts) +---------------------------------------- + error config channel actoraddress_map runtime worker python + error 0 0 0 0 0 0 0 0 + config 0 1 0 0 0 0 0 0 + channel 0 0 3 0 0 1 0 0 + actor 2 0 0 7 0 0 0 0 + address_map 0 0 0 1 2 0 0 0 + runtime 2 1 2 6 2 6 1 0 + worker 1 1 2 9 4 3 3 0 + python 0 0 0 5 0 3 0 10 + + Cross-module edges: 46 / 78 (59.0%) + +CONNECTOME COMPLEXITY INDEX (CCI) +---------------------------------------- + Sub-metric Raw Normalized Weight Contrib + ──────────────────────────────────────── ────────── ────────── ──────── ──────── + Algebraic connectivity (lambda_2/n) 0.0000 0.0000 0.25 0.0000 + Spectral entropy (H/log2(k)) 4.6411 0.9123 0.25 0.2281 + Edge density (|E|/n(n-1)) 0.0619 0.0619 0.15 0.0093 + Cross-module coupling ratio 0.5897 0.5897 0.20 0.1179 + Spectral radius (rho/(n-1)) 6.6762 0.1907 0.15 0.0286 + ──────────────────────────────────────── ────────── ────────── ──────── ──────── + CCI (weighted sum) 1.00 0.3839 + + Interpretation: MODERATE complexity — typical well-structured codebase + +======================================================================== \ No newline at end of file diff --git a/spectral_report.txt b/spectral_report.txt new file mode 100644 index 0000000..a76d543 --- /dev/null +++ b/spectral_report.txt @@ -0,0 +1,125 @@ +======================================================================== + SPECTRAL ANALYSIS REPORT — Dependency DAG +======================================================================== + +GRAPH SUMMARY +---------------------------------------- + Nodes: 36 + Directed edges: 78 + Modules: 8 + Connected components: 2 + Modules: error, config, channel, actor, address_map, runtime, worker, python + +LAPLACIAN EIGENVALUE SPECTRUM +---------------------------------------- + lambda_ 0 = 0.0000 + lambda_ 1 = 0.0000 <-- Fiedler value (lambda_2) + lambda_ 2 = 0.1864 + lambda_ 3 = 0.4814 + lambda_ 4 = 0.6124 + lambda_ 5 = 0.7986 + lambda_ 6 = 0.8319 + lambda_ 7 = 1.0046 + lambda_ 8 = 1.2394 + lambda_ 9 = 1.3690 + lambda_10 = 1.4527 + lambda_11 = 1.6261 + lambda_12 = 2.3213 + lambda_13 = 2.3936 + lambda_14 = 2.9092 + lambda_15 = 3.1570 + lambda_16 = 3.2200 + lambda_17 = 3.3902 + lambda_18 = 3.4799 + lambda_19 = 3.6052 + lambda_20 = 3.8848 + lambda_21 = 4.1863 + lambda_22 = 4.7070 + lambda_23 = 5.1732 + lambda_24 = 5.5865 + lambda_25 = 5.7959 + lambda_26 = 5.8550 + lambda_27 = 6.1829 + lambda_28 = 6.4619 + lambda_29 = 6.8986 + lambda_30 = 7.3807 + lambda_31 = 7.8965 + lambda_32 = 9.1602 + lambda_33 = 11.1710 + lambda_34 = 14.0475 + lambda_35 = 15.5333 + + Spectral gap (lambda_max - lambda_2): 15.5333 + Fiedler value (algebraic connectivity): 0.0000 + +FIEDLER VECTOR — SPECTRAL BISECTION +---------------------------------------- + Partition A (Fiedler < 0): + HybridChannel [channel ] f = -0.0000 + RuntimeConfig [config ] f = -0.0000 + SenderT [runtime ] f = -0.0000 + PyRuntimeHandle [python ] f = -0.0000 + PyRuntimeConfig [python ] f = -0.0000 + Placement [address_map ] f = -0.0000 + Message [actor ] f = -0.0000 + PyActorAddress [python ] f = -0.0000 + PyRuntime [python ] f = -0.0000 + Envelope [runtime ] f = -0.0000 + PyActor [python ] f = -0.0000 + TickContext [worker ] f = -0.0000 + Sender [channel ] f = -0.0000 + InboxRegistry [runtime ] f = -0.0000 + Runtime [runtime ] f = -0.0000 + Receiver [channel ] f = -0.0000 + ──────────────────────────────────── + Partition B (Fiedler >= 0): + Error [error ] f = +0.0000 + Ctx [actor ] f = +0.0000 + Effect [python ] f = +0.0000 + BackoffPolicy [config ] f = +0.0000 + ActorAddress [actor ] f = +0.0000 + AddressMap [address_map ] f = +0.0000 + ContextInner [actor ] f = +0.0000 + Worker [worker ] f = +0.0000 + ActorPool [worker ] f = +0.0000 + WorkerContext [worker ] f = +0.0000 + RuntimeHandle [runtime ] f = +0.0000 + PyInbox [python ] f = +0.0000 + ActorInterface [actor ] f = +0.0000 + AnyActor [actor ] f = +0.0000 + WorkerId [address_map ] f = +0.0000 + Inbox [runtime ] f = +0.0000 + PyCtx [python ] f = +0.0000 + PyMsg [python ] f = +0.0000 + Actor [actor ] f = +0.0000 + Mailbox [worker ] f = +1.0000 + +MODULE COUPLING MATRIX (directed edge counts) +---------------------------------------- + error config channel actoraddress_map runtime worker python + error 0 0 0 0 0 0 0 0 + config 0 1 0 0 0 0 0 0 + channel 0 0 3 0 0 1 0 0 + actor 2 0 0 7 0 0 0 0 + address_map 0 0 0 1 2 0 0 0 + runtime 2 1 2 6 2 6 1 0 + worker 1 1 2 9 4 3 3 0 + python 0 0 0 5 0 3 0 10 + + Cross-module edges: 46 / 78 (59.0%) + +CONNECTOME COMPLEXITY INDEX (CCI) +---------------------------------------- + Sub-metric Raw Normalized Weight Contrib + ──────────────────────────────────────── ────────── ────────── ──────── ──────── + Algebraic connectivity (lambda_2/n) 0.0000 0.0000 0.25 0.0000 + Spectral entropy (H/log2(k)) 4.6411 0.9123 0.25 0.2281 + Edge density (|E|/n(n-1)) 0.0619 0.0619 0.15 0.0093 + Cross-module coupling ratio 0.5897 0.5897 0.20 0.1179 + Spectral radius (rho/(n-1)) 6.6762 0.1907 0.15 0.0286 + ──────────────────────────────────────── ────────── ────────── ──────── ──────── + CCI (weighted sum) 1.00 0.3839 + + Interpretation: MODERATE complexity — typical well-structured codebase + +======================================================================== \ No newline at end of file diff --git a/src/actor.rs b/src/actor.rs index 8f5bc11..78886c7 100644 --- a/src/actor.rs +++ b/src/actor.rs @@ -1,6 +1,6 @@ use std::any::Any; -use crate::runtime::Ctx; +use crate::Error; /// The primary trait defining data that can be passed to and from actor processes pub trait Message: 'static + Sized + Clone + Send + Sync {} @@ -49,3 +49,48 @@ where } } } + +/// Object-safe inner trait for sending type-erased messages. +pub(crate) trait ContextInner { + fn send_any(&self, addr: ActorAddress, msg: Box) -> Result<(), Error>; + fn spawn_any(&self, addr: ActorAddress, actor: Box) -> Result<(), Error>; + fn mailbox_waterlevel(&self) -> usize; +} + +/// Actor syscall interface — passed to `ActorInterface::handle()`. +/// +/// Wraps a `&dyn ContextInner` to solve the object-safety problem while +/// providing a typed public API. +pub struct Ctx<'a> { + inner: &'a dyn ContextInner, + self_addr: ActorAddress, +} + +impl<'a> Ctx<'a> { + pub(crate) fn new(inner: &'a dyn ContextInner, self_addr: ActorAddress) -> Self { + Self { inner, self_addr } + } + + #[cfg(feature = "python")] + pub(crate) fn raw_inner(&self) -> &dyn ContextInner { + self.inner + } + + /// Returns the address of the actor currently being ticked. + pub fn self_addr(&self) -> ActorAddress { + self.self_addr + } + + /// Send a typed message to an actor address. + pub fn send(&self, addr: ActorAddress, msg: M) -> Result<(), Error> { + self.inner.send_any(addr, Box::new(msg)) + } + + /// Spawn a new actor, returning its address. + pub fn spawn(&self, actor: A) -> Result { + let addr = ActorAddress::new_random(); + let boxed: Box = Box::new(Actor::new(actor)); + self.inner.spawn_any(addr, boxed)?; + Ok(addr) + } +} diff --git a/src/python.rs b/src/python.rs index 0c15d09..bb6fc32 100644 --- a/src/python.rs +++ b/src/python.rs @@ -4,9 +4,9 @@ use std::cell::RefCell; use pyo3::prelude::*; use pyo3::types::PyModule; -use crate::actor::{Actor, ActorAddress, ActorInterface, AnyActor}; +use crate::actor::{Actor, ActorAddress, ActorInterface, AnyActor, Ctx}; use crate::config::{BackoffPolicy, RuntimeConfig}; -use crate::runtime::{Ctx, Inbox, Runtime, RuntimeHandle}; +use crate::runtime::{Inbox, Runtime, RuntimeHandle}; use crate::Error; // ─── PyMsg newtype ─────────────────────────────────────────────────────────── diff --git a/src/runtime.rs b/src/runtime.rs index b797164..abce263 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -65,43 +65,8 @@ impl RuntimeHandle { } } -/// Actor syscall interface — passed to `ActorInterface::handle()`. -/// -/// Wraps a `&dyn ContextInner` to solve the object-safety problem while -/// providing a typed public API. -pub struct Ctx<'a> { - inner: &'a dyn ContextInner, - self_addr: ActorAddress, -} - -impl<'a> Ctx<'a> { - pub(crate) fn new(inner: &'a dyn ContextInner, self_addr: ActorAddress) -> Self { - Self { inner, self_addr } - } - - #[cfg(feature = "python")] - pub(crate) fn raw_inner(&self) -> &dyn ContextInner { - self.inner - } - - /// Returns the address of the actor currently being ticked. - pub fn self_addr(&self) -> ActorAddress { - self.self_addr - } - - /// Send a typed message to an actor address. - pub fn send(&self, addr: ActorAddress, msg: M) -> Result<(), Error> { - self.inner.send_any(addr, Box::new(msg)) - } - - /// Spawn a new actor, returning its address. - pub fn spawn(&self, actor: A) -> Result { - let addr = ActorAddress::new_random(); - let boxed: Box = Box::new(Actor::new(actor)); - self.inner.spawn_any(addr, boxed)?; - Ok(addr) - } -} +// Re-export Ctx and ContextInner for backwards compatibility +pub use crate::actor::{ContextInner, Ctx}; /// Type-erased sender for external inboxes. pub(crate) trait SenderT: Send + Sync { @@ -287,7 +252,7 @@ impl Runtime { inbox_registry: &rt_clone.inbox_registry, config: &rt_clone.config, }; - worker.run(&tc, &rt_clone.is_running, &rt_clone.config.backoff_policy); + worker.run(&tc, &rt_clone.is_running); }); handles.push(handle); } @@ -400,14 +365,6 @@ impl InboxRegistry { -/// Object-safe inner trait for sending type-erased messages. -pub(crate) trait ContextInner { - fn send_any(&self, addr: ActorAddress, msg: Box) -> Result<(), Error>; - fn spawn_any(&self, addr: ActorAddress, actor: Box) -> Result<(), Error>; - fn mailbox_waterlevel(&self) -> usize; -} - - impl ContextInner for Runtime { fn send_any(&self, addr: ActorAddress, msg: Box) -> Result<(), Error> { match self.address_map.lookup(&addr) { diff --git a/src/worker/mod.rs b/src/worker/mod.rs index 493b9c4..c2493ff 100644 --- a/src/worker/mod.rs +++ b/src/worker/mod.rs @@ -5,11 +5,11 @@ use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; use std::sync::Arc; use std::thread; -use crate::actor::{ActorAddress, AnyActor, Message}; +use crate::actor::{ActorAddress, AnyActor, ContextInner, Ctx, Message}; use crate::address_map::{AddressMap, Placement, WorkerId}; use crate::channel::{Receiver, Sender}; -use crate::config::{BackoffPolicy, RuntimeConfig}; -use crate::runtime::{ContextInner, Ctx, Envelope, InboxRegistry}; +use crate::config::RuntimeConfig; +use crate::runtime::{Envelope, InboxRegistry}; use crate::Error; /// Per-worker stats published via atomics. Readable from any thread. @@ -90,12 +90,7 @@ impl Worker { { let worker_ctx = WorkerContext { worker_id: self.id, - address_map: tc.address_map, - transfer_txs: tc.transfer_txs, - spawn_txs: tc.spawn_txs, - placement: tc.placement, - inbox_registry: tc.inbox_registry, - config: tc.config, + tc, pending_local: &pending_local, }; processed = self.pool.tick_all(&worker_ctx); @@ -121,7 +116,8 @@ impl Worker { did_work } - pub(crate) fn run(&mut self, tc: &TickContext, is_running: &AtomicBool, backoff: &BackoffPolicy) { + pub(crate) fn run(&mut self, tc: &TickContext, is_running: &AtomicBool) { + let backoff = &tc.config.backoff_policy; let mut idle_count: u32 = 0; while is_running.load(Ordering::Acquire) { let did_work = self.tick_once(tc); @@ -151,18 +147,13 @@ impl Worker { /// Cross-worker sends go through the transfer queue. struct WorkerContext<'a> { worker_id: WorkerId, - address_map: &'a AddressMap, - transfer_txs: &'a [Sender], - spawn_txs: &'a [Sender<(ActorAddress, Box)>], - placement: &'a Placement, - inbox_registry: &'a InboxRegistry, - config: &'a RuntimeConfig, + tc: &'a TickContext<'a>, pending_local: &'a RefCell)>>, } impl ContextInner for WorkerContext<'_> { fn send_any(&self, addr: ActorAddress, msg: Box) -> Result<(), Error> { - match self.address_map.lookup(&addr) { + match self.tc.address_map.lookup(&addr) { Some(wid) if wid == self.worker_id => { // Same worker: buffer for local delivery (after current tick round) self.pending_local.borrow_mut().push((addr, msg)); @@ -171,26 +162,26 @@ impl ContextInner for WorkerContext<'_> { Some(wid) => { // Cross worker: envelope through transfer queue let envelope = Envelope::new(addr, msg); - let _ = self.transfer_txs[wid.as_usize()].try_send(envelope); + let _ = self.tc.transfer_txs[wid.as_usize()].try_send(envelope); Ok(()) } None => { // Try inbox registry (external inboxes) - self.inbox_registry.try_deliver(addr, msg) + self.tc.inbox_registry.try_deliver(addr, msg) } } } fn spawn_any(&self, addr: ActorAddress, actor: Box) -> Result<(), Error> { - let worker_id = self.placement.next_worker(); - self.address_map.insert(addr, worker_id); - self.spawn_txs[worker_id.as_usize()] + let worker_id = self.tc.placement.next_worker(); + self.tc.address_map.insert(addr, worker_id); + self.tc.spawn_txs[worker_id.as_usize()] .try_send((addr, actor)) .map_err(|_| Error::from("Spawn queue full")) } fn mailbox_waterlevel(&self) -> usize { - self.config.mailbox_waterlevel + self.tc.config.mailbox_waterlevel } } diff --git a/src/worker/tests.rs b/src/worker/tests.rs index 720d968..5cdca33 100644 --- a/src/worker/tests.rs +++ b/src/worker/tests.rs @@ -4,11 +4,11 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; use std::thread; -use crate::actor::{ActorAddress, AnyActor}; +use crate::actor::{ActorAddress, AnyActor, Ctx}; use crate::address_map::{AddressMap, Placement, WorkerId}; use crate::channel::Receiver; -use crate::config::{BackoffPolicy, RuntimeConfig}; -use crate::runtime::{Ctx, Envelope, InboxRegistry}; +use crate::config::RuntimeConfig; +use crate::runtime::{Envelope, InboxRegistry}; use super::{TickContext, Worker, WorkerStats}; @@ -399,7 +399,6 @@ fn run_loop_stops_on_shutdown() { let mut worker = Worker::new(WorkerId(0), transfer_rx, spawn_rx, stats); let is_running = AtomicBool::new(false); - let backoff = BackoffPolicy::default(); let address_map = AddressMap::new(); let placement = Placement::new(1); let inbox_registry = InboxRegistry::new(); @@ -415,6 +414,6 @@ fn run_loop_stops_on_shutdown() { }; thread::scope(|s| { - s.spawn(|| worker.run(&tc, &is_running, &backoff)); + s.spawn(|| worker.run(&tc, &is_running)); }); } diff --git a/tools/spectral/spectral_analysis.py b/tools/spectral/spectral_analysis.py index 5b4b869..57f2a44 100644 --- a/tools/spectral/spectral_analysis.py +++ b/tools/spectral/spectral_analysis.py @@ -1412,8 +1412,8 @@ def main() -> None: description="Spectral analysis of dependency DAGs" ) parser.add_argument("dot_file", help="Path to DOT file (from depgraph)") - parser.add_argument("-o", "--output-dir", default=".", - help="Output directory (default: current directory)") + parser.add_argument("-o", "--output-dir", default="docs/connectome", + help="Output directory (default: docs/connectome)") parser.add_argument("--no-plots", action="store_true", help="Text report only (no matplotlib dependency)") parser.add_argument("--json", action="store_true", @@ -1435,25 +1435,25 @@ def main() -> None: # Generate report report = generate_report(result) print(report) - report_path = os.path.join(args.output_dir, "spectral_report.txt") + report_path = os.path.join(args.output_dir, "connectome_report.txt") with open(report_path, "w") as f: f.write(report) print(f"\nReport saved to {report_path}") # Generate interactive HTML dashboard - html_path = os.path.join(args.output_dir, "spectral_dashboard.html") + html_path = os.path.join(args.output_dir, "connectome_dashboard.html") generate_dashboard_html(result, html_path, dot_source=dot_text) print(f"Interactive dashboard saved to {html_path}") # Generate static PNG dashboard if not args.no_plots: - dashboard_path = os.path.join(args.output_dir, "spectral_dashboard.png") + dashboard_path = os.path.join(args.output_dir, "connectome_dashboard.png") generate_dashboard(result, dashboard_path) print(f"Static dashboard saved to {dashboard_path}") # Generate JSON if args.json: - json_path = os.path.join(args.output_dir, "spectral_metrics.json") + json_path = os.path.join(args.output_dir, "connectome_metrics.json") with open(json_path, "w") as f: json.dump(metrics_to_dict(result), f, indent=2) print(f"JSON saved to {json_path}")