diff --git a/tools/rt-app-v2/.gitignore b/tools/rt-app-v2/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..ea8c4bf7f35f6f77f75d92ad8ce8349f6e81ddba --- /dev/null +++ b/tools/rt-app-v2/.gitignore @@ -0,0 +1 @@ +/target diff --git a/tools/rt-app-v2/.rustfmt.toml b/tools/rt-app-v2/.rustfmt.toml new file mode 100644 index 0000000000000000000000000000000000000000..69a377c96c68e37c8d34ae5189784730c8e0d6d0 --- /dev/null +++ b/tools/rt-app-v2/.rustfmt.toml @@ -0,0 +1,2 @@ +imports_granularity="Crate" +group_imports="StdExternalCrate" diff --git a/tools/rt-app-v2/Cargo.lock b/tools/rt-app-v2/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..25ec2152ae1fc1dd0e16459898dfb00e4379090c --- /dev/null +++ b/tools/rt-app-v2/Cargo.lock @@ -0,0 +1,350 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + +[[package]] +name = "clap" +version = "4.5.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "errno" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "libc" +version = "0.2.173" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rt-app-v2" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "errno", + "libc", + "serde", + "serde_json", + "strum", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/tools/rt-app-v2/Cargo.toml b/tools/rt-app-v2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..400e5b4b5270af075c9439647f91925d94323f06 --- /dev/null +++ b/tools/rt-app-v2/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rt-app-v2" +version = "0.1.0" +edition = "2024" + +[lib] +name = "lib" +path = "src/lib/lib.rs" + +[dependencies] +anyhow = "~1.0" +clap = { version = "~4.5", features = ["derive"] } +errno = "~0.3" +libc = "~0.2" +serde = { version = "~1.0", features = ["derive"] } +serde_json = "~1.0" +strum = { version = "~0.27", features = ["derive"] } diff --git a/tools/rt-app-v2/README.md b/tools/rt-app-v2/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4502c584bdc2b155bc0b5d29dc8290c4497e75f6 --- /dev/null +++ b/tools/rt-app-v2/README.md @@ -0,0 +1,180 @@ +# rt-app-v2 : A Rustified rt-app + +## Description + +This is a very minimal prototype for a Rust version of rt-app, meant to serve as proof of concept for how the full version will work. + +It implements the current rt-app memory workload ('mem' event) as an example of how events can be added in this Rust version. + +There is also a compatibility high level configuration, meant to illustrate how multiple frontends can be used. The current rt-app configuration features will be ported here for backwards compatibility. + +Both the v2 and compatibility frontend have their own CLI, such that no changes to config or command line are required when running rt-app-v2 in compatibility mode. + +## Motivation + +Certain aspects of rt-app could benefit from a refresh, e.g.: + +- configuration quirks: cpus not being handled like any other key, coupling between priority and policy etc. +- bugs linked to propagating the current state to the next phase +- boilerplate resulting from dealing with JSON in C + +Combined, such aspects complicate the addition new events and features to rt-app. + +In addition, an internal representation of the low level execution plan can be used as "compilation" target not just by rt-app frontends, but by other external tools. + +It would also be nice to have errors reported will full context when the input is ill-formed (e.g. versus being told simply that `sched_setattr()` returned `-1` when the user sets a priority value for a **SCHED_DEADLINE** task). + +## Approach + +The approach used here is the compiler pattern: + +- a frontend takes a (CLI, high level JSON/YAML config) pair and generates the low level internal representation +- the low level configuration is converted to a runnable "instruction" format + +The main advantage of this approach is that the low level representation can be a target for multiple frontends. Backwards compatibility with current rt-app configurations can be implemented as a frontend. + +This also means full freedom to develop the v2 frontend according to current needs, although we don't foresee major changes to the CLI and high level config. + +Selecting between the two frontends (v2 and compatibility) is done via an optional subcommand (e.g. `exec rt-app-v2 run-compat "$@"` to run in compatibility mode). The subcommand will default to selecting the v2 frontend, so it need not be supplied when running in default mode. Additionally, to preserve compatibility with old scripts, when invoked as "rt-app" (e.g. via symlink), compatibility mode is selected regardless of the subcommand. + +### High level configuration + +The high-level configuration will be similar to current rt-app, but with each phase fully independent from the others. + +Logic for managing default values will be simplified. + +Each high level configuration node is converted to one or more low level statements. + +### Low level configuration + +Structured as a simple imperative language - list of statements where each syscall has an associated action. Can be printed out as a low level execution plan. + +Examples: + +- Current rt-app events are also actions +- A sub-program is a list of actions +- A loop is an event with a loop counter and an associated sub-program +- A fork is an event with a task name and an associated sub-program +- An action to emit a line of log in a ftrace marker or on stderr +- An action to increment a counter, with support in the log action to dump its value + +This internal representation is lowered to a list of instructions. This representation is nearly identical to the low level configuration, the subtle difference being that it holds only the resources needed to execute these statements. + +The Memset event provides an example of such a difference: the low level Memset node holds metadata about the event (the buffer size), but the Memset instruction requires a pointer to a memory region of that size and consequently does not need to track the buffer size separately. + +### Calibration + +The original rt-app calibration functionality has been ported to rt-app-v2, with some changes. + +The calibration step can now be run as an independent subcommand. This decision was made because there is not much use for traces generated by a run where the calibration step executed before the rt-app events. Also, when the user wanted to execute only the calibration, with the intention of generating the number for subsequent runs, they would have to also provide some dummy task, otherwise rt-app would complain. + +In consequence, the `calibration` section is mandatory in the JSON config for the v2 frontend. +The original behaviour can be preserved for the compatibility frontend by wrapping the calibration data in an `Option` and executing calibration when we parse `None` from the JSON (not implemented in this PoC). + +Calibration methods are now versioned, which should help with backwards compatibility and repeatability of experiments in the future since we could have as many calibration methods as we want. + +## Getting started + +To build and run this project, you will need an up to date Rust environment: + +If you already have Rust installed, consider updating it by running `rustup update`. So far, this project was developed using rustc >= 1.85. + +## Usage + +```bash +$ ./target/debug/rt-app-v2 help +Usage: rt-app-v2 + +Commands: + run Selects the v2 frontend + run-compat Selects the compatibility frontend + calibrate Runs the calibration step + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help +``` + +You can also run ```./target/debug/rt-app-v2 help ``` to learn about all the arguments and options of a subcommand. + +### Running the v2 example + +Run `cargo build` and then execute the binary: + +```bash +$ cargo build +$ ./target/debug/rt-app-v2 examples/config.json +Running with calibration: CalibrationData { cpu: 0, version: V1, pLoad: 139 } +Running Memset { count: 10, buf_size: 4194304 } +Running Memset { count: 10, buf_size: 2097152 } +``` + +or, alternatively: + +```bash +$ cargo run -- examples/config.json + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s + Running `target/debug/rt-app-v2 examples/config.json` +Running with calibration: CalibrationData { cpu: 0, version: V1, pLoad: 139 } +Running Memset { count: 10, buf_size: 4194304 } +Running Memset { count: 10, buf_size: 2097152 } +``` + +### Running the compatibility example + +Run `cargo build` and then execute the binary: + +```bash +$ cargo build +$ ./target/debug/rt-app-v2 run-compat examples/config_compat.json +Running with calibration: CalibrationData { cpu: 0, version: V1, pLoad: 139 } +Running Memset { count: 10, buf_size: 4194304 } +Running Memset { count: 10, buf_size: 2097152 } +``` + +or, alternatively: + +```bash +$ cargo run -- run-compat examples/config_compat.json + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s + Running `target/debug/rt-app-v2 examples/config.json` +Running with calibration: CalibrationData { cpu: 0, version: V1, pLoad: 139 } +Running Memset { count: 10, buf_size: 4194304 } +Running Memset { count: 10, buf_size: 2097152 } +``` + +For full compatibility with the rt-app command line, the program can be called under the 'rt-app' name and it will automatically select the compatibility frontend: + +```bash +$ cargo build +$ ln -s target/debug/rt-app-v2 rt-app +$ ./rt-app examples/config_compat.json +Running with calibration: CalibrationData { cpu: 0, version: V1, pLoad: 139 } +Running Memset { count: 10, buf_size: 4194304 } +Running Memset { count: 10, buf_size: 2097152 } +``` + +### Running the calibration step + +Both frontends currently expect to find the calibration info in the JSON configuration, so the example configs have been provided with a calibration section already populated. +To run calibration manually and obtain numbers relevant to your platform, invoke the 'calibrate' subcommand: + +```bash +$ ./target/debug/rt-app-v2 calibrate 0 +Calibration step completed: CalibrationData { cpu: 0, version: V1, pLoad: 145 } +``` + +***Note***: The resulting `pLoad` depends not only on the platform, but also the optimization level applied by the compiler, i.e. whether it is a debug or release build. To build in release mode use ```$ cargo build --release``` (the output will be under `target/release/`). + +If the calibration step is successful, a `calibration_data.json` file should have been generated in the project root directory. It contains JSON representation of the generated `CalibrationData`. + +## Roadmap + +- Implement the existing rt-app workloads. +- Port the current rt-app configuration to the "legacy" frontend. +- Try to implement Rust [FFI](https://doc.rust-lang.org/nomicon/ffi.html) bindings to basic musl libc functions. +- Try to implement an event that executes an assembly snippet. + +## License + +This project is licensed under Apache-2.0 (). diff --git a/tools/rt-app-v2/examples/config.json b/tools/rt-app-v2/examples/config.json new file mode 100644 index 0000000000000000000000000000000000000000..17d7a4a1f8f8e284bfe64a8f8df74c172215b7c8 --- /dev/null +++ b/tools/rt-app-v2/examples/config.json @@ -0,0 +1,20 @@ +{ +"tasks": [ + { + "type": "Memset", + "count": 10 + }, + { + "type" : "Memset", + "count": 10, + "buf_size": 2097152 + } +], + +"calibration": { + "type":"CalibrationData", + "cpu":0, + "version":"V1", + "p_load":139 +} +} diff --git a/tools/rt-app-v2/examples/config_compat.json b/tools/rt-app-v2/examples/config_compat.json new file mode 100644 index 0000000000000000000000000000000000000000..b16536c2f5cb3b2b9d0c8c124e687b38e075ba81 --- /dev/null +++ b/tools/rt-app-v2/examples/config_compat.json @@ -0,0 +1,20 @@ +{ +"tasks": [ + { + "type": "Mem", + "count": 10 + }, + { + "type" : "Mem", + "count": 10, + "buf_size": 2097152 + } +], + +"calibration": { + "type":"CalibrationData", + "cpu":0, + "version":"V1", + "p_load":139 +} +} diff --git a/tools/rt-app-v2/src/lib/affinity_utils.rs b/tools/rt-app-v2/src/lib/affinity_utils.rs new file mode 100644 index 0000000000000000000000000000000000000000..1589bf2fb097dcca777054f3b0d59c76d3d5f33a --- /dev/null +++ b/tools/rt-app-v2/src/lib/affinity_utils.rs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2025, ARM Limited and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Affinity utils +//! +//! Collection of helpers for manipulating a thread's affinity. + +use std::mem; + +use anyhow::{Ok, Result, anyhow}; +use libc::{CPU_SET, CPU_ZERO, cpu_set_t, pid_t, sched_getaffinity, sched_setaffinity}; + +/// Set the affinity mask of a thread to a given cpu_set_t. +/// +/// # Errors +/// +/// This operation can fail if the call to sched_setaffinity() is unsuccessful, in which case the +/// errno value will be returned wrapped inside an anyhow::Error. +/// Consult the man page for sched_setaffinity for all the possible reasons it might fail. +pub fn set_thread_affinity(pid: pid_t, cpu_set: &cpu_set_t) -> Result<()> { + let res = unsafe { sched_setaffinity(pid, mem::size_of::(), cpu_set) }; + + match res { + 0 => Ok(()), + _ => Err(anyhow!( + "sched_setaffinity() failed with error {}", + errno::errno() + )), + } +} + +/// Set the affinity mask of the calling (current) thread to a given cpu_set_t. +pub fn set_calling_thread_affinity(cpu_set: &cpu_set_t) -> Result<()> { + set_thread_affinity(0, cpu_set) +} + +/// Get the affinity mask of a thread. +/// +/// # Errors +/// +/// This operation can fail if the call to sched_getaffinity() is unsuccessful, in which case the +/// errno value will be returned wrapped inside an anyhow::Error. +/// Consult the man page for sched_getaffinity for all the possible reasons it might fail. +pub fn get_thread_affinity(pid: pid_t) -> Result { + let mut s = empty_cpu_set(); + + let res = unsafe { sched_getaffinity(pid, mem::size_of::(), &mut s) }; + + match res { + 0 => Ok(s), + _ => Err(anyhow!( + "sched_getaffinity() failed with error {}", + errno::errno() + )), + } +} + +/// Get the affinity mask of the calling (current) thread. +pub fn get_calling_thread_affinity() -> Result { + get_thread_affinity(0) +} + +fn empty_cpu_set() -> cpu_set_t { + unsafe { + let mut s = mem::zeroed::(); + // Although we just zeroed the memory, the libc documentation recommends we treat + // cpu_set_t as an opaque type, so we call CPU_ZERO to ensure the set is empty. + CPU_ZERO(&mut s); + + s + } +} + +/// Convert a vector of CPU ids to a cpu_set_t. +pub fn cpu_set_from_vec(cpus: Vec) -> cpu_set_t { + let mut s = empty_cpu_set(); + + for cpu in cpus { + unsafe { CPU_SET(cpu, &mut s) }; + } + + s +} diff --git a/tools/rt-app-v2/src/lib/calibration.rs b/tools/rt-app-v2/src/lib/calibration.rs new file mode 100644 index 0000000000000000000000000000000000000000..211d59c7ce7f3a999686f3f01c013fc99b1a9c00 --- /dev/null +++ b/tools/rt-app-v2/src/lib/calibration.rs @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2025, ARM Limited and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Calibration +//! +//! Implements functions for determining the calibration number (p_load) for a target CPU. +//! +//! p_load is the number of ns spent per iteration of a cycle-eater loop, and is meant to represent +//! the highest achievable compute capacity. +//! +//! A cycle-eater is a function whose purpose is to "waste" cycles while stressing the system in a +//! particular way. Picking a cycle-eater that creates a similar kind of load or instruction mix to +//! an event will lead to better calibration numbers for runs using that event. + +use std::{ + cmp, fmt, + hint::black_box, + str::FromStr, + thread::sleep, + time::{Duration, Instant}, +}; + +use anyhow::{Context, Result, anyhow}; +use clap::Parser; +use serde::{Deserialize, Serialize}; +use strum::{EnumString, VariantNames}; + +use crate::affinity_utils::{ + cpu_set_from_vec, get_calling_thread_affinity, set_calling_thread_affinity, +}; + +/// Executes given amount of iterations of a cycle-eater function. +pub fn busyloop(loop_count: u64, f: fn() -> Result) -> Result<()> { + for _ in 0..loop_count { + black_box(f()?); + } + + Ok(()) +} + +/// Recursive cycle-eater based on floating point multiplication and exponentiation. +/// +/// Black boxes are used extensively alongside recursion to prevent aggressive optimization +/// by the compiler. +fn exp2_cycle_eater(x: f64, y: f64, rec: u32) -> f64 { + match rec { + 0 => black_box(x) * black_box(y.exp2()), + _ => black_box(x) * black_box(exp2_cycle_eater(x, y, rec - 1)).exp2(), + } +} + +/// Current rt-app (v1) cycle-eater. +fn rt_app_cycle_eater() -> Result { + let param = 0.95; + let n: f64 = 4.0; + + Ok(exp2_cycle_eater(param, n, 11)) +} + +/// Determine the average ns spent in a cycle-eater iteration. +/// +/// Runs the busyloop function until either the average ns per loop value converges within +/// a 2% difference or it has completed the given iterations. +/// Also offers the option to sleep for a number of seconds between calibration iterations. +/// +/// # Errors +/// +/// This function fails when the average ns per loop value does not converge within the given +/// iteration count. +fn calibrate_cpu_cycles( + sleep_s: u64, + max_iterations: u32, + f: fn() -> Result, +) -> Result { + let mut max_load_loop = 10000; + let mut nsec_per_loop = 0; + let mut avg_ns_per_loop = 0; + + for _ in 0..max_iterations { + if sleep_s > 0 { + sleep(Duration::new(sleep_s, 0)); + } + + // TODO: Allow different clock sources? Instant uses monotonic, maybe allow a real-time + // source as well? + let start_time = Instant::now(); + black_box(busyloop(max_load_loop, black_box(f))?); + let duration_ns = start_time.elapsed().as_nanos(); + + nsec_per_loop = duration_ns / u128::from(max_load_loop); + avg_ns_per_loop = (avg_ns_per_loop + nsec_per_loop) >> 1; + + // Check if the values converge. + if nsec_per_loop.abs_diff(avg_ns_per_loop) * 50 < avg_ns_per_loop { + return Ok(avg_ns_per_loop); + } + + // Randomize the busyloop iteration count in order to avoid hitting some specific platform + // loop duration (e.g. cpufreq period). + max_load_loop += 33333; + max_load_loop %= 1000000; + } + + Err(anyhow!( + "Failed to converge after {} iterations.\n\ + Average nanoseconds per cycle-eater loop on last calibration iteration: {}.\n\ + Average nanoseconds per cycle-eater loop across all calibration iterations: {}", + max_iterations, + nsec_per_loop, + avg_ns_per_loop + )) +} + +/// Current rt-app (v1) calibration method. +/// +/// Mixes two calibration runs (with and without sleep intervals) and chooses the minimum value. +fn calibrate_v1() -> Result { + let calib1 = + calibrate_cpu_cycles(1, 1000, rt_app_cycle_eater).with_context(|| "Method 1 failed")?; + + let calib2 = + calibrate_cpu_cycles(0, 1000, rt_app_cycle_eater).with_context(|| "Method 2 failed")?; + + Ok(cmp::min(calib1, calib2)) +} + +fn calibrate(version: &CalibrationMethod, target_cpu: usize) -> Result { + let original_affinity = get_calling_thread_affinity() + .with_context(|| "Failed to obtain affinity mask for current thread")?; + + let calibration_affinity = cpu_set_from_vec(vec![target_cpu]); + + set_calling_thread_affinity(&calibration_affinity).with_context(|| { + format!( + "Failed to pin calibration task to target cpu {}", + target_cpu + ) + })?; + + let p_load = match version { + CalibrationMethod::V1 => calibrate_v1(), + }; + + set_calling_thread_affinity(&original_affinity) + .with_context(|| "Failed to restore original affinity after calibration step")?; + + p_load +} + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct CalibrationArgs { + /// Target CPU to use during calibration + #[arg(index = 1)] + pub cpu: usize, + + /// Which version of the calibration method to use + #[arg(short, long)] + pub calibration_version: Option, +} + +/// Internal representation for the different versions of calibration methods. +/// +/// The enum variants are parsed from strings with the same name, provided by users via arguments +/// for the calibration subcommand. +#[derive(Default, Debug, Serialize, Deserialize, EnumString, VariantNames)] +#[strum(serialize_all = "snake_case")] +enum CalibrationMethod { + #[default] + V1, +} + +/// Holds information for the calibration run. +/// +/// The value of pload is determined by the calibration step. +/// +/// This struct is serialized/deserialized such that the output of a calibration run can be pasted +/// into the JSON config of a subsequent workload run to provide the calibration data. +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub struct CalibrationData { + cpu: usize, + version: CalibrationMethod, + p_load: u128, +} + +impl CalibrationData { + pub fn new() -> Self { + CalibrationData { + cpu: 0, + version: Default::default(), + p_load: 0, + } + } + + /// Build the CalibrationData from command line arguments and the calibration step. + /// + /// If a version for the calibration method is not supplied, v1 will be selected by default. + /// + /// # Errors + /// + /// This function can fail when the calibration step is unsuccessful. + pub fn build(args: &CalibrationArgs) -> Result { + let v = match &args.calibration_version { + Some(s) => CalibrationMethod::from_str(s).with_context(|| { + format!( + "Unrecognized calibration method '{}'. Supported methods are: {:?}", + s, + CalibrationMethod::VARIANTS + ) + })?, + None => Default::default(), + }; + + let p = calibrate(&v, args.cpu)?; + + Ok(CalibrationData { + cpu: args.cpu, + version: v, + p_load: p, + }) + } +} + +impl Default for CalibrationData { + fn default() -> Self { + Self::new() + } +} + +impl fmt::Display for CalibrationData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CalibrationData") + .field("cpu", &self.cpu) + .field("version", &self.version) + .field("pLoad", &self.p_load) + .finish() + } +} diff --git a/tools/rt-app-v2/src/lib/cli.rs b/tools/rt-app-v2/src/lib/cli.rs new file mode 100644 index 0000000000000000000000000000000000000000..4f78a01dfce2a662ce5e426d9225079345277808 --- /dev/null +++ b/tools/rt-app-v2/src/lib/cli.rs @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2025, ARM Limited and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! CLI implementation for rt-app-v2. +//! +//! Implements subcommands which allow the user to select between available frontends: +//! - v2, which implements the new functionality. +//! - compatibility mode, in which rt-app-v2 behaves like the current C Version +//! (from command line arguments down to parsing the JSON config). +//! +//! Also implements the 'calibrate' subcommand which runs the cycle calibration step. + +use std::env; + +use anyhow::{Ok, Result, anyhow}; +use clap::{Parser, Subcommand}; + +use crate::{ + calibration::CalibrationArgs, + high_level::{ + frontend::Frontend, + frontend_compat::{FrontendCompat, RunCompatArgs}, + frontend_v2::{FrontendV2, RunArgs}, + }, + low_level::LowLevelConfig, +}; + +const COMPAT_EXE_NAME: &str = "rt-app"; + +#[derive(Debug, Subcommand)] +pub enum CliSub { + /// Selects the v2 frontend. + /// + /// This is the default if no subcommand is present, unless the program is called under + /// the 'rt-app' name, which selects the compatibility frontend. + Run(RunArgs), + /// Selects the compatibility frontend. + /// + /// This frontend implements the old rt-app behaviours relative to configuration + /// (including command line arguments). + /// + /// If the binary is called with the 'rt-app' name (e.g. via symlink), this frontend is + /// automatically selected. + RunCompat(RunCompatArgs), + /// Runs the calibration step. + /// + /// Runs the calibration step and prints out the derived calibration number. + Calibrate(CalibrationArgs), +} + +#[derive(Parser, Debug)] +pub struct Cli { + #[command(subcommand)] + pub subcommand: CliSub, +} + +impl Cli { + pub fn build_cli_args() -> Result { + let mut cli_args = env::args().collect::>(); + + /* + * std::env::current_exe() treats symlinks in a platform-specific way. + * + * On Linux, it follows the symlink and returns the path to the target executable. + * To work around this, try to use the first argv and fall back to std::env::current_exe(). + * + * See https://github.com/rust-lang/rust/issues/43617 + */ + let exe = match std::env::args().next() { + Some(ref s) if !s.is_empty() => { + let path = std::path::PathBuf::from(s); + if !path.is_file() { + std::env::current_exe().unwrap() + } else { + path + } + } + _ => { + let e = std::env::current_exe().unwrap(); + cli_args.push(e.to_string_lossy().to_string()); + + e + } + }; + let exe_name = exe.file_name().unwrap(); + + let subcommand_present = ["run", "run-compat", "calibrate", "help"] + .iter() + .any(|&x| cli_args.contains(&x.to_string())); + + assert!(!cli_args.is_empty()); + + if exe_name == COMPAT_EXE_NAME { + // Prepend the 'run-compat' subcommand to the command line. This mode is meant to provide + // full compatibility with older scripts and, as such, it does not expect subcommands. + if subcommand_present { + return Err(anyhow!( + "Executable name is '{}'. This mode is meant to provide full compatibility \ + with older scripts and, as such, it does not expect a subcommand to be present.", + COMPAT_EXE_NAME + )); + } + + cli_args.insert(1, "run-compat".to_string()); + } else if !subcommand_present { + // If no subcommand is present and the executable name is not 'rt-app', select the v2 + // frontend. + cli_args.insert(1, "run".to_string()); + } + + Ok(Cli::parse_from(cli_args)) + } +} + +/// Frontend dispatcher - selects which frontend to use based on the subcommand present in the CLI. +pub enum FrontendKind { + V2(FrontendV2), + Compat(FrontendCompat), +} + +impl Frontend for FrontendKind { + fn build(args: Cli) -> Result { + let c = match args.subcommand { + CliSub::Run(a) => FrontendKind::V2(FrontendV2::build(a)?), + CliSub::RunCompat(a) => FrontendKind::Compat(FrontendCompat::build(a)?), + CliSub::Calibrate(_) => { + return Err(anyhow!( + "The calibrate subcommand does not have an associated frontend" + )); + } + }; + + Ok(c) + } + + fn lower(self) -> Result { + let l = match self { + FrontendKind::V2(c) => c.lower()?, + FrontendKind::Compat(c) => c.lower()?, + }; + + Ok(l) + } +} diff --git a/tools/rt-app-v2/src/lib/high_level/frontend.rs b/tools/rt-app-v2/src/lib/high_level/frontend.rs new file mode 100644 index 0000000000000000000000000000000000000000..0ff136fa94a15327b916fb587eb91f6cf81214d1 --- /dev/null +++ b/tools/rt-app-v2/src/lib/high_level/frontend.rs @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2025, ARM Limited and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! High level configuration which allows for the existence of multiple frontends, all internally +//! converting to the same low level config representation. + +use anyhow::Result; + +pub trait Frontend +where + Self: Sized, +{ + fn build(args: T) -> Result; + fn lower(self) -> Result; +} + +/// Generates the definition and trait implementations for an Enum where each variant is a +/// different kind of high level config node. +macro_rules! define_config { + ($name:ident, {$($variant:ident),* $(,)?}) => { + #[derive(Debug, Serialize, Deserialize)] + #[serde(tag = "type")] + pub enum $name { + $( + $variant($variant) + ),* + } + + impl TryFrom<$name> for LowLevelNode { + type Error = anyhow::Error; + + fn try_from(high_level: $name) -> core::result::Result { + match high_level { + $( + $name::$variant(x) => { + x.try_into() + } + ),* + } + } + } + + #[derive(Debug, Serialize, Deserialize)] + #[serde(tag = "type")] + pub struct Config { + pub tasks: Vec<$name>, + pub calibration: CalibrationData, + } + + impl TryFrom for LowLevelConfig { + type Error = anyhow::Error; + + fn try_from(high_level: Config) -> core::result::Result { + let mut c = LowLevelConfig::new(); + + for node in high_level.tasks { + c.nodes.push(node.try_into()?); + } + + c.calibration = high_level.calibration; + Ok(c) + } + } + } +} +pub(crate) use define_config; + +macro_rules! define_frontend { + ($name:ident, $cli:ident) => { + pub struct $name { + config: Config, + cli_args: $cli, + } + + impl Frontend<$cli> for $name { + fn build(args: $cli) -> Result { + let c = json_utils::parse_json::(&args.file) + .with_context(|| "Could not build config from JSON".to_string())?; + + Ok($name { + config: c, + cli_args: args, + }) + } + + fn lower(self) -> Result { + let l = LowLevelConfig::try_from(self.config) + .with_context(|| "Could not lower to low level representation".to_string())?; + + if let Some(json_output) = &self.cli_args.dump_json { + json_utils::write_to_json::(&l, json_output).with_context( + || "Could not write low level representation to JSON".to_string(), + )?; + } + + Ok(l) + } + } + }; +} +pub(crate) use define_frontend; + +use crate::low_level::LowLevelConfig; diff --git a/tools/rt-app-v2/src/lib/high_level/frontend_compat.rs b/tools/rt-app-v2/src/lib/high_level/frontend_compat.rs new file mode 100644 index 0000000000000000000000000000000000000000..18e7a420b9e1ee53901358b53bb92aef0a3578a3 --- /dev/null +++ b/tools/rt-app-v2/src/lib/high_level/frontend_compat.rs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2025, ARM Limited and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Second frontend, implementing the old rt-app configuration style for backwards compatibility. + +use anyhow::{Context, Result}; +use clap::Parser; +use serde::{Deserialize, Serialize}; + +use super::frontend::{Frontend, define_frontend}; +use crate::{ + calibration::CalibrationData, + high_level::frontend::define_config, + json_utils, + low_level::{self, LowLevelConfig, LowLevelNode}, +}; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct RunCompatArgs { + /// Relative path to JSON config file + #[arg(index = 1)] + file: std::path::PathBuf, + + /// Relative path to destination file for dumping the resulting low-level config in JSON format + #[arg(short, long)] + dump_json: Option, +} + +/// rt-app 'mem' event. Called 'Mem' (vs. 'mem') for now so we can play nicely with serde and get +/// the parsing for free. This is just to show how multiple frontends can exist. +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub struct Mem { + pub count: usize, + + #[serde(default = "Mem::default_buf_size")] + pub buf_size: usize, +} + +impl TryFrom for LowLevelNode { + type Error = anyhow::Error; + + fn try_from(high_level: Mem) -> core::result::Result { + Ok(LowLevelNode::Memset(low_level::Memset { + count: high_level.count, + buf_size: high_level.buf_size, + })) + } +} + +impl Mem { + pub fn default_buf_size() -> usize { + 4 * 1024 * 1024 + } +} + +define_config! { + ConfigCompatNode, + { + Mem, + } +} + +define_frontend! {FrontendCompat, RunCompatArgs} diff --git a/tools/rt-app-v2/src/lib/high_level/frontend_v2.rs b/tools/rt-app-v2/src/lib/high_level/frontend_v2.rs new file mode 100644 index 0000000000000000000000000000000000000000..32f93a1ecf3bce6359bb056f12343c734a3a58db --- /dev/null +++ b/tools/rt-app-v2/src/lib/high_level/frontend_v2.rs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2025, ARM Limited and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Frontend implementation for the new high level configuration style. + +use anyhow::{Context, Result}; +use clap::Parser; +use serde::{Deserialize, Serialize}; + +use super::frontend::{Frontend, define_frontend}; +use crate::{ + calibration::CalibrationData, + high_level::frontend::define_config, + json_utils, + low_level::{self, LowLevelConfig, LowLevelNode}, +}; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct RunArgs { + /// Relative path to JSON config file + #[arg(index = 1)] + file: std::path::PathBuf, + + /// Relative path to destination file for dumping the resulting low-level config in JSON format + #[arg(short, long)] + dump_json: Option, +} + +const MEMSET_DEFAULT_BUFFER_SIZE: usize = 4 * 1024 * 1024; + +/// Equivalent to the current rt-app 'mem' event. +/// +/// If left unspecified in the JSON config, the buffer size will be set to +/// MEMSET_DEFAULT_BUFFER_SIZE during conversion to the corresponding low level config node. +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub struct Memset { + pub count: usize, + pub buf_size: Option, +} + +impl TryFrom for LowLevelNode { + type Error = anyhow::Error; + + fn try_from(high_level: Memset) -> core::result::Result { + Ok(LowLevelNode::Memset(low_level::Memset { + count: high_level.count, + buf_size: high_level.buf_size.unwrap_or(MEMSET_DEFAULT_BUFFER_SIZE), + })) + } +} + +define_config! { + ConfigV2Node, + { + Memset, + } +} + +define_frontend! {FrontendV2, RunArgs} diff --git a/tools/rt-app-v2/src/lib/high_level/mod.rs b/tools/rt-app-v2/src/lib/high_level/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..05c5484c1a611d3ec2551cbae4a9ec5a544f692a --- /dev/null +++ b/tools/rt-app-v2/src/lib/high_level/mod.rs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2025, ARM Limited and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod frontend; +pub mod frontend_compat; +pub mod frontend_v2; diff --git a/tools/rt-app-v2/src/lib/instr.rs b/tools/rt-app-v2/src/lib/instr.rs new file mode 100644 index 0000000000000000000000000000000000000000..a11478295fd48f6ac7dbfebff8b074a8616a7131 --- /dev/null +++ b/tools/rt-app-v2/src/lib/instr.rs @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2025, ARM Limited and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Backend implementation. +//! +//! A program is a list of executable instructions, lowered from the low level config +//! representation. + +use std::{cell::RefCell, cmp, fmt, ptr}; + +use anyhow::{Ok, Result}; + +use crate::{ + calibration::CalibrationData, + low_level::{self, LowLevelConfig, LowLevelNode}, +}; + +pub trait Run { + fn run(&self) -> Result<()>; +} + +#[derive(Debug)] +pub struct Memset { + count: usize, + buf: RefCell>, +} + +impl Run for Memset { + fn run(&self) -> Result<()> { + unsafe { + let vec_ptr = self.buf.borrow_mut().as_mut_ptr(); + let len = self.buf.borrow().len(); + let mut remaining_count = self.count; + + while remaining_count > 0 { + let count = cmp::min(remaining_count, len); + ptr::write_bytes(vec_ptr, 0xfe, count); + + remaining_count -= count; + } + } + + Ok(()) + } +} + +impl TryFrom for Memset { + type Error = anyhow::Error; + + fn try_from(node: low_level::Memset) -> core::result::Result { + let m = Memset { + count: node.count, + buf: RefCell::new(vec![0u8; node.buf_size]), + }; + + Ok(m) + } +} + +impl fmt::Display for Memset { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Memset") + .field("count", &self.count) + .field("buf_size", &self.buf.borrow().len()) + .finish() + } +} + +#[derive(Debug)] +pub struct SubProgram { + instructions: Vec, +} + +impl Run for SubProgram { + fn run(&self) -> Result<()> { + for instr in &self.instructions { + instr.run()?; + } + + Ok(()) + } +} + +impl TryFrom for SubProgram { + type Error = anyhow::Error; + + fn try_from(group: low_level::SubProgram) -> core::result::Result { + let mut g = SubProgram { + instructions: vec![], + }; + + for n in group.nodes { + g.instructions.push(n.try_into()?); + } + + Ok(g) + } +} + +impl fmt::Display for SubProgram { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.instructions.iter()).finish() + } +} + +/// Generates the definition and trait implementations for an Enum where each variant is a +/// particular kind of instruction. +macro_rules! define_instr { + ($name:ident, {$($variant:ident),* $(,)?}) => { + #[derive(Debug)] + pub enum $name { + $( + $variant($variant) + ),* + } + + impl Run for $name { + fn run(&self) -> Result<()> { + println!("Running {self}"); + match self { + $( + $name::$variant(instr) => instr.run()? + ),* + } + + Ok(()) + } + } + + impl TryFrom for $name { + type Error = anyhow::Error; + + fn try_from(node: LowLevelNode) -> core::result::Result { + let i = match node { + $( + LowLevelNode::$variant(instr) => $name::$variant(instr.try_into()?) + ),* + }; + + Ok(i) + } + } + + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $( + $name::$variant(t) => write!(f, "{}", t) + ),* + } + } + } + + } +} + +define_instr! { + Instruction, + { + Memset, + SubProgram, + } +} + +/// Executable program, lowered from a LowLevelConfig. +#[derive(Debug)] +pub struct Program { + pub instructions: Vec, + pub calibration: CalibrationData, +} + +impl Program { + pub fn new() -> Program { + Program { + instructions: vec![], + calibration: CalibrationData::new(), + } + } +} + +impl Default for Program { + fn default() -> Self { + Self::new() + } +} + +impl Run for Program { + fn run(&self) -> Result<()> { + println!("Running with calibration: {}", self.calibration); + for instr in &self.instructions { + instr.run()? + } + + Ok(()) + } +} + +impl TryFrom for Program { + type Error = anyhow::Error; + + fn try_from(config: LowLevelConfig) -> core::result::Result { + let mut p = Program::new(); + + for node in config.nodes { + p.instructions.push(node.try_into()?); + } + + p.calibration = config.calibration; + Ok(p) + } +} diff --git a/tools/rt-app-v2/src/lib/json_utils.rs b/tools/rt-app-v2/src/lib/json_utils.rs new file mode 100644 index 0000000000000000000000000000000000000000..2f351f28d78ffa56d5054f463daf8c02f2a8dbb9 --- /dev/null +++ b/tools/rt-app-v2/src/lib/json_utils.rs @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2025, ARM Limited and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use anyhow::Context; + +pub fn parse_json(file: &std::path::PathBuf) -> core::result::Result +where + for<'a> T: serde::Deserialize<'a>, +{ + let data = + std::fs::read_to_string(file).with_context(|| format!("Could not read from `{file:?}`"))?; + + let t: T = serde_json::from_str(&data).with_context(|| "Failed to parse JSON".to_string())?; + + Ok(t) +} + +pub fn write_to_json( + data: &T, + file: &std::path::PathBuf, +) -> core::result::Result<(), anyhow::Error> +where + T: serde::Serialize, +{ + let json_data = serde_json::to_string(data) + .with_context(|| "Failed to serialize to JSON string".to_string())?; + + std::fs::write(file, json_data).with_context(|| format!("Failed to write to `{file:?}`")) +} diff --git a/tools/rt-app-v2/src/lib/lib.rs b/tools/rt-app-v2/src/lib/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..2c4579a74599b8a6de088d4fe51daf92beb233fa --- /dev/null +++ b/tools/rt-app-v2/src/lib/lib.rs @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2025, ARM Limited and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod affinity_utils; +pub mod calibration; +pub mod cli; +pub mod high_level; +pub mod instr; +pub mod json_utils; +pub mod low_level; diff --git a/tools/rt-app-v2/src/lib/low_level.rs b/tools/rt-app-v2/src/lib/low_level.rs new file mode 100644 index 0000000000000000000000000000000000000000..1036da456ca3b94c14f5e59d362877e5df23f118 --- /dev/null +++ b/tools/rt-app-v2/src/lib/low_level.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2025, ARM Limited and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Low level internal representation of an rt-app configuration. +//! +//! This is intended as a simple imperative language to be used as target for a frontend which +//! takes a high level configuration as input. +//! +//! This representation is lowered to a program composed of nearly identical instructions which +//! can be executed. + +use serde::{Deserialize, Serialize}; + +use crate::calibration::CalibrationData; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Memset { + pub count: usize, + pub buf_size: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SubProgram { + pub nodes: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum LowLevelNode { + Memset(Memset), + SubProgram(SubProgram), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LowLevelConfig { + pub nodes: Vec, + pub calibration: CalibrationData, +} + +impl LowLevelConfig { + pub fn new() -> LowLevelConfig { + LowLevelConfig { + nodes: vec![], + calibration: CalibrationData::new(), + } + } +} + +impl Default for LowLevelConfig { + fn default() -> Self { + Self::new() + } +} diff --git a/tools/rt-app-v2/src/main.rs b/tools/rt-app-v2/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..ea1f5e6123d7808efc8a5322b6bbd77b3509b28d --- /dev/null +++ b/tools/rt-app-v2/src/main.rs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2025, ARM Limited and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::str::FromStr; + +use anyhow::{Context, Ok, Result}; +use lib::{ + calibration::CalibrationData, + cli::{Cli, CliSub, FrontendKind}, + high_level::frontend::Frontend, + instr::{Program, Run}, + json_utils, +}; + +fn main() -> Result<()> { + let cli = Cli::build_cli_args().with_context(|| "Could not build command line".to_string())?; + + match cli.subcommand { + CliSub::Calibrate(a) => { + let cal = + CalibrationData::build(&a).with_context(|| "Calibration step failed".to_string())?; + + println!("Calibration step completed: {cal}"); + + let f = std::path::PathBuf::from_str("calibration_data.json") + .with_context(|| "Could not parse string to file path".to_string())?; + + json_utils::write_to_json(&cal, &f) + .with_context(|| "Could not write calibration data to JSON".to_string())?; + } + _ => { + let f = + FrontendKind::build(cli).with_context(|| "Could not build frontend".to_string())?; + + let l = f + .lower() + .with_context(|| "Lowering pass failed".to_string())?; + + let p = Program::try_from(l).with_context(|| { + "Could not lower low level representation to runnable program".to_string() + })?; + + p.run() + .with_context(|| "Program encountered error during execution".to_string())?; + } + }; + + Ok(()) +}