From c997790bb76ac6e8a272aafc6b6d22d751760b50 Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Wed, 9 Sep 2026 14:56:44 +0200 Subject: [PATCH 01/31] test(ctrace): establish multi-source implementation baseline --- .github/workflows/ctrace.yml | 32 + tools/ctrace/CMakeLists.txt | 20 + .../ctrace/docs/multicore-multisource-plan.md | 1287 +++++++++++++++++ tools/ctrace/docs/todo.md | 31 +- tools/ctrace/src/decode/OpenCsdItmDecoder.cpp | 11 +- tools/ctrace/src/tracerun/CtraceRunMeta.cpp | 17 +- tools/ctrace/test/data/.gitattributes | 3 + .../expected/Blinky+Arm.SWO.traceanalysis.xml | 904 ++++++++++++ .../expected/Blinky+Arm.ctf/metadata | 310 ++++ .../expected/Blinky+Arm.ctf/stream_0 | Bin 0 -> 65536 bytes tools/ctrace/test/data/README.md | 27 +- .../test/data/TB-Trace/Blinky+Arm.TB.raw | Bin 0 -> 4096 bytes .../data/TB-Trace/Blinky+Arm.ctrace-run.yml | 150 ++ tools/ctrace/test/data/TB-Trace/README.md | 98 ++ .../test/data/TB-Trace/regenerate_tb_trace.py | 290 ++++ .../test/data/TB-Trace/split_tb_trace.py | 170 +++ .../test/integration/src/CtraceIntegTests.cpp | 149 +- .../src/decode/OpenCsdItmDecoderTests.cpp | 9 + .../unit/src/tracerun/CtraceRunMetaTests.cpp | 31 + .../tracerun/TraceRunConfigReaderTests.cpp | 6 +- 20 files changed, 3514 insertions(+), 31 deletions(-) create mode 100644 tools/ctrace/docs/multicore-multisource-plan.md create mode 100644 tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.SWO.traceanalysis.xml create mode 100644 tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.ctf/metadata create mode 100644 tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.ctf/stream_0 create mode 100644 tools/ctrace/test/data/TB-Trace/Blinky+Arm.TB.raw create mode 100644 tools/ctrace/test/data/TB-Trace/Blinky+Arm.ctrace-run.yml create mode 100644 tools/ctrace/test/data/TB-Trace/README.md create mode 100755 tools/ctrace/test/data/TB-Trace/regenerate_tb_trace.py create mode 100755 tools/ctrace/test/data/TB-Trace/split_tb_trace.py diff --git a/.github/workflows/ctrace.yml b/.github/workflows/ctrace.yml index 60cb78201..73ec33ea5 100644 --- a/.github/workflows/ctrace.yml +++ b/.github/workflows/ctrace.yml @@ -310,6 +310,7 @@ jobs: working-directory: ./build/tools/ctrace - name: Generate coverage report + id: generate_coverage_report run: | lcov-1.15/bin/lcov --rc lcov_branch_coverage=1 --rc geninfo_no_exception_branch=1 -c --directory . --output-file full_coverage.info lcov-1.15/bin/lcov --rc lcov_branch_coverage=1 --rc geninfo_no_exception_branch=1 -e full_coverage.info '*/tools/ctrace/src/*' -o coverage_ctrace.info @@ -318,6 +319,36 @@ jobs: lcov-1.15/bin/genhtml coverage_ctrace.info --output-directory coverage_ctrace --branch-coverage working-directory: ./build/tools/ctrace + - name: Enforce 100% ctrace source-line coverage + run: | + awk ' + /^SF:/ { + source = substr($0, 4) + files += 1 + next + } + /^DA:/ { + split(substr($0, 4), fields, ",") + lines += 1 + if ((fields[2] + 0) == 0) { + printf "uncovered source line: %s:%s\n", source, fields[1] + missed += 1 + } + } + END { + if (files == 0 || lines == 0) { + print "coverage gate failed: no ctrace source-line records found" > "/dev/stderr" + exit 2 + } + printf "ctrace source-line coverage: %d/%d lines\n", lines - missed, lines + if (missed != 0) { + printf "coverage gate failed: %d source lines are uncovered\n", missed > "/dev/stderr" + exit 1 + } + } + ' coverage_ctrace_codecov.info + working-directory: ./build/tools/ctrace + - name: Upload Report to Codecov if: ${{ !github.event.repository.private }} uses: Wandalen/wretry.action@e68c23e6309f2871ca8ae4763e7629b9c258e1ea # v3.8.0 @@ -333,6 +364,7 @@ jobs: attempt_delay: 5000 - name: Archive coverage report + if: ${{ always() && steps.generate_coverage_report.outcome == 'success' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-report-ctrace diff --git a/tools/ctrace/CMakeLists.txt b/tools/ctrace/CMakeLists.txt index 375e9024a..50ded0b1c 100644 --- a/tools/ctrace/CMakeLists.txt +++ b/tools/ctrace/CMakeLists.txt @@ -39,6 +39,26 @@ target_link_libraries(ctrace PRIVATE ctracelib ) +# GCC/gcov attributes optional NRVO exception-cleanup blocks to otherwise +# covered closing source lines. Disable that elision only for ctrace coverage +# objects; normal builds, tests, and external dependencies remain unchanged. +if(COVERAGE AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(CTRACE_COVERAGE_TARGETS + ctrace-model + ctrace-cli + ctrace-trace-run + ctrace-diagnostics + ctrace-decode + ctrace-output + ctrace-control + ctracelib + ctrace + ) + foreach(target IN LISTS CTRACE_COVERAGE_TARGETS) + target_compile_options(${target} PRIVATE -fno-elide-constructors) + endforeach() +endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")) diff --git a/tools/ctrace/docs/multicore-multisource-plan.md b/tools/ctrace/docs/multicore-multisource-plan.md new file mode 100644 index 000000000..91277d395 --- /dev/null +++ b/tools/ctrace/docs/multicore-multisource-plan.md @@ -0,0 +1,1287 @@ +# ctrace Multi-Core and Multi-Source Plan + +## Goal + +Use one OpenCSD `DecodeTree` based input architecture for both the existing unformatted SWO stream and formatted +CoreSight captures containing any number of Trace Bus IDs. One devtools branch and pull request shall contain the +complete change. + +The change must preserve all current single-core output and extend it to every currently supported ITM-carried trace +type on every configured processor: + +- ITM software trace +- DWT data, address, and match trace +- exception trace +- periodic PC samples and sleep indications +- DWT event counters and PMU trace-on-overflow +- local and global timestamps +- synchronization, overflow, decoder errors, and data-loss events + +Non-ITM streams, including instruction trace, may coexist in a formatted input. The current trace-run schema does not +describe their protocol, so ctrace observes their formatter IDs, diagnoses each unsupported ID once, and skips their +payload without guessing a protocol. This is reported as a non-failing Warning so supported routes still complete, +matching today's behavior for unsupported side channels. ETM, ETE, PTM, and MTB instruction decoding and explicit +protocol routing are deferred. + +## Restrictions + +- Valid normal CoreSight trace source IDs are `0x01..0x6F` (`1..111`). Although `ITM_TCR.TraceBusID` is a 7-bit + field, not every representable value is a configurable trace source ID. ID `0x00` denotes the NULL source or that + multi-source trace is not in use. IDs `0x70..0x7F` are reserved or have formatter-control meanings, including + trigger ID `0x7D`; `0x7F` is prohibited because it conflicts with synchronization encodings. Ctrace already + enforces the valid normal source-ID range and deterministically rejects configured values `0x70..0x7F`; pyTS + currently validates only the field width and must be restricted to the same range for processor source assignment. + That producer correction is tracked outside this devtools PR and does not justify relaxing ctrace's input check. + + | Value | Architectural meaning | + | :--- | :--- | + | `0x00` | No multi-source trace in `ITM_TCR`; NULL source in the formatter | + | `0x01..0x6F` | Valid normal trace source IDs | + | `0x70..0x7A` | Reserved | + | `0x7B` | Reserved in CoreSight v2; formatter flush response in CoreSight v3 | + | `0x7C` | Reserved | + | `0x7D` | Formatter trigger indication; not a configurable normal source | + | `0x7E` | Reserved | + | `0x7F` | Prohibited because it conflicts with formatter synchronization encodings | + + Sources: [Armv7-M Architecture Reference Manual, DDI0403E.e][armv7-m-arm], + section C1.7.6 and printed pages C1-716 and C1-718; + [Armv8-M Architecture Reference Manual, DDI0553](https://developer.arm.com/documentation/ddi0553/latest/), + `ITM_TCR.TraceBusID`; + [CoreSight Architecture Specification v2.0, IHI0029D][coresight-v2], + section D4.2.4 on printed page D4-132; and + [CoreSight Architecture Specification v3.0, IHI0029F][coresight-v3], + section D4.2.4. + +- A Trace Bus ID identifies one ATB trace source, not one processor or one semantic trace event. DWT and PMU packets + are carried by their processor's ITM stream and share its ID. A separate ETM source on the same processor requires + another ID. +- An unformatted single-protocol stream contains no Trace Bus ID. OpenCSD deliberately assigns channel ID `0` to the + decoder in `OCSD_TRC_SRC_SINGLE` mode; this value is internal and must never be interpreted as an architectural + source ID from the capture. +- The formatter may be enabled for a single source. Neither the number of configured source IDs, the sink type, nor + the raw filename determines the input format. Multiple interleaved sources require formatting, but one source may + be captured either formatted or unformatted. +- This PR decodes exactly one active raw input per `ctrace-run.yml`. A legacy file without explicit input metadata + keeps only its existing `*.SWO.raw` compatibility path active. With explicit input metadata, discovery must resolve + exactly one matching raw file; zero or multiple candidates are fatal before a decoder or output artifact is + created. The provisional root-level `trace-format` value applies globally to the trace-run group and defaults to + `unformatted` when absent or null. The normalized model must retain whether a non-null declaration was supplied: + an absent or null field uses the legacy SWO-only discovery rule, while an explicit `unformatted` or `formatted` + value opts SWO/TB candidates into the new input contract. Framing remains an internal ctrace setting and globally + defaults to memory-aligned CoreSight frames for formatted input; no public `trace-framing` YAML field is introduced + in this PR. A future specification may refine both assumptions when explicit input identity is added. +- The OpenCSD `DecodeTree` implementation keeps its current error logger and live-tree registry in static process + state. Ctrace therefore creates and feeds only one tree at a time. The tree-session wrapper installs the ctrace + logger before tree creation, destroys the tree before that logger, and restores the previously installed OpenCSD + logger on every exit path. Parallel tree execution is outside this change. + +## Evidence from the TB fixture + +`tools/ctrace/test/data/TB-Trace/Blinky+Arm.TB.raw` is a 4096-byte reconstructed test fixture containing 256 +memory-aligned CoreSight formatter frames. It contains neither FSYNC nor HSYNC framing. An independent Python +deformatter produces: + +| Formatter ID | Payload | Observed trace | +| :--- | ---: | :--- | +| `0x01` | 1488 bytes | CM4 address range (`0x0810...`), PC samples, and exceptions | +| `0x02` | 2093 bytes | CM7 address range (`0x0800...`), PC samples, exceptions, and DWT comparator 0 values | +| `0x00` | 7 bytes | Reserved end padding; not a trace source | + +Both source payloads begin with an ITM hardware synchronization sequence, and no payload precedes the first formatter +ID. A recorded countercheck with the current single-stream decoder produced 213 semantic rows on ID `0x01` and 312 +on ID `0x02` without decoder errors. The fixture preserves the usable real hardware payload and formatter +interleaving from the legacy capture, but swaps the IDs to the current `CM4 = 1`, `CM7 = 2` assignment and adds +synthetic leading synchronization. Its transformations, source/output hashes, and independent deformatter are +documented beside the checked-in canonical fixture in `tools/ctrace/test/data/TB-Trace/README.md`. + +The fixture is useful for validating formatter demultiplexing, routing, and semantic output on both processors. It is +not a contract for how pyOCD exports trace-buffer captures. pyOCD owns trace-buffer readout, wrap handling, valid-data +boundaries, and chronological linearization and must provide ctrace with a complete, decoder-ready raw trace file. + +The original pyTS 0.1.0 `ctrace-run` file neither records the two stream IDs nor matches the captured setup. The test +copy follows the current per-processor and generated-reference structure and is manually aligned with the observed +capture. Missing capture provenance cannot be reconstructed from the trace bytes. + +## Specification gaps confirmed by the fixture + +- `stream` associates a generated reference with an ATB ID, but no node explicitly lists the raw input files and + channels belonging to the trace-run group. The provisional root `trace-format` field supplies their common format; + ctrace uses an internal memory-aligned framing default for formatted input. +- The valid architectural source-ID range `0x01..0x6F` and the distinction from special formatter IDs are not stated + in the trace-run schema. +- The standard pyTS workflow preserves disabled and otherwise unmodified user entries in `ctrace-setup`; effective + generated stream assignments are carried by `ctrace-refs`. `ctrace-refs.stream` is therefore the only routing + authority. Ctrace may tolerate an `itm.atbid` enrichment produced by a non-standard/direct pyTS generator call, but + ignores it for routing and does not require it to agree with the generated references. Group-level input format and + generated route metadata follow the same separation between copied user intent and generated effective + configuration. +- The trace-run specification defines timestamp references as `type: itm`, while current pyTS emits `type: dwt`. + Ctrace needs the transitional normalization rule below until pyTS emits the normative form. +- Per-processor timestamp clocks are representable, but the required behavior for independent clock domains and the + absence of global time synchronization in combined CTF output is not defined. + +## Input contract + +The raw bytes do not reliably identify whether they are an unformatted protocol stream or CoreSight frames. Ctrace +must not guess from synchronization patterns or the number of configured Trace Bus IDs. + +One trace-run configuration defines a group of associated raw inputs. For this PR, the existing filename heuristic +selects exactly one active input from the group. The following assumptions are deliberately provisional until the +CMSIS-Toolbox trace specification defines an explicit input model: + +- optional root-level `trace-format: unformatted | formatted` applies globally to every trace file in the group; +- missing or null `trace-format` silently selects the global default `unformatted` and remains undeclared for legacy + candidate selection; +- framing is not exposed as a `ctrace-run.yml` field; ctrace stores one internal global framing value and defaults it + to memory-aligned CoreSight frames whenever `trace-format` is `formatted`; +- FSYNC and HSYNC input are outside this PR and may later require a specified public framing field; +- one valid Trace Bus ID identifies each supported formatted ITM source; a processor `[/]itm` reference is the + preferred authoritative route anchor, with the narrowly constrained current-pyTS feature-reference fallback below. + +The absent-or-null default preserves the existing legacy `*.SWO.raw` path; it does not make a coexisting legacy TB or +ER side file a second active input. An explicit non-null `trace-format` opts the eligible SWO/TB candidates into the +new global input contract. Discovery must then resolve exactly one matching raw file; zero or multiple candidates are +fatal before decoder or output creation. + +An explicit channel/file list is future discovery metadata. It may replace the filename heuristic and refine the +current global assumptions. Route identity then expands from ATB ID to `(input identity, ATB ID)`. + +### Provisional `ctrace-run.yml` extension + +Decision record for this devtools PR: the root `trace-format` field, its absent/null/explicit behavior, and the internal +memory-aligned framing default are ctrace-private working assumptions. They are not normative CMSIS-Toolbox schema, +and this PR neither changes a capture producer nor claims that pyTS or pyOCD emits them. The reconstructed TB fixture +is explicitly and manually annotated consumer test input. Phase 0 freezes this boundary and records separate +specification/producer follow-ups; those external changes are not gates for the devtools phases. A future normative +input model may require a deliberate reader migration while the legacy absent/null behavior remains compatible. + +The external work is recorded here with an explicit owner boundary; no external issue or pull request is created by +this devtools change: + +| Owner repository | Follow-up | Relationship to this PR | +| :--- | :--- | :--- | +| [CMSIS-Toolbox](https://github.com/Open-CMSIS-Pack/cmsis-toolbox) | Specify input identity, format, framing, and the ownership of effective capture metadata in `ctrace-run.yml`. | May replace the private field through a deliberate reader migration; not a devtools gate. | +| [pyTS](https://github.com/Open-CMSIS-Pack/pyTS) | Restrict allocated `ITM_TCR.TraceBusID` values to `1..111`; after the toolbox contract exists, emit its normative effective-format metadata. | Ctrace remains strict now; producer alignment is tracked outside this PR. | +| [pyOCD](https://github.com/pyocd/pyOCD) or the invoking capture integration | Export a decoder-ready, chronologically linearized trace-buffer artifact and communicate the effective formatter state through the future toolbox contract. | The manually annotated TB fixture supplies only consumer-side evidence in this PR. | + +Within that boundary, this PR introduces one provisional decoder-control field directly below the `ctrace-run` root: + +```yml +ctrace-run: + trace-format: formatted +``` + +- `trace-format` + - Values: `unformatted`, `formatted`. + - Default: `unformatted` when the field is absent or null; no diagnostic is emitted for this default, and null does + not count as an explicit format declaration during discovery. + - Effect: selects an OpenCSD `SINGLE` input or `FRAME_FORMATTED` deformatter. + +The internal framing value is `memory-aligned` and maps to `OCSD_DFRMTR_FRAME_MEM_ALIGN`. A formatted file therefore +begins at a complete 16-byte CoreSight formatter-frame boundary, contains no formatter FSYNC/HSYNC words, and has a +size that is a multiple of 16 bytes. This internal default is deliberately not parsed from or emitted to YAML. If a +future input requires another framing mode, that mode must first be added to the public trace contract. + +`trace-format` describes the effective bytes in the capture artifact, not target capability or copied user intent. +It must not be copied into processor setup entries or repeated on feature references. The reader retains the optional +declaration; the normalized descriptor stores both its resolved value and whether the declaration was explicit, plus +the internal framing default for decoder construction. These are one configuration source, not competing settings. +The capture producer that knows the effective formatter state must eventually emit equivalent format information; +ctrace cannot reconstruct it from copied target configuration. Producer emission and schema standardization are +deliberately outside this devtools PR. + +Concretely, discovery considers `.SWO.raw`, `.TB.raw`, and the specification-defined +`.TB_.raw` form as decoder candidates. This PR still accepts exactly one active file, including +at most one named Trace Buffer; simultaneous inputs need the future explicit file-association model. Without an +explicit format field, only the SWO name is eligible for the legacy compatibility path. A coexisting legacy TB or +`TB_` file retains today's non-failing "unsupported channel" Warning and does not +invalidate the selected SWO conversion or its completed output. With explicit metadata, either filename is eligible +and zero or more than one existing candidate is fatal before output creation. `.ER.raw` remains a +discovered but unsupported side input in this PR: emit the same non-failing Warning and exclude it from the active +decoder-candidate count. If an eligible SWO/TB input exists, its conversion still completes; an ER-only group reports +the Warning and then fails because no eligible input remains. This rule removes ambiguity when SWO and TB artifacts +coexist and is replaced, not extended, by the future explicit channel/file list. + +The reconstructed TB fixture deliberately retains the existing generated `ctrace-setup` and `ctrace-refs` content +needed by this PR: + +- `ctrace-setup[].pname` plus `ctrace-refs[].pname` and `stream` bind Trace Bus IDs 1 and 2 to CM4 and CM7; +- `timestamps.clock` and `timestamps.itm-prescaler` supply the independent time configuration for each processor; +- `type`, `source`, `address`, `data-type`, and `size` retain the source metadata needed by CSV filtering and + per-stream CTF metadata; +- reference `info`, `warning`, and `error` diagnostics retain their existing handling. A diagnostic annotation does + not by itself decide whether the readable fields are usable; the normalized ctrace model validates those fields + independently before it creates a route. + +Other generated setup and register fields remain in the realistic fixture but do not gain new semantics in this PR. +`unformatted` selects an OpenCSD `SINGLE` input whose bytes carry no Trace Bus ID; `formatted` selects the CoreSight +frame deformatter and preserves the IDs carried by the frames. + +Trace-buffer implementation details such as RAM bounds, sink write position, wrap state, and chronological +linearization remain internal to pyOCD. Ctrace consumes the resulting decoder-ready file and does not reconstruct a +trace-buffer image. + +A `single` input always creates exactly one synthetic logical ITM route with no ATB ID because its raw bytes carry no +ID. Legacy configurations therefore remain valid even when `ctrace-refs` is empty or no reference contains +`stream`. With no matching processor metadata, documented time/label defaults apply. One unambiguous binding is +attached directly; multiple candidates may be merged only when every processor-specific value consumed by the +requested operation is equivalent. Ambiguous processor identity or prescaler metadata prevents decoding because the +bytes cannot choose the required interpretation. Clock, source-description, and label conflicts affect only an +output backend that consumes them; they do not invalidate an otherwise viable independent operation. A configured +architectural ID may help resolve metadata but is not copied onto the synthetic route. OpenCSD reports its transport +channel as ID `0`. A formatted input instead retains the architectural IDs found in its frames, also when it contains +only one source. + +The current `*.cbuild-run.yml` processor list is sufficient for pyTS to select processor implementations but does not +describe the actual formatter state or raw capture path. Ctrace must not read it as a second and potentially +conflicting source of capture metadata. The component that knows the effective debugger and sink configuration must +record that resolved information in `*.ctrace-run.yml` before decoding. + +`trace-format` is a ctrace-private provisional global ctrace-run extension used by this implementation and manually +annotated fixture. It requires agreement in the CMSIS-Toolbox trace specification before any producer can emit it as +a normative field. Internal framing remains memory-aligned and is not a YAML extension. Producer changes and the +exact future node for explicit channel/file association remain outside this PR and may refine these working +assumptions. Existing `*.SWO.raw` inputs without the new metadata remain compatible and default to the current +unformatted single-ITM interpretation. Formatted input without sufficient routing metadata is rejected with a clear +diagnostic instead of being decoded heuristically. + +## End-to-end contract + +Ctrace resolves the raw input and `ctrace-run` together. File discovery identifies the capture artifact; normalized +trace-run metadata identifies how it must be decoded and how every resulting stream is described to the outputs. +For correctly generated pyTS input, disabled setup entries have no effective references and therefore create no +routes. Stale references without a matching effective processor route are rejected by normalization rather than +silently treated as valid input. + +The `info`, `warning`, and `error` lists attached to reference nodes describe producer diagnostics. The current +schema does not define these fields on `ctrace-setup` entries. Ctrace forwards every diagnostic found on a reference +it already reads, preserving its severity, but does not assume that an `error` annotation makes every scalar on that +reference false. It then validates the readable data through its own normalized model. A complete, consistent model +may continue despite the reported producer error. Missing, contradictory, out-of-range, or ambiguous +routing-critical information without a documented fallback is a fatal ctrace configuration error. Output-specific +requirements are validated separately after reading and normalization. Missing, null, malformed, zero, or ambiguous +clock metadata prevents only CTF generation and emits an Error; no frequency is invented. Validation-only and CSV +decoding remain available. With `--all`, CSV still completes while CTF is not created and the Error keeps the command +status non-zero. The decoder lifecycle starts after the input descriptor passes input-level validation and at least +one requested operation remains viable. + +- A legacy `*.SWO.raw` preserves the compatibility rule and creates a `SINGLE` ITM input. Differing + processor-specific settings require one unambiguous effective setup. OpenCSD reports internal ID `0`; CSV leaves + `stream` empty and CTF writes `stream_0`. +- An eligible `*.SWO.raw`, `*.TB.raw`, or `*.TB_.raw` with explicit `trace-format: unformatted` binds one synthetic + protocol/processor route and creates a `SINGLE` decoder. The filename and physical sink do not override the + declaration. Transport ID remains `0`; explicit metadata supplies processor clock, prescaler, and labels. +- An eligible `*.SWO.raw`, `*.TB.raw`, or `*.TB_.raw` with explicit `trace-format: formatted` creates a + `FRAME_FORMATTED` input + with the internal memory-aligned framing default and one ITM protocol decoder per supported normalized route. The + filename and source count do not override the declaration. Supported formatter IDs survive semantic decoding and + both outputs; other IDs are diagnosed and skipped. +- A `*.TB.raw` or `*.TB_.raw` without input metadata is rejected as ambiguous before decoder or output + creation. The heuristic may discover it, but the bytes and existing references do not determine formatting in the + general case. + +Formatted CoreSight ID `0` is NULL/padding/control data and is consumed by the deformatter without creating a +protocol route or semantic event. It must not create a CSV stream value, a CTF stream class or file of its own, or a +Trace Compass lane. + +The normalized model contains one input descriptor and explicit protocol routes, conceptually: + +```cpp +struct TraceInputRoute { + RouteId id; + std::optional traceBusId; // present only for formatted CoreSight input + TraceProtocol protocol; + std::optional processorName; +}; +``` + +A configured ATB value may participate in `SINGLE` metadata normalization, but it is discarded before this route is +created and never becomes transport, CSV, or CTF identity. + +For formatted input, normalization builds exactly one ITM decoder route per unique valid `stream`. An effective +`[/]itm` reference with `type: itm` is the preferred authoritative processor-ITM anchor; `source` on it denotes +an ITM stimulus channel, not another route. If an anchor is present, malformed fields or a conflict with another +reference are fatal and cannot be hidden by the fallback below. + +Current pyTS can emit a stream-bearing feature reference without an implicit processor-ITM anchor for an unnamed +single-processor setup. When no anchor exists for a stream, ctrace may therefore establish the same ITM route from a +unique, internally consistent group of generated feature references, but only for supported ITM-carried setting +pairs: `data#*` with `type: dwt`, `timestamps` with `type: itm` or the transitional `type: dwt`, `exceptions` with +`type: exception`, `events#*` with `type: event` or `type: pmu`, `pcsampling` with `type: pcsample`, and +`synchronization` with `type: dwt`. Streamless references, instruction/trace-halt conditions, `timesync`, unsupported +types, and unknown path/type pairs never create a fallback route. This compatibility rule is sufficient to associate +the decoder consumer with pyTS' declared stream; it does not prove that a capture producer programmed +`ITM_TCR.TraceBusID` correctly. Observed formatter IDs still have to match a configured route or follow the +unsupported-ID policy. + +All references for a stream must normalize to one compatible `(ITM, pname)` binding, and one bound processor ITM +binding must not map to multiple streams. Distinct explicitly unbound routes do not acquire a shared processor binding +merely because both omit `pname`. With multiple active processor setups, `pname` must identify the matching setup; +with one setup, an omitted `pname` may use the existing unique-setup inference. An absent setup does not prevent +decoding an otherwise unambiguous route, but processor-specific metadata then uses documented defaults. Compatible +DWT, exception, event, PMU, timestamp, PC-sampling, overflow, and global-timestamp references describe content on the +route and do not create additional routes. The reader retains common binding metadata and diagnostics for all +supported reference types (`dwt`, `event`, `exception`, `itm`, `pmu`, `overflow`, `pcsample`, and `global_ts`). +Repeated consistent stream IDs are valid; route uniqueness does not mean that a stream number may occur only once in +`ctrace-refs`. `ctrace-refs.stream` is authoritative. An optional copied or enriched `ctrace-setup.itm.atbid` is +ignored for routing and cannot repair or invalidate the generated binding. + +The normative timestamp reference uses `type: itm`; current pyTS emits `type: dwt`. Ctrace temporarily accepts the +second spelling only when the `ctrace-ref` leaf is `timestamps`, then normalizes both to the same local timestamp +configuration of the ITM route. A timestamp reference never creates a second route or a DWT comparator source, but +may establish the constrained no-anchor fallback above. Any other spelling or a conflicting `(pname, stream)` binding +is fatal. Tests retain both accepted forms until pyTS matches the normative spelling. + +CoreSight routing identity, CTF identity, and filesystem naming are separate concepts: + +- the Trace Bus ID comes from the formatted CoreSight transport and is resolved through `ctrace-refs`; +- the CTF stream-class ID is a backend-assigned numeric identifier referenced by the packet header, the matching + `stream { id = ...; }` declaration, and each event's `stream_id`; +- a data-stream filename is not referenced by CTF metadata. `stream_` is only a ctrace naming convention. + +[CTF 1.8][ctf-spec], section 5, +requires neither stream-class ID `0` nor contiguous IDs. Its +[filesystem representation][ctf-spec], section 2, +does not define data-stream filenames. For this PR, ctrace deliberately uses the formatted Trace Bus ID as the CTF +stream-class ID and assigns CTF ID `0` to an unformatted `SINGLE` input. This direct mapping keeps diagnostics and +artifacts simple, but it is an explicit backend choice rather than a CTF requirement. The file is named +`stream_`; no logic may infer a source identity from that filename alone. + +Ctrace reserves the following CTF-local stream-class ID namespace: + +| CTF stream-class ID | ctrace assignment | +| :--- | :--- | +| `0x00000000` | Unformatted `SINGLE` input | +| `0x00000001..0x0000006F` | Formatted CoreSight source; equal to its Trace Bus ID | +| `0x00000070..0x0000007F` | Unused, matching the reserved/special CoreSight 7-bit values | +| `0x00000080..0x000000FF` | CMSIS Event Recorder instances `0..127`, using `0x80 + instance` | +| `0x00000100..0xFFFFFFFF` | Other backend-only streams, allocated deterministically | + +CMSIS Event Recorder has no CoreSight Trace Bus ID. `Event Recorder` maps explicitly to CTF stream-class ID +`0x80 + n` for `n = 0..127`; filenames render that numeric ID in decimal, so `Event Recorder<0>` is written to +`stream_128`, `Event Recorder<1>` to `stream_129`, and so on. The assigned ID is local to the CTF bundle and is not +written back into `ctrace-run.yml` or exposed as a Trace Bus ID. Metadata and the source descriptor remain +authoritative for the source kind. Event Recorder decoding itself remains outside this PR. If a later feature needs +more than 128 Event Recorder instances, or multiple raw inputs introduce overlapping CoreSight ID namespaces, its +collision-free CTF allocation must be specified before extending this namespace. + +The one-input constraint means an ATB ID uniquely identifies a formatted route in this PR. Future multiple-input +support must key routes as `(input identity, ATB ID)` because different formatter domains may reuse the same ATB ID, +and multiple unformatted inputs all use OpenCSD transport channel `0`. + +The CTF model therefore stores stream identity and time-domain identity explicitly instead of treating IDs or equal +frequencies as aliases: + +```cpp +using CtfClockDomainId = std::uint32_t; + +struct CtfClockDomainDescriptor { + CtfClockDomainId id; + std::string name; + std::optional uuid; + std::uint64_t frequencyHz; + bool absolute = false; +}; + +struct CtfStreamDescriptor { + std::uint32_t streamClassId; + TraceSourceKind sourceKind; + std::optional traceBusId; + std::optional processorName; + CtfClockDomainId clockDomainId; +}; +``` + +`cmsis_trace_bus_id` is meaningful only when `traceBusId` is present. An Event Recorder stream must not put its CTF +ID `0x80` into that field as though it were a CoreSight ID. + +The decoder produces one common semantic `TraceEvent` flow. Each event carries a normalized source-route identity, +local cycle timestamp, source information, payload, and quality state. A formatted CoreSight route contains its ATB +Trace Bus ID; an unformatted or non-CoreSight route does not. Processor name, clock, prescaler, configured source +metadata, and output labels remain on the normalized route and are resolved from that identity instead of being +copied into every event. + +The existing `TraceSelection` remains one shared output contract for CSV and CTF. All supported routes are decoded +so that synchronization, recovery, and diagnostics stay correct; `--stream` and `--type` are applied to normalized +semantic events immediately before backend output. Both backends reuse `traceEventSelectedForOutput` instead of +implementing separate filter rules. Several values after one option, for example `--type dwt itm`, form a union of +types; when both stream and type filters are present, both predicates must match. Diagnostic reporting observes the +complete decoded flow independently of output filtering. CTF creates lazy stream, clock, metadata, and XML artifacts +only after the first selected event, so a fully filtered route remains artifact-free. + +The output behavior is deliberately asymmetric: + +- CSV writes one combined file in synchronous semantic callback order. It preserves the stable `stream`, `type`, + `source`, `value`, `pc`, `address`, and diagnostic fields. Trace-run data controls routing, timestamp-prescaler + correction, validation, and filtering, but does not add processor names, clock frequencies, configured addresses, + or other inferred values to the CSV schema. +- CTF writes one bundle containing one shared `metadata` file and one `stream_` file for every + emitted CTF stream descriptor. It uses trace-run processor names, clocks, source labels, DWT data types/sizes, and + address ranges as CTF metadata. All stream files share the trace UUID, while each stream class references one + explicit clock domain and owns its packet sequence. +- CSV retains the synchronous semantic callback order produced by OpenCSD. Pending packets may be released by a + later timestamp on their own stream, so this is not a promise of byte-exact raw-input order across streams. A CTF + reader can scale each stream with its assigned clock; it can define a global event order only for clock domains + with a proven common reference. + +## Target architecture + +```text +raw file discovery ctrace-run + SWO.raw / TB[_name].raw trace-format, setup, refs, routes + \ / + +---- normalized input + stream metadata + | + OpenCSD DecodeTree session + / \ + SINGLE input FRAME_FORMATTED input + one protocol route by Trace Bus ID + \ / + stream-bound protocol decoders + | + CortexMStreamDecoder + independent state per stream + | + semantic TraceEvent flow + / \ + one ordered CSV one CTF bundle + metadata + stream_... +``` + +One `DecodeTree` is created per raw input. `OCSD_TRC_SRC_SINGLE` connects one configured ITM decoder directly; +`OCSD_TRC_SRC_FRAME_FORMATTED` creates the deformatter and connects one decoder for each configured ITM Trace Bus ID. +DWT and PMU packets share their processor's ITM stream and do not create separate OpenCSD decoder instances. +An ETB or ETR capture containing only one ITM source still uses the formatted path when its formatter was enabled. +The formatted deformatter discards ID-0 NULL/padding data. OpenCSD uses transport channel `0` only for unformatted +`SINGLE` mode; the CTF backend independently assigns that route CTF stream-class ID `0`. +The `DecodeTree` is the mandatory OpenCSD entry point for every raw input, including legacy `*.SWO.raw`. There is no +second direct-ITM raw-input architecture; unformatted SWO is represented by the tree's `SINGLE` input mode. + +## Instance and state audit + +The existing ctrace implementation contains no mutable process-global trace state. File-local `static` functions, +`constexpr` tables, and immutable default values are stateless and can be shared safely. The OpenCSD decoder registry +is a shared factory/registry, while every decoder component and its protocol state are owned by the session/tree. +Logical multi-stream decoding remains synchronous; this plan does not require concurrent calls from multiple +threads. + +The following instance boundaries already support independent streams: + +- `CortexMStreamDecoder` owns a lazy `Trace Bus ID -> CortexMPostDecoder` map. +- Each `CortexMPostDecoder` owns its timestamp, overflow, pending-event, and `DwtPacketDecoder` state. +- Each `DwtPacketDecoder` owns its pending comparator correlation state. +- `CtfEncoder` already partitions semantic timestamp, overflow, and exception-lane state by Trace Bus ID. +- `DecodeConsumers` and `TraceOutputLifecycle` deliberately fan one semantic event flow out to the selected outputs. +- `CsvFileOutput` deliberately remains one instance because CSV preserves the combined decoder callback order; the + `stream` column identifies formatted sources. + +The following shared-instance assumptions must be changed or reviewed during implementation: + +- `YmlTraceRunConfigReader` currently drops a setup as soon as it sees `disable`. Preserve each disabled setup + fragment's optional `pname`, original ordinal, and referenceable feature paths, but do not invent producer- + diagnostic fields that are not part of the `ctrace-setup` schema. A disabled fragment contributes no active decoder + metadata and never disables another active fragment with the same `pname`. It lets normalization reject a stale + reference only when that path resolves exclusively to disabled fragments, while keeping disabled and absent setups + distinguishable. +- `DecodePipeline` currently owns one direct `OpenCsdItmDecoder`; replace it with one tree session that owns one or + more protocol decoder components. +- `OpenCsdPacketCollector` currently has one tree-wide transaction buffer. Replace it with route-aware buffering so a + protocol error can discard only the failing route's uncommitted elements at or after the error offset while + retaining events from other routes. Raw packet-monitor callbacks need a small per-decoder adapter that supplies the + bound Trace Bus ID. Input-wide framing failures still discard the complete transaction. +- `OpenCsdItmDecoder` currently tracks one input-wide data-loss/resynchronization interval. Replace it with per-route + recovery state: one route's next synchronization/event must not close another route's data-loss interval. +- `OpenCsdErrorRecord` and resulting diagnostics must retain an optional OpenCSD channel ID. Channel `0` is a valid + `SINGLE` channel, normal formatted IDs are `1..111`, and `OCSD_BAD_CS_SRC_ID` (`0xFF`) maps to no channel for a + deformatter/input-wide error; it is unrelated to CTF stream-class ID `255`. +- `OpenCsdErrorController` already collects callbacks during one synchronous OpenCSD operation and decides after that + operation returns. Preserve this non-reentrant boundary, but extend its single-error decision into a complete batch + grouped by normalized route. Fatal, non-recoverable, or channel-less errors take precedence; every retained warning + and recoverable route error is still reported. +- OpenCSD's alternate `DecodeTree` error logger is process-global. Encapsulate its installation and restoration in + the tree-session lifetime, restore the previously installed logger, and do not let a global OpenCSD callback retain + a pointer to destroyed ctrace state. +- `TraceIssueReporter` currently aggregates its overflow summary across all streams. Keep overall diagnostics if + useful, but partition overflow counters and first timestamps by Trace Bus ID and include the stream in messages. +- `TraceOutputLifecycle` deliberately lets one backend fail while other backends continue and publish their output. + Preserve this contract: a CTF-specific configuration or writer failure must not disable otherwise valid CSV, and + vice versa. Within the CTF backend, treat metadata, all emitted stream files, and the optional companion XML as one + bundle for completion and cleanup so a failed or repeated conversion cannot leave stale CTF streams or XML lanes. +- `CtfEncoder` currently owns one binary `CtfStreamWriter`; replace it with one writer instance per emitted CTF + stream descriptor, indexed by CTF stream-class ID, and move trace UUID ownership to the enclosing bundle/encoder. +- Add one bundle-local `CtfMetadataModel` that owns common trace identity and all per-stream declarations. Populate it + from normalized trace-run data and augment it with observations during decoding. It is not a process-global + singleton. Keep `CtfMetadataWriter` as a stateless serializer of the completed model. +- `CtfMetadataWriter` currently keys ITM and DWT symbols only by channel/comparator number. In the new model, key + source metadata by `(Trace Bus ID, source)` and generate stream-specific type aliases/environment names so equal + source numbers on different processors cannot overwrite each other's labels, types, sizes, or addresses. +- `TraceCompassXmlWriter` currently builds one global state-system path per event/source. Prefix relevant state paths + with Trace Bus ID or resolved processor identity so exceptions, sleep, DWT sources, and counters from different + processors do not merge into the same GUI lane. + +## Sequential implementation phases + +This section defines the mandatory execution order. The complete change stays on one topic branch and in one +devtools pull request, but every phase is implemented as a focused, reviewable commit series. A phase may add tests +before production code, but its final commit must leave the branch buildable and all established behavior green. +Formatted input is never passed to the unformatted decoder as an intermediate shortcut. + +```text +Phase 0 -> Phase 1 -> Phase 2 -> Phase 3 -> Phase 4 + -> Phase 5 -> Phase 6 -> Phase 7 -> Phase 8 -> Phase 9 +``` + +| Phase | Deliverable | Status | +| :--- | :--- | :--- | +| 0 | Baseline, fixtures, goldens, coverage gate | Complete | +| 1 | Trace-run declaration and route normalization | Next | +| 2 | Raw-input discovery and preflight | Pending | +| 3 | Route-aware semantic state, diagnostics, and CSV | Pending | +| 4 | CTF descriptors and metadata model | Pending | +| 5 | Multi-stream CTF bundle and Trace Compass policy | Pending | +| 6 | DecodeTree `SINGLE` migration | Pending | +| 7 | Clean formatted decoding and TB integration | Pending | +| 8 | Route-local recovery and error isolation | Pending | +| 9 | Consumer validation, documentation, and final hardening | Pending | + +Update this table only after the corresponding exit criterion and common gate pass. + +The implementation baseline includes the merged +[devtools PR #2602](https://github.com/Open-CMSIS-Pack/devtools/pull/2602); this topic branch is based on the resulting +`main` and must not duplicate its reader changes. Its null contract applies throughout every phase: optional null +scalars and null entries are read as absent wherever possible, unrelated nodes and diagnostics remain ignored, and +defaults or mode-specific requirements are evaluated only after reading. Presence-only nodes retain their specified +meaning. In particular, `trace-format: null` behaves like an omitted declaration and selects the legacy global +`unformatted` default. Missing or null clock remains acceptable for validation-only and CSV operation but is an +Error that prevents CTF generation. + +### Gate after every phase + +Before starting the next phase: + +1. Format changed C++ and CMake files and run `git diff --check`. +2. Build ctrace and run the complete portable unit and integration suite, not only the new tests. +3. Run the deterministic 100% ctrace source-line coverage gate and inspect the unfiltered branch report. Do not add + exclusions to make a phase pass. +4. Re-run the relevant legacy SWO golden-output tests. Any intentional artifact change must be explained and + approved in the plan before updating a golden file. +5. Review the complete phase diff for ownership, cleanup, error paths, and accidental changes outside ctrace. Fix all + findings before proceeding. + +The supported-platform CI matrix is required whenever a phase is pushed. Local success is not used to dismiss a +platform failure. + +### Phase 0: Freeze the baseline and fixtures + +Purpose: establish reproducible evidence and quality gates without changing runtime behavior. + +1. Freeze the ctrace-private decision record for root `trace-format`, absent/null/explicit declaration behavior, and + internal memory-aligned framing. State that the manually annotated TB fixture is consumer test input, that this PR + makes no normative CMSIS or producer-emission claim, and that specification/producer work is tracked separately + rather than gating the devtools phases. Record the external pyTS follow-ups for the `1..111` assignment range and + eventual normative capture-format metadata. +2. Add this reviewed plan, the TODO links, and the documented reconstructed `TB-Trace` fixture with its independent + deformatter/analysis helper. +3. Verify every checked-in SHA-256 value, formatter payload count, source ID, and decoded CM4/CM7 semantic row count. + Keep the helper outside the ctrace runtime and build. +4. Record the current legacy SWO CSV and CTF/XML artifacts as the compatibility baseline. +5. Add or confirm a deterministic 100% source-line coverage gate before the functional phases begin. Keep the + ctrace workflow push and pull-request path filters identical and limited to their existing ctrace scope. +6. Run the complete baseline gate. No production source behavior changes in this phase. + +Exit criterion: the private-contract boundary is explicit, and the fixture, legacy artifacts, complete test suite, +and coverage result are reproducible from the topic branch. + +### Phase 1: Normalize trace-run declarations and routes + +Purpose: create one schema-aware model before raw-file discovery or decoding changes. + +1. Extend `TraceRunConfig` with the optional global `trace-format` declaration. Missing and null remain absent; + explicit `unformatted` and `formatted` remain distinguishable from the default so discovery can preserve legacy + behavior. +2. Until Phase 7 enables the formatted frontend, stop a job with an explicit effective `formatted` declaration + deterministically before raw-input discovery or selection, before the legacy frontend can receive bytes, and + before decoder or output construction. This temporary guard is part of Phase 1 even though final discovery moves + to Phase 2. +3. Preserve disabled setup fragments with optional `pname`, original ordinal, and referenceable feature paths instead + of active metadata. `disable` remains a presence-only node, independent of its YAML value, and applies only to its + own fragment. +4. Retain binding metadata and diagnostics for every consumed reference type. Normalize authoritative processor-ITM + anchors, the constrained current-pyTS feature-reference fallback, and normative/transitional timestamp spellings + without creating decoder objects. +5. Build a normalized route catalogue with processor association, optional architectural Trace Bus ID, timestamp + clock/prescaler metadata, ITM enable mask, source descriptions, and producer diagnostics. +6. Validate only routing-critical structure at this layer: reference-authoritative IDs in `1..111`, unique compatible + bindings, valid anchor-or-fallback evidence, fragment-local disabled-reference resolution, and ambiguous processor + association. Ignore a copied/enriched `ctrace-setup.itm.atbid` for routing, reject configured stream values + `112..127`, and keep output-specific clock and DWT metadata requirements deferred. +7. Cover missing/null/invalid format values, the pre-Phase-7 formatted guard, disabled fragments, anchor and fallback + routes, consistent and conflicting bindings, references without `source`, producer diagnostics, both timestamp + spellings, tolerated `itm.atbid`, and rejected values `112..127` with focused reader/model/job tests. + +Exit criterion: trace-run parsing and normalization are independent of filenames and outputs; existing legacy +decoding still uses the old raw frontend unchanged, while an explicit formatted declaration is safely rejected before +that frontend or any output can be reached. + +### Phase 2: Resolve and preflight exactly one raw input + +Purpose: establish the final input-selection boundary before any decoder or output is created. + +1. Introduce the normalized input descriptor containing selected path/channel, effective format, declaration state, + internal framing, and normalized routes. +2. Implement the legacy and explicit discovery matrix: absent/null format activates only SWO; an explicit non-null + format makes SWO, TB, and specification-defined `TB_` channels eligible; ER remains a diagnosed but + unsupported side input. +3. Resolve exactly one eligible file and reject zero or multiple candidates. Preflight regular-file status, + readability, and memory-aligned formatted size before constructing outputs or OpenCSD state. +4. Reorder job construction so trace-run read, normalization, discovery, input preflight, and output-requirement + evaluation happen before output creation. Preserve backend independence for later CTF-only requirement failures. +5. Continue decoding legacy SWO and explicitly unformatted SWO/TB/`TB_` with the existing direct frontend. + Retain the + Phase-1 formatted-input guard at the normalized descriptor boundary until Phase 7; after discovery and preflight, + reject formatted input before output creation and never feed it to the unformatted decoder. +6. Test the complete SWO/TB/`TB_`/ER matrix, empty inputs, unreadable/non-regular files, ambiguous candidates, + partial formatted frames, and proof that failed preflight creates no artifact or decoder. + +Exit criterion: every job owns one validated descriptor and existing unformatted output remains compatible. + +### Phase 3: Make semantic state, diagnostics, and CSV route-aware + +Purpose: remove single-stream assumptions below the raw frontend before enabling formatted bytes. + +1. Introduce a stable normalized route identity. Preserve the optional architectural Trace Bus ID separately from + OpenCSD's transport channel `0`, so a synthetic unformatted route is never mistaken for CoreSight source ID `0`. +2. Carry the route identity through `OpenCsdTraceElement`, `CortexMStreamDecoder`, `TraceEvent`, diagnostics, selection, + and outputs. Keep processor metadata on the route catalogue rather than copying it into every event. +3. Verify or complete independent `CortexMPostDecoder`, DWT correlation, timestamp, overflow, synchronization, + quality, and pending-event state per route. +4. Apply each route's timestamp prescaler exactly once in `CortexMStreamDecoder`; OpenCSD, CSV, and CTF do not repeat + the scaling. +5. Partition overflow summaries and decoder/data-loss diagnostics by route. Preserve a combined overall summary only + if it cannot hide per-route information. +6. Keep one CSV writer and the existing column set. Preserve synchronous callback order, optional formatted stream + IDs, payload width, common type/stream filtering, and independent diagnostic reporting. +7. Test interleaved elements on at least two synthetic routes, duplicate source numbers, different prescalers, + independent errors, filtering semantics, and exact 1/2/4-byte zero payload rendering. + +Exit criterion: direct semantic tests prove multi-route behavior while all raw input still uses the unchanged +unformatted frontend. + +### Phase 4: Introduce the CTF stream/clock and metadata model + +Purpose: replace global CTF assumptions with explicit descriptors without enabling multiple binary writers yet. + +1. Replace `coreClockHz` configuration with normalized stream-class and clock-domain descriptors. Keep transport, + processor, CTF stream-class, and clock-domain identity separate. +2. Implement the bundle-local `CtfMetadataModel` and make `CtfMetadataWriter` a stateless serializer of that model. + Key ITM/DWT source metadata by route and source, not by source number alone. +3. Move trace UUID ownership to the bundle and pass it explicitly to the existing writer and metadata model. Clock + UUIDs remain separate identities. +4. Resolve CTF requirements after trace-run/input normalization. Missing, null, invalid, zero, or ambiguous clocks + disable only CTF with a targeted Error; CSV/check remain viable and `--all` remains non-zero while completing CSV. +5. Preserve the existing one-stream runtime path, `stream_0`, `swo_clock`, UUID optionality, event layouts, and XML + shape. Exercise multi-stream/multi-clock metadata models directly in unit tests but do not publish multiple stream + files until Phase 5. +6. Test descriptor identity, boundary/non-contiguous stream-class IDs, source-name collisions, equal-frequency but + independent domains, shared-domain consistency, and absence of every clock fallback. + +Exit criterion: the legacy CTF output is produced through the new model and remains equivalent; the model can +represent the final formatted topology without writer-side global state. + +### Phase 5: Emit a complete multi-stream CTF bundle + +Purpose: make CTF and Trace Compass consume the route-aware model before formatted decoding is enabled. + +1. Replace the single binary writer with an owning map of lazy `CtfStreamWriter` instances indexed by CTF + stream-class ID. Every writer receives the bundle trace UUID and owns independent packet/timestamp state. +2. Route selected semantic events to their descriptor, create the writer on first emission, and emit exactly one + stream-local `trace_start` and exception bootstrap before the triggering event. Preserve the eager legacy empty + unformatted `stream_0` exception. +3. Generate one metadata stream class and referenced clock declaration per emitted descriptor. Do not create + artifacts for configured formatted routes that produce no selected event. +4. Remove the equal-clock restriction. Distinct processor domains get distinct clock UUIDs even at equal frequency; + sharing requires an explicitly identical counter/timebase domain. +5. Generate Trace Compass XML only when every emitted stream uses the same one clock declaration, and partition its + state paths by route/processor. Otherwise keep valid CTF, remove stale XML, and report one Warning. +6. Treat all stream files plus metadata and optional XML as one CTF backend lifecycle. Any CTF start/write/finish + failure cleans the incomplete bundle while an independent CSV backend may still complete. +7. Drive the encoder directly with interleaved semantic events on at least two routes and test lazy creation, + non-contiguous IDs, shared trace UUID, independent clocks/state, filters, metadata isolation, XML conditions, and + failure cleanup. + +Exit criterion: multi-route semantic input produces a standards-consistent CTF bundle without requiring a formatted +raw frontend; all legacy CTF/XML tests remain green. + +### Phase 6: Move legacy unformatted input onto DecodeTree + +Purpose: replace the direct ITM session with the final common frontend while changing only the `SINGLE` path. + +1. Add a tree-session wrapper that owns `DecodeTree`, its configured components, callback adapters, and error state. +2. Install the alternate OpenCSD logger for exactly the tree lifetime, restore the previously installed logger on + every exit path, and enforce at most one live tree session. +3. Create one ITM decoder in `OCSD_TRC_SRC_SINGLE` mode, bind OpenCSD channel `0` to the synthetic route, and attach + callbacks to both the full decoder and associated packet processor as required by OpenCSD. +4. Preserve chunk feeding, bounded `WAIT`/flush behavior, end-of-trace, current recovery semantics, byte/event counts, + and output lifecycle. +5. Remove the direct-session production path once all injected-session and legacy SWO tests use the tree wrapper. +6. Test normal and exceptional construction/destruction, foreign logger restoration, overlapping-tree rejection, + associated-component errors, channel `0`, empty input, bounded progress, and byte-for-byte legacy CSV plus + equivalent CTF/XML output. + +Exit criterion: every supported legacy input uses `DecodeTree(SINGLE)` and no formatted behavior is enabled yet. + +### Phase 7: Enable clean formatted CoreSight decoding + +Purpose: add the memory-aligned formatted path after the semantic and output layers are already multi-route capable. + +1. Construct `OCSD_TRC_SRC_FRAME_FORMATTED` with the internal memory-aligned framing value and one ITM decoder for + every normalized anchor- or feature-fallback route. +2. Attach a route-bound raw-packet adapter to every ITM decoder and a tree-level unpacked-frame monitor for observed + formatter IDs. +3. Keep ID `0` silent as NULL/padding. Diagnose each unsupported normal source ID once and skip it without guessing a + protocol; preserve supported routes and outputs. +4. Preserve the architectural Trace Bus ID through semantic events, CSV filtering/stream values, CTF stream-class + mapping, diagnostics, and Trace Compass when XML is valid. +5. Enable the reconstructed TB fixture end to end and add focused clean formatted fixtures for single-source, + boundary-ID, unsupported-ID, and empty-input behavior. +6. Treat any formatted protocol or deformatter error as input-fatal in this phase. Route-local recovery is enabled + only after Phase 8 proves transaction isolation and deformatter-state preservation. + +Exit criterion: clean formatted one- and multi-source captures decode correctly; the CM4/CM7 TB fixture produces one +combined CSV and independent CTF streams, with no misleading multi-clock XML. + +### Phase 8: Isolate formatted errors and recover one route + +Purpose: make recoverable ITM failures local without corrupting other routes or the formatter state. + +1. Preserve every OpenCSD callback in a stable operation-local batch, normalize its optional channel, and make fatal, + channel-less, or deformatter errors take precedence after the OpenCSD call returns. +2. Replace tree-wide packet transactions and data-loss state with route-aware buffering and recovery intervals. + Retain unaffected-route events while discarding only unsafe events from the failing route. +3. Resolve and reset only the failing decoder chain. Never reset the complete formatted tree for a route-local + protocol error. +4. Preserve the deformatter's current ID and partial-frame state. Drain already unpacked segments with bounded flush + operations before feeding the next aligned raw block, using the root processed-byte count as the only file cursor. +5. Keep only the affected route in data loss until its next hardware synchronization; close an unresolved interval + explicitly at end-of-trace. +6. Abort every active output on input-wide fatal failure, but preserve the established independent-backend behavior + for writer failures and publish recoverable semantic diagnostics in otherwise complete output. +7. Test failures from both decoder components, several callbacks per operation, partial-frame interruption, + continuous IDs without repeated markers, unaffected interleaved routes, failed local reset, deformatter errors, + unresolved end-of-trace, and the existing `SINGLE` recovery path. + +Exit criterion: injected and formatted-fixture faults prove that one route can recover without resetting, losing, or +misattributing another route. + +### Phase 9: Complete integration, consumers, and documentation + +Purpose: close coverage gaps and validate the complete feature as one product change. + +1. Add documented synthetic formatted fixtures for every currently supported ITM-carried trace type not covered by + the reconstructed hardware fixture, including malformed/recovery intervals and zero-width-sensitive payloads. +2. Run the full CLI matrix for check, CSV, CTF, and `--all`; type/stream filters; absent/null clocks; missing routes; + unsupported IDs; output failures; and repeated conversions with stale artifacts. +3. Add the pinned Linux Babeltrace consumer test and validate each stream's time scaling independently. Validate the + supported Trace Compass single-clock case and the deliberate multi-clock no-XML case semantically. +4. Update `architecture.md`, `constraints.md`, fixture provenance, and `todo.md`. Mark `trace-format` and internal + framing as ctrace-private provisional decisions, link their specification/producer follow-ups, and keep + FSYNC/HSYNC, explicit file identity, ETM/ETE/PTM/MTB, Event Recorder decoding, and cross-domain correlation visibly + deferred. +5. Run formatting, all portable tests, the Linux consumer gate, deterministic source-line coverage, the unfiltered + branch report, and the complete supported-platform CI matrix. Review the total branch diff and acceptance + criteria before declaring the pull request ready. + +Exit criterion: every acceptance criterion below is demonstrated by a test, an external-consumer check, or explicit +documentation, with no temporary Phase 2 or Phase 7 restrictions left in production code. + +## Detailed implementation requirements + +The subsystem sections below are the detailed contract used by the phases above. If a detail appears to conflict +with the phase sequence, the safer behavior applies and the plan must be corrected before implementation continues. + +### Trace-run and discovery (Phases 1-2) + +1. Add a normalized raw-input descriptor containing global `trace-format`, internal global framing, channel, path, + and source routes. +2. Parse and validate provisional root-level `trace-format: unformatted | formatted`. Treat an absent or null value + as `unformatted` without a diagnostic. Keep framing internal and fixed to the global `memory-aligned` default for + formatted input; do not parse or emit a `trace-framing` YAML field. +3. In Phase 1, before final raw-input discovery exists, reject an explicit effective `formatted` declaration at the + job boundary before legacy raw-input selection, decoder construction, or output creation. In Phase 2, retain that + guard after descriptor discovery and preflight. Remove it only when Phase 7 constructs the formatted tree; never + use the unformatted frontend as a temporary fallback. +4. Preserve declaration state while parsing, then resolve the effective format before candidate selection: an absent + or null declaration uses the global unformatted default and legacy SWO-only eligibility; a coexisting legacy TB + remains an unsupported side input rather than becoming a second candidate. An explicit non-null format makes + SWO/TB eligible, and an explicit formatted input uses the internal memory-aligned framing default without a + diagnostic. +5. Resolve exactly one active raw input. Reject zero or multiple matching inputs, then preflight its regular-file + status, readability, and required byte alignment before decoder and output creation; do not partially convert one + candidate. Empty unformatted and memory-aligned captures are valid. Preserve the legacy unsupported-channel + behavior separately: an ineligible + coexisting TB/ER side input reports a non-failing Warning but does not abort a valid selected SWO conversion. +6. For formatted input, derive exactly one route per unique valid `stream`. Prefer an effective `[/]itm` + reference with `type: itm` as the authoritative anchor. If no anchor exists, accept only the supported ITM-carried + path/type pairs listed in the end-to-end contract as a current-pyTS fallback. Associate other compatible feature + references without creating more routes. Allow consistent repetitions, but reject conflicting + `ID -> (protocol, pname)` or bound-`pname + protocol -> ID` bindings and reject a malformed or conflicting present + anchor rather than falling back around it. +7. Create exactly one synthetic no-ATB-ID ITM route for a `SINGLE` input and require at least one reference- + authoritative ID in `1..111` for formatted input. Bind processor metadata when uniquely available. Explicit stream + ID `0` and configured values `112..127` are invalid formatted routes. Tolerate a copied or enriched + `ctrace-setup.itm.atbid`, but ignore it for routing and consistency decisions. +8. Store and forward diagnostics from every consumed reference type, then independently validate all + routing-critical fields. + Missing, malformed, contradictory, or ambiguous required data is fatal even when the reader could retain part of + the node. Retain structurally valid fields from processor-ITM and timestamp references even when they have no + `source` and carry a producer `error`; formatted routing still requires valid anchor-or-fallback evidence. Preserve + each setup containing `disable` as a disabled fragment with its optional `pname`, original ordinal, and + referenceable feature paths. It contributes no active metadata and never disables another active fragment with the + same `pname`. Reject a reference as stale-disabled only when its path resolves exclusively to disabled fragments; + use an active match when present, and merge multiple active matches only when every consumed value is compatible. + Diagnostics are not defined on `ctrace-setup` nodes by the current schema. +9. Validate the selected formatted file against the internal memory-aligned framing contract: an empty capture is + valid, and every non-empty capture has a length that is a multiple of 16 bytes. Attach a tree-level unpacked raw- + frame monitor to collect observed formatter IDs. FSYNC/HSYNC acquisition and partial-sync validation remain out + of scope until a public framing mode is specified. +10. Keep the current unformatted SWO default for older trace-run files. +11. Replace filename-only Trace Buffer decisions and support multiple simultaneous named inputs once the explicit + channel/file-association declaration is specified. + +### OpenCSD integration (Phases 6-8) + +1. Replace the direct `OpenCsdItmSession` construction with a `DecodeTree` session used by both input formats. +2. Preserve the existing chunked feeding, flush, end-of-trace, and bounded-progress behavior. +3. Create one `ITMConfig` and ITM decoder per routed Trace Bus ID for formatted input. + Keep OpenCSD's ITM prescaler at `1`; ctrace applies the configured per-stream prescaler later and exactly once. +4. Do not create a protocol decoder for formatter ID `0`; consume it only as deformatter NULL/padding data. +5. Attach the generic trace-element callback once to the tree. For every ITM decoder, attach the ctrace error logger + to both the full decoder component and its associated packet processor; `DecodeTree` does not wire the alternate + logger to the associated component automatically. +6. Keep OpenCSD error callbacks as a synchronous observation boundary, not a recovery execution context. The callback + copies severity, error code, raw index, optional OpenCSD channel, and message into a session-owned batch and + returns without resetting decoders, changing outputs, or throwing. After the active OpenCSD data-path call returns, + ctrace combines the batch with its response and processed-byte count, then decides whether to continue, flush, + reset one route, or abort. This prevents reentrant mutation of the tree from inside an OpenCSD callback. +7. Attach a packet-monitor adapter with bound Trace Bus ID to every ITM decoder so that ITM sync, overflow, reserved + packets, and incomplete tails retain their source identity. +8. Attach a tree-level `ITrcRawFrameIn` monitor and enable `OCSD_DFRMTR_UNPACKED_RAW_OUT` for per-element `traceID` + observation. This makes formatter IDs without an ITM + route visible even though no protocol decoder is attached. Handle ID `0` as NULL/padding and formatter-special + IDs according to the restrictions above; do not interpret other IDs as ITM or guess whether they carry + ETM/ETE/PTM/MTB. Diagnose each unsupported observed normal source ID once as a non-failing Warning and skip its + payload without flooding the log. Continue and publish the successfully decoded supported routes; ID `0` remains + silent NULL/padding rather than an unsupported-source warning. + +### CSV output (Phase 3) + +1. Keep one CSV writer and preserve the deterministic, synchronous OpenCSD decoder callback order across all streams. + Do not promise raw-byte or chronological ordering between independent streams. + Do not write concurrently from per-stream chains into the same CSV file; OpenCSD callbacks are consumed + synchronously by this single fan-in writer. Separate parallel CSV writers would imply separate output files and + are outside the specified output contract. +2. Continue writing the architectural Trace Bus ID in `stream`; leave it empty for unformatted internal ID `0`. +3. Apply the per-stream ITM timestamp prescaler before CSV mapping, while retaining cycle values rather than + converting them to wall-clock units. `CortexMStreamDecoder` performs this scaling exactly once; CSV and CTF consume + the resulting canonical events and never apply the prescaler again. +4. Keep the specified CSV representation close to the trace stream. Do not enrich rows with `pname`, configured + clocks, comparator base addresses, or other values that did not arrive in the trace packet. +5. Attribute decoder errors, synchronization, overflow, and data loss to the affected stream without changing the + existing CSV column set. +6. Preserve the common `TraceSelection` output filter after semantic decoding. Multiple requested types are a union; + type and stream predicates form an intersection. Do not suppress decoder diagnostics or recovery work merely + because their semantic events are filtered from CSV output. + +### Errors and recovery (Phase 8) + +1. Normalize the channel supplied by `ocsdError` into `std::optional` in `OpenCsdErrorRecord`: preserve + channel `0` and normal formatted IDs, but map `OCSD_BAD_CS_SRC_ID` (`0xFF`) to absence. +2. Preserve every callback in the current data-path-call batch. Group recoverable errors by normalized route after + the call returns; warnings remain diagnostics, while any non-recoverable/fatal or unassignable protocol error takes + precedence and aborts the input. Do not perform reset, rollback, output, or exception propagation in the callback. +3. Associate protocol errors, data-loss intervals, synchronization, and overflow with the affected normalized route + and include its Trace Bus ID when one exists. +4. Treat a framing/deformatter error as fatal for the complete input. Abort every active output instead of attempting + to preserve partially routed data. +5. For a recoverable ITM protocol error with a normalized route, reset only that route's OpenCSD packet + processor/full-decoder chain. Resolve the decoder through `DecodeTree::getDecoderElement(traceBusId)`, obtain its + public `ITrcDataIn` interface through the decoder manager, and send `OCSD_OP_RESET`. Packet-processor reset + propagates to its associated full decoder without resetting the frame deformatter or any other route. Use the + synthetic channel `0` to resolve the one `SINGLE` decoder. +6. Do not reset the complete formatted `DecodeTree` for route-local recovery. A tree reset clears the deformatter's + current source ID, so resuming at an arbitrary later frame can silently lose a continuous source that does not + repeat an ID marker. If a protocol error cannot be attributed to one configured route, or route-local reset fails, + treat it as fatal for the input instead of guessing a recovery point. +7. After route-local reset, retain the deformatter's framing, current-ID, and partially delivered-frame state. The + root input's returned `numBytesProcessed` is the only raw-file cursor; never re-feed bytes it reports as consumed. + Before supplying the next aligned raw block, issue bounded `OCSD_OP_FLUSH` calls so the deformatter can finish + delivering any already unpacked frame segments. Handle further callback batches after each flush by the same + route-local rules. A successful `CONT` response proves the pending frame is drained; `WAIT` remains bounded, and a + fatal/channel-less result aborts the input. +8. Only the affected ITM decoder searches for its next hardware synchronization and remains in data loss until it + finds one; other routes continue without reset or a synthetic discontinuity. If the affected route never + resynchronizes, explicitly close and emit its interval as unresolved through end-of-trace. +9. Retain valid callbacks before the failing raw-file offset. Route-aware transaction buffering discards callbacks + from the failing route at or after that offset while preserving unaffected routes; an input-wide fatal error + discards all uncommitted callbacks and aborts every active output. A writer error aborts its own backend according + to the existing independent-output contract; recoverable protocol diagnostics remain in completed output as + today. + +### Processor and time domains (Phases 1, 3-5) + +The existing trace-run normalization already resolves timestamp settings through +`ctrace-setup.pname -> ctrace-ref.stream`. It retains both `timestamps.clock` and +`timestamps.itm-prescaler` per Trace Bus ID. The stream decoder applies the resolved prescaler to local ITM +timestamps before handing events to the independent per-stream post-decoders. For the TB fixture this maps stream +`1` to CM4 at 240 MHz with prescaler 1, and stream `2` to CM7 at 480 MHz with prescaler 1. + +The [CMSIS-Toolbox trace specification][cmsis-trace] +defines `timestamps.clock` as optional and gives it no default; only `timestamps.itm-prescaler` has the default `1`. +Ctrace therefore does not make clock mandatory while reading or decoding. It becomes an operational requirement +only for CTF, whose generated clock declaration needs a real frequency. Missing or invalid clock metadata disables +that backend with an Error instead of inventing a processor frequency. +[CTF 1.8](https://github.com/efficios/ctf/blob/master/common-trace-format-specification.md#8-clocks) itself assumes +1 GHz when a clock's `freq` is omitted, but ctrace must not use that format-level default: its timestamp values are +processor-clock cycles, not nanoseconds, so the resulting time scale would be silently wrong. + +Every OpenCSD `ITMConfig` uses prescaler `1`, so OpenCSD exposes raw ITM ticks. Only `CortexMStreamDecoder`, which +knows the resolved stream identity, multiplies each local timestamp once by that stream's `itm-prescaler`. CSV and +CTF consume these canonical scaled events and must not scale them again. + +This association requires the formatter ID, or an explicit single-input processor route. An unformatted stream is +reported by OpenCSD as internal ID `0`; it cannot be assigned to one of several processors with different timestamp +settings from the trace bytes alone. Missing prescaler metadata keeps the specified default of `1`; missing CTF clock +metadata has no default. It is accepted by the reader and by validation-only/CSV decoding but prevents CTF generation +for the selected route. Ambiguous or invalid prescaler metadata is fatal. + +The current CTF writer has one `CtfStreamWriter` instance and one hard-coded `swo_clock`. It therefore accepts +multiple selected streams only when their resolved `timestamps.clock` values are equal. This is an implementation +restriction, not a CTF 1.8 restriction: one metadata file can declare multiple clocks and stream classes. The +multi-source implementation replaces the single writer with one writer instance per CTF stream descriptor and maps +every CTF stream class to an explicit clock-domain descriptor. Equal frequency does not imply a shared origin. +[CTF 1.8 clocks](https://github.com/efficios/ctf/blob/master/common-trace-format-specification.md#8-clocks) +define non-absolute clocks as synchronized only when they have the same UUID. + +1. Preserve the validated source-route-to-processor binding in normalized metadata and resolve it for outputs; do + not duplicate the processor name in every decoded event. +2. Maintain independent DWT correlation, timestamp quality, and local cycle state per Trace Bus ID. +3. Keep CSV rows in deterministic, synchronous decoder callback order. The existing `stream` column remains the + Trace Bus ID; no non-standard CSV column is added. +4. Use the processor binding for CTF metadata and Trace Compass labels where available. +5. Emit one CTF stream per emitted CTF stream descriptor and one clock declaration per actual counter/timebase, + including when processor clocks differ. Streams may share a declaration only when they use the same counter, + frequency, and origin; mere equal frequency or mathematical correlation is insufficient. +6. Treat clock frequency and cross-stream synchronization separately: frequency converts local cycles to elapsed + time, while global timestamps or another common reference are required to align independent processor time + origins exactly. + +The current `ctrace-run.yml` supplies frequency but no clock-domain identity, origin, or inter-processor offset. +Therefore this PR treats different processor bindings as independent domains even when their frequencies are equal. +The reconstructed CM4/CM7 fixture consequently produces two independent CTF clocks and no companion Trace Compass +XML. A later producer field may prove that streams use the same counter/timebase. Correlated but distinct counters +remain separate clock declarations and need their own specified offsets/correlation model. + +A formatted route may be valid without a matching processor setup. Such a route remains explicitly unbound and gets +its own clock domain and UUID; it never shares a domain merely because another route is also unbound. With no valid +clock value, validation-only and CSV decoding may continue with the default prescaler `1`, but CTF generation reports +an Error and does not start. Processor labels are omitted or use the existing generic source fallback. + +### CTF streams and clocks (Phases 4-5) + +1. Let the single `CtfBundleOutput`/`CtfEncoder` instance own one bundle-local `CtfMetadataModel`, an owning map of + `CtfStreamWriter` instances indexed by CTF stream-class ID, and an explicit normalized-route-to-CTF-ID lookup. No + CTF metadata state is process-global. +2. Populate configured metadata before decoding from normalized CTF stream and clock-domain descriptors: CTF + stream-class ID, source kind, optional Trace Bus ID, processor name, clock-domain ID/frequency, ITM/DWT sources, + labels, data types, sizes, and address ranges. +3. Augment the model only from selected semantic events with actually emitted streams and dynamic observations such + as exception numbers. Mark a stream class emitted on its first selected semantic event; unselected observations + and configured routes that never occur in the raw trace do not create an empty stream file, metadata declaration, + clock declaration, or XML lane. The + per-stream writers do not independently generate or own schema metadata. Preserve the existing eager + `stream_0`/`trace_start` behavior for an unformatted `SINGLE` capture, including an empty capture; this explicit + compatibility exception does not create empty files for configured formatted routes. +4. Resolve each decoded event's normalized source route to its CTF stream-class ID, route it to the corresponding + lazily created writer instance, and write it to `stream_`. When creating a writer, emit the + same selection-dependent `TRACE_STATUS/trace_start` record and exception-lane bootstrap as today's single writer, + exactly once and before the event that caused creation. A first overflow, synchronization, or issue-only event + follows the same rule; a route whose events are all filtered remains artifact-free. + Apply the same `traceEventSelectedForOutput` predicate as CSV before lazy creation; do not duplicate or weaken the + type/stream filter in CTF-specific code. +5. Allocate the trace UUID once at bundle level and pass the same UUID to every writer and to the metadata model. + Every packet header UUID must equal this metadata trace UUID. `CtfStreamWriter::open` must no longer generate an + independent UUID for each file. Clock UUIDs identify time domains and are never reused as the trace UUID. +6. Allocate a CTF stream-class ID independently from filesystem naming. As a deliberate ctrace mapping for this PR, + use the Trace Bus ID for formatted sources and CTF ID `0` for an unformatted single input. CTF does not require + zero-based or contiguous IDs. Packet headers and metadata identify the stream class; filenames are descriptive + and are not referenced for routing. Reserve `0x80..0xFF` specifically for CMSIS Event Recorder instances and IDs + beginning at `0x100` for other sources without a unique direct ATB-ID mapping. +7. Extend `CtfOutputConfig` and `CtfEncoderConfig` from one `coreClockHz` value to normalized stream descriptors plus + clock-domain descriptors. A stream references a domain ID; the domain owns name, optional UUID, frequency, + `absolute`, and future offsets. +8. Generate one uniquely named CTF clock declaration per actual counter/timebase, for example `cmsis_clock_1` and + `cmsis_clock_2`, and one clock-mapped timestamp type for each declaration. Without producer evidence of a common + origin, assign every processor domain a distinct clock UUID and `absolute = false`, even when frequencies match. + Share one declaration/UUID only when the streams use the same counter, frequency, and origin. Correlated but + distinct clocks remain distinct declarations; their future `absolute`/offset representation must be specified + separately. Preserve the existing UUID-optional single-stream `swo_clock` representation for compatibility. +9. Generate one CTF stream declaration per CTF stream descriptor whose event-header and packet-context timestamps + map to that stream's clock. Generate the supported event declarations for every stream class; event IDs remain + identical because they are scoped by stream ID. +10. Generate ITM channel and DWT comparator metadata in the scope of their stream so identical source numbers on + different processors retain independent labels and value metadata. Because CTF `env` is trace-global, include + the CTF stream-class ID in every generated environment/type symbol that would otherwise collide. +11. Keep `cmsis_trace_bus_id` in CoreSight/ITM event contexts for CMSIS CTF profile and consumer compatibility. Write + the architectural ID for formatted CoreSight routes and retain `0` as the existing no-ATB-ID sentinel for the + legacy unformatted ITM route. Event Recorder stream classes omit this field; never write a CTF-local ID such as + `0x80` into it. +12. Remove the output-planning rejection for selected streams with different valid clocks. Resolve frequency once + per clock-domain descriptor: one valid value is used by every stream on that domain, while conflicting valid + values are a CTF requirement failure. Diagnose missing, null, invalid, zero, or conflicting declarations with an + Error naming the configuration, route, and processor where available. Do not invent a frequency and do not start + or publish the CTF backend when any selected domain lacks one unambiguous positive clock. With `--all`, keep CSV + generation active and return non-zero because of the CTF Error. CSV generation and validation-only decoding do + not depend on this clock. +13. Preserve local timestamp, monotonicity, overflow, exception-lane, and packet sequence state independently in each + writer/stream state. +14. At successful completion, close every stream writer, validate the completed metadata model, and serialize exactly + one shared `metadata` file through the stateless `CtfMetadataWriter`. Abort the complete bundle if any stream + cannot be completed. +15. Generate the companion Trace Compass XML only when all emitted stream classes reference exactly the same single + CTF clock declaration. The current Trace Compass CTF reader does not correctly scale/sort a trace with multiple + clock declarations, even if those clocks are otherwise correlatable. For such an otherwise valid bundle, keep + the CTF output, ensure no stale companion XML remains at the target path, and emit one clear Warning. A future + per-domain-bundle/experiment output may restore a combined GUI without claiming false cross-domain ordering. + An empty formatted capture produces a metadata-only CTF bundle with no XML and no multiple-clock Warning. +16. When XML is generated, partition Trace Compass state-system paths by normalized route/processor identity before + the existing event/source hierarchy. +17. Without a common time reference, describe clocks as independent and do not claim wall-clock or cross-core + synchronization. Their frequencies still provide correct elapsed time within each stream. When usable global + timestamp correlation is available, specify and test the appropriate separate-clock offset/absolute + representation in a later extension. Assign streams to one declaration only when the producer proves the same + counter/timebase; do not normalize independent clocks to nanoseconds merely to force a global order. + +## Compatibility + +- Existing single-core SWO captures must produce byte-for-byte identical CSV and equivalent CTF/XML output. +- OpenCSD transport channel `0` remains the internal marker for unformatted input and stays empty in the CSV + `stream` column; the CTF backend separately assigns this route CTF stream-class ID `0`. +- An explicit source route with `stream: 0` is invalid; ID `0` is introduced only by the unformatted decode path. +- Formatter ID `0` is discarded as NULL/padding and never creates a route, event, CTF stream class/file of its own, + or Trace Compass lane. Under this PR's direct mapping, `stream_0` remains the unformatted-input artifact; this is a + ctrace convention, not a CTF rule. +- Formatted sources retain their architectural Trace Bus IDs `1..111` through decoding and output filtering. +- Output type names and current CTF event layouts do not change merely because the input uses a `DecodeTree`. +- A formatted capture containing only one source is valid and must not be forced through the unformatted path. + +## Tests + +The phase sections define when tests are added. The catalogue below is the final required coverage; ownership is: + +| Phase | Primary test responsibility | +| :--- | :--- | +| 0 | Fixture reproducibility, legacy goldens, and coverage gate | +| 1 | YAML reading, null/default behavior, disabled fragments, reference and route normalization | +| 2 | Discovery, input ambiguity, preflight, and no-artifact failure ordering | +| 3 | Per-route semantic state, diagnostics, prescaling, CSV, and filters | +| 4 | CTF descriptors, clock requirements, metadata model, and legacy model compatibility | +| 5 | Multiple CTF writers/clocks, lazy artifacts, Trace Compass policy, and bundle cleanup | +| 6 | DecodeTree `SINGLE`, logger/session lifetime, and legacy end-to-end compatibility | +| 7 | Clean formatted routing, formatter IDs, memory alignment, and TB end-to-end output | +| 8 | Error batching, route-local rollback/reset/resynchronization, and fatal framing errors | +| 9 | Complete fixture/CLI matrix, Babeltrace, Trace Compass, documentation, and all-platform CI | + +### Unit tests + +- Parse every provisional `trace-format` value. Verify that an absent or null value silently selects the global + `unformatted` default. Reject unknown and non-scalar non-null values. Verify separately that formatted input uses + the internal global `memory-aligned` framing value without reading a YAML field. Before Phase 7, verify that an + explicit formatted declaration, including one beside an SWO-named file, stops before raw frontend, decoder, or + output construction. +- Resolve zero, one, and multiple discovered raw inputs. Verify that only one succeeds and every failure occurs + before decoder or output creation. +- Preserve legacy discovery with coexisting SWO and TB files: without explicit metadata, only SWO is active and TB or + `TB_` is diagnosed with a non-failing unsupported-channel Warning, while SWO output is still completed. With + explicit metadata, verify SWO-only, TB-only, and one named-TB success; SWO+TB, TB+named-TB, and two-named-TB + ambiguity failures before output; non-failing ER exclusion from the active count while another eligible input + completes; and ER-only failure because no eligible input remains. +- Reject missing, non-regular, and unreadable selected raw inputs before output creation. Accept empty unformatted + and memory-aligned captures. +- Reject non-empty formatted input whose size is not a multiple of the internal 16-byte memory-aligned frame size. +- Accept consistent repetitions of one stream ID across feature references. Reject conflicting route bindings, + malformed route anchors, stale routes into disabled-only setup fragments, and insufficient routing data. Accept + IDs `1` and `111`, reject configured values `0`, `112`, and `127`, and prove that an optional + `ctrace-setup.itm.atbid` is tolerated but neither creates nor changes a route. +- Diagnose and skip observed formatter IDs without a supported ITM route, without guessing their protocol or + creating semantic output. Verify one non-failing Warning per unsupported normal ID and completed output from + supported ITM routes; ID `0` remains silent. +- Forward `info`, `warning`, and `error` diagnostics while independently retaining a structurally valid route. Cover + every supported reference type, including processor-ITM and timestamp bindings without `source`; reject them only + when ctrace's own normalization fails. +- Preserve disabled setup fragments and their original ordinals. Verify active and disabled fragments with the same + `pname` and different features, an active and disabled match for the same path, compatible and conflicting multiple + active matches, a disabled-only stale reference, and unnamed single- and multiple-fragment cases. A disabled + fragment never suppresses an active one; an absent optional setup remains distinguishable and may use the + documented defaults. Do not interpret unsupported diagnostic fields on setup nodes. +- Verify a formatted route with an authoritative `[/]itm`, `type: itm`, `stream` anchor and, separately, the + current-pyTS fallback for an unnamed single-core data-only setup with no anchor and for a uniquely bound named + feature. Reject conflicting fallback references, a conflict with a present anchor, and streamless/control-only or + unsupported refs as route evidence. A legacy `SINGLE` input with empty refs receives one synthetic no-ATB-ID ITM + route. Accept timestamp refs using normative `type: itm` and transitional `type: dwt` only for the `timestamps` + leaf, including the constrained fallback, and reject conflicting bindings. +- Construct `DecodeTree` sessions for both `SINGLE` and `FRAME_FORMATTED` input. +- Verify the one synthetic `single` route and reject only ambiguous/conflicting processor metadata; verify that + equivalent candidates may merge and OpenCSD channel ID `0` resolves to the resulting source metadata. +- Verify both formatted and unformatted captures containing exactly one configured ITM source. +- Preinstall a foreign OpenCSD logger and verify that every normal and exceptional exit restores that exact logger + after destroying the tree session. Reject an overlapping second live tree, and verify guard release when tree + construction itself fails. +- Feed formatter ID-0 NULL data and verify that it creates no route, callback, event, CTF stream, or XML lane. +- Verify stream-bound raw callbacks and OpenCSD error-channel normalization for valid `SINGLE` channel `0`, a normal + formatted ID, and `0xFF` as an absent/deformatter-wide channel. +- Inject errors from both the ITM full decoder and its associated packet processor and verify that the ctrace logger, + stream attribution, and recovery path receive both. +- Verify that error callbacks only capture stable data. Reset/output actions occur after the OpenCSD call returns; + multiple callbacks in one call are retained and fatal or channel-less errors take precedence over route recovery. +- Verify independent timestamp, DWT correlation, overflow, and recovery state for multiple IDs. +- Verify one CTF file and stream class per emitted CTF stream descriptor, with no artifact for a configured route that + produces no selected semantic event. Verify clock declarations only for domains referenced by emitted streams. +- Verify that every lazily created formatted writer emits exactly one selection-dependent `trace_start` and + exception-lane bootstrap before its first selected record, including overflow/sync/issue-only routes. Preserve the + eager legacy `stream_0` behavior for an empty unformatted capture. +- Verify that all CTF stream files share the trace UUID and carry their own stream ID, packet sequence, and clock- + mapped timestamps. +- Use non-contiguous boundary IDs such as `1` and `111`; verify exact decimal filenames, packet `stream_id`, metadata + `stream { id = ...; }`, and event `stream_id` without any array-index or next-ID assumption. +- Verify CTF generation for distinct processor clocks without applying one processor's frequency to another stream. + Give independent domains distinct clock UUIDs and prove that equal frequencies do not merge them; verify that + every packet UUID equals the metadata trace UUID and `trace_uuid != clock_uuid` for every declared clock. +- Verify clock-domain frequency normalization: two streams sharing one domain accept one equal valid value and reject + missing, null, invalid, zero, or conflicting values for CTF without inventing a frequency or creating CTF output. + Verify that `--all` still completes CSV and returns non-zero for the CTF Error. CSV-only and validation-only decoding + remain unaffected. +- Verify that two formatted routes without processor setup stay explicitly unbound and are not merged. They remain + valid for CSV/check but are rejected for CTF because neither has a clock. +- Preserve the legacy CTF representation explicitly: `stream_0`, exactly one `swo_clock` declaration, unchanged + clock-UUID optionality/metadata shape, and companion XML present. +- Verify that an empty memory-aligned formatted capture produces metadata only, no stream/clock declaration or XML, + and no multiple-clock Warning. +- Use different ITM prescalers on two streams and prove that OpenCSD keeps prescaler 1 and both CSV and CTF observe + exactly one ctrace scaling step. +- Configure the same ITM channel and DWT comparator numbers with different labels/types on two streams and verify + that CTF metadata remains independent. Verify independent Trace Compass lanes separately when both streams + reference exactly the same single clock declaration. +- Verify per-stream overflow and decoder-error diagnostics when events from multiple IDs are interleaved. +- Reset only one decoder in a continuous formatted stream whose next frame has no repeated formatter-ID marker. + Make the failure stop delivery partway through an unpacked formatter frame and prove that bounded post-reset flush + drains the remaining segments before the next raw block. The deformatter must retain its current ID, another + interleaved route receives neither reset nor data loss, and only the failing route waits for ITM hardware + synchronization. Cover `SINGLE` channel-0 recovery, internal memory-aligned framing, multiple recoverable callbacks, + and a route + that remains data-loss-active through end-of-trace. +- Verify that channel-less/non-local-resettable protocol errors and frame-deformatter errors abort the input without + attempting a complete-tree recovery. +- Preserve the independent-output lifecycle tests. Fail CSV and CTF start, write, and completion separately and + verify that the healthy backend still completes. Within CTF, fail a later stream, metadata completion, and XML + generation in turn; verify cleanup of the entire incomplete CTF bundle, including stale streams and XML. A + recoverable decoder error remains publishable with its semantic error/data-loss records. + +The following namespace contract tests are deferred with Event Recorder/backend allocation, not implemented in this +PR: `Event Recorder<0> -> 128 -> stream_128`, `Event Recorder<127> -> 255 -> stream_255`, rejection of instance 128, +and allocation of other backend-only sources from 256 upward without using `0x70..0xFF`. + +### Integration tests + +- Retain the existing real SWO fixtures as compatibility tests. +- Verify that a legacy or explicitly unformatted SWO input produces the unchanged deterministic CSV and a + `stream_0` CTF file. Verify separately that an explicitly formatted SWO-named input follows its declaration and + produces only the configured nonzero CTF stream IDs. +- Use the documented reconstructed memory-aligned CoreSight frame capture containing two interleaved real-hardware + ITM streams. +- Use the reconstructed two-stream fixture for real formatter/PC/exception/DWT-value coverage. Add focused documented + synthetic formatted fixtures for the supported packet/output types it does not contain: ITM software, DWT address + and match, PC-sample sleep indications, DWT event counters, PMU, timestamps, synchronization, overflow, and + decoder-error handling. Across the formatted fixture set, exercise every currently supported trace type on at + least two normalized ITM routes, including anchor and constrained feature-fallback coverage, so routing is tested + rather than merely packet decoding. +- Verify that one TB or `TB_` input plus trace-run produces one combined CSV with both stream IDs and one + decimal-named CTF `stream_` file per formatter ID that emits selected semantic events. +- Verify that the memory-aligned fixture's seven ID-0 padding bytes do not produce `stream_0` or semantic output. +- Exercise a partial memory-aligned formatter frame and verify a deterministic preflight diagnostic and no partial + output bundle. FSYNC and HSYNC fixtures are deferred until a public framing field is specified. +- Include one malformed ITM interval on one ID and verify route-local recovery and stream attribution while another + ID continues across the same raw interval without a discontinuity. +- Include an unsupported formatter ID with opaque payload and verify that supported ITM streams continue without + false decoding or an invented instruction-protocol label. +- Verify CSV stream IDs, CTF records, filters, and validation-only mode. Preserve Trace Compass labels and timing on + the existing single-stream case. Cover one `--type` followed by multiple values, multiple `--stream` values, their + union/intersection semantics, and a fully filtered formatted route that creates no CTF artifact. The two-stream + shared-clock positive case is a direct CTF-output test. +- Verify that CSV preserves the raw payload width for ITM and DWT values on every supported 1-, 2-, and 4-byte packet + after formatted routing: `0x00`, `0x0000`, and `0x00000000` respectively. +- Read each CTF stream with Babeltrace using a source graph or an isolated metadata-plus-one-stream fixture and + verify frequency scaling independently (for example, 240 ticks at 240 MHz and 480 ticks at 480 MHz both represent + 1 us). Verify that the default whole-bundle mux rejects distinct clock UUIDs rather than inventing a global order. + For Trace Compass, test semantic timestamp scaling/order rather than merely successful load: one shared clock + declaration is the positive case; two declarations, including equal-frequency clocks, must produce no XML and + exactly one Warning, also replacing a pre-existing stale XML target. +- Maintain 100% ctrace source-line coverage and preserve or improve the reviewed branch report without coverage + exclusions. + +### CI and external consumer gates (Phases 0 and 9) + +- Keep the portable internal CTF structure/integration tests in `CtraceIntegTests` on every supported platform. +- Register the Babeltrace consumer check as a separately labelled Linux-only CTest. Add a pinned Babeltrace 2.x + installation to the Linux test and coverage jobs; absence is a CI failure, not a silent skip. +- Keep generating and archiving line/branch LCOV data. Add a deterministic 100% source-line check to the coverage job; + Codecov's current relative line threshold and archived branch HTML alone are not that gate. Continue publishing + the branch report for review without filtering new code or adding exclusions. +- Before PR review, repeat the documented single-clock positive and multiple-clock negative acceptance cases with + the supported Trace Compass/trace-server version; successful import alone is insufficient. + +## Documentation updates (Phases 0 and 9) + +- Update `architecture.md` from a direct ITM decoder to the shared `DecodeTree` architecture. +- Record format, framing, routing, reset, and compatibility rules in `constraints.md`. +- Document provenance for every generated formatted-trace fixture. +- Remove the completed Multi-Core, formatted Trace Bus, and processor-routing items from `todo.md`. +- Keep instruction-trace decoding in `todo.md` as independent later work. + +## Acceptance criteria + +- Every selected trace-run resolves to exactly one active raw input, and all routing-critical metadata is validated + before decoder or output creation. +- The same `DecodeTree` session abstraction processes unformatted SWO and formatted CoreSight input. +- Every configured ITM Trace Bus ID is decoded independently and mapped to the correct processor when that binding is + configured; otherwise it remains explicitly unbound and uses only documented defaults. +- All currently supported trace types work for every decoded ITM source. +- Observed formatter IDs without a supported ITM route do not corrupt supported streams, are diagnosed once, and + are not falsely classified as a particular instruction protocol. +- Errors, sync, overflow, timestamps, filters, and CSV/CTF retain the correct stream identity; Trace Compass does so + whenever its XML is generated for a supported clock topology. +- OpenCSD error callbacks only capture stable error batches. Recoverable ITM errors reset their known route after the + data-path call returns; they do not reset the formatter or interrupt unaffected routes. +- A combined CTF bundle supports different processor clocks through explicit stream-to-clock-domain mappings; + independent domains have distinct clock UUIDs and no claimed global event order. +- Trace Compass XML is generated only when every emitted stream references exactly the same one CTF clock + declaration; any multiple-clock bundle remains valid CTF and emits exactly one Warning instead of misleading XML. +- A selected clock domain without one unambiguous positive configured frequency emits an Error diagnostic and + prevents CTF creation; no frequency fallback is used. Validation-only and CSV-only decoding remain valid, and + `--all` still completes CSV while returning non-zero for the CTF Error. +- Memory-aligned formatted input is covered end to end; FSYNC and FSYNC+HSYNC remain deferred until framing is + specified publicly. +- Formatter ID-0 padding never becomes a decoded or output stream. +- Fatal input/decode errors abort every active output. A backend-local output error aborts only that backend; a CTF + failure cleans up the entire incomplete CTF/XML bundle while an otherwise valid CSV may still complete. +- Existing single-SWO behavior remains compatible. +- No ETM, ETE, PTM, or MTB instruction decoding is introduced. +- All portable ctrace tests pass on supported CI platforms, the Linux Babeltrace gate passes, source-line coverage is + 100%, and the unfiltered branch report has been reviewed without adding exclusions. + +[armv7-m-arm]: https://documentation-service.arm.com/static/606dc36485368c4c2b1bf62f +[cmsis-trace]: https://github.com/Open-CMSIS-Pack/cmsis-toolbox/blob/main/docs/Experimental-Features.md#timestamps +[coresight-v2]: https://documentation-service.arm.com/static/5f9009d5f86e16515cdc0417 +[coresight-v3]: https://documentation-service.arm.com/static/63a03a981d698c4dc521ca77 +[ctf-spec]: https://github.com/efficios/ctf/blob/master/common-trace-format-specification.md diff --git a/tools/ctrace/docs/todo.md b/tools/ctrace/docs/todo.md index 0e14bd217..b2f9421f4 100644 --- a/tools/ctrace/docs/todo.md +++ b/tools/ctrace/docs/todo.md @@ -1,33 +1,38 @@ # ctrace TODO -## Pull-request cleanup +## Cleanup -- [ ] Split commit `88c3f4dc`; it mixes PC Sampling and Exception-Return handling. -- [ ] Submit Exception-Return preservation as an independent bug-fix PR. +- [ ] Add explicit parentheses to compound payload-validation expressions where they improve readability. +- [ ] Replace the CSV payload-type `if`/`else` chain with a backend-local `std::visit` visitor. +- [ ] Replace the CTF payload-type `if`/`else` chain with a backend-local `std::visit` visitor. ## DWT -- [ ] Preserve DWT reference, group, and setup-binding identities when expanding source arrays. +- [ ] Preserve logical DWT reference and setup identities when expanding comparator source arrays. - [ ] Complete Armv7-M linked-comparator, range, and value-match decoding. -- [ ] Add Armv8-M and Armv8.1-M DWT decoding. - [ ] Resolve programmable PMU event-counter names from trace-run configuration. -## Multiple streams +## Inputs and multiple streams -- [ ] Propagate processor identity into decoded events and outputs. -- [ ] Support separate trace clock domains and cross-stream synchronization in CTF. +[Implementation plan](multicore-multisource-plan.md) + +- [ ] Define raw-input format and framing metadata for `ctrace-run.yml`. +- [ ] Move the unformatted SWO/ITM path to an OpenCSD `DecodeTree`. +- [ ] Decode formatted CoreSight frames and route them by Trace Bus ID. +- [ ] Preserve normalized source-route-to-processor bindings and use them in outputs. +- [ ] Support separate trace clock domains in CTF. +- [ ] Define cross-stream clock correlation and offsets once a common producer time reference is available. +- [ ] Add per-clock-domain Trace Compass bundles/experiments for uncorrelated streams when required. ## Additional decoders -- [ ] Add named trace-buffer discovery (`TB_`) in its own PR. -- [ ] Decode formatted `*.TB.raw` input by Trace Bus ID in its own PR. -- [ ] Add ETM instruction trace decoding and output in its own PR. +- [ ] Support multiple simultaneous named trace-buffer inputs after explicit file association is specified. +- [ ] Add ETM/ETE/PTM instruction trace decoding and output in its own PR. - [ ] Add MTB instruction trace decoding and output in its own PR. +- [ ] Add Event Recorder decoding and output when it enters the implementation scope. ## Dependencies and release - [ ] Replace private OpenCSD `common/` and `interfaces/` headers with supported public APIs. - [ ] Update OpenCSD after the [empty-buffer issue](opencsd-issues.md) is fixed upstream. -- [ ] Verify the first release archive and hosted Windows, Linux, and macOS build/test matrix. -- [ ] Complete runtime-license, source-provenance, and relinking requirements for statically linked releases. - [ ] Decide the signing, macOS notarization, SBOM, and archive-checksum requirements for production releases. diff --git a/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp b/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp index a4a663820..a1783db6f 100644 --- a/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp +++ b/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp @@ -23,6 +23,13 @@ static_assert(sizeof(ocsd_trc_index_t) == sizeof(std::uint64_t), "ctrace requires 64-bit OpenCSD trace indices"); +/** @brief Creates the production OpenCSD ITM session. */ +static std::unique_ptr +createDefaultOpenCsdItmSession(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController) +{ + return std::make_unique(collector, errorController); +} + /** @brief Implements OpenCSD feeding, bounded retry, and hardware-sync recovery. */ class OpenCsdItmDecoderImpl { public: @@ -318,9 +325,7 @@ class OpenCsdItmDecoderImpl { }; OpenCsdItmDecoder::OpenCsdItmDecoder(OpenCsdTraceElementSink& elementSink) - : OpenCsdItmDecoder(elementSink, [](OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController) { - return std::make_unique(collector, errorController); - }) + : m_impl(std::make_unique(elementSink, createDefaultOpenCsdItmSession)) { } diff --git a/tools/ctrace/src/tracerun/CtraceRunMeta.cpp b/tools/ctrace/src/tracerun/CtraceRunMeta.cpp index 69d6eb5ef..f5567fa83 100644 --- a/tools/ctrace/src/tracerun/CtraceRunMeta.cpp +++ b/tools/ctrace/src/tracerun/CtraceRunMeta.cpp @@ -253,18 +253,15 @@ static ProcessorIdentity processorIdentity(const TraceRunConfig& config, std::ve }; } -/** @brief Resolves the data setup referenced by one DWT route. */ -static const TraceRunDataSetup* referencedDataSetup(const TraceRunConfig& config, const TraceRunReference& reference) +/** @brief Resolves the required data setup index of one prevalidated DWT route. */ +static const TraceRunDataSetup* referencedDataSetup(const TraceRunConfig& config, const TraceRunReference& reference, + std::size_t index) { - const auto index = reference.dataSetupIndex; - if (!index.has_value()) { - return nullptr; - } for (const auto& setup : config.setups) { if (!TraceRunSchema::processorNamesMayBind(setup.processorName, reference.processorName)) { continue; } - const auto* candidate = *index < setup.data.size() ? &setup.data[*index] : nullptr; + const auto* candidate = index < setup.data.size() ? &setup.data[index] : nullptr; if (candidate != nullptr) { return candidate; } @@ -276,7 +273,8 @@ static const TraceRunDataSetup* referencedDataSetup(const TraceRunConfig& config static CtraceRunSourceMeta sourceMeta(const TraceRunConfig& config, const TraceRunReference& reference, std::uint32_t source, const ProcessorIdentity& processorIdentity) { - const auto* dataSetup = reference.type == "dwt" ? referencedDataSetup(config, reference) : nullptr; + const auto* dataSetup = + reference.type == "dwt" ? referencedDataSetup(config, reference, *reference.dataSetupIndex) : nullptr; CtraceRunSourceMeta meta; meta.type = reference.type; meta.processorName = processorIdentity.canonicalName(reference.processorName); @@ -383,9 +381,10 @@ static std::vector resolveStreamBindings(const TraceRunCo continue; } const auto processorName = processorIdentity.canonicalName(reference.processorName); + const auto traceBusId = static_cast(reference.stream.value_or(0U)); bindings.push_back({ reference.line, - static_cast(reference.stream.value_or(0U)), + traceBusId, processorName, reference.ctraceRef, findProcessor(processors, processorName), diff --git a/tools/ctrace/test/data/.gitattributes b/tools/ctrace/test/data/.gitattributes index 63719c3ba..d87f102c5 100644 --- a/tools/ctrace/test/data/.gitattributes +++ b/tools/ctrace/test/data/.gitattributes @@ -3,3 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 *.csv text eol=lf +*.xml text eol=lf +metadata text eol=lf +stream_* binary diff --git a/tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.SWO.traceanalysis.xml b/tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.SWO.traceanalysis.xml new file mode 100644 index 000000000..0a73999d3 --- /dev/null +++ b/tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.SWO.traceanalysis.xml @@ -0,0 +1,904 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.ctf/metadata b/tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.ctf/metadata new file mode 100644 index 000000000..65679754f --- /dev/null +++ b/tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.ctf/metadata @@ -0,0 +1,310 @@ +/* CTF 1.8 */ +trace { + major = 1; + minor = 8; + uuid = "00000000-0000-0000-0000-000000000000"; + byte_order = le; + packet.header := struct { + integer { size = 32; align = 8; signed = false; } magic; + integer { size = 8; align = 8; signed = false; } uuid[16]; + integer { size = 32; align = 8; signed = false; } stream_id; + }; +}; + +env { + cmsis_ctf_profile = "cmsis.ctf"; + cmsis_ctf_profile_version = 1; + cmsis_dwt0_value_type = "unsigned"; + cmsis_dwt0_address_start = "0x24000024"; + cmsis_dwt0_address_end = "0x24000027"; +}; + +clock { + name = swo_clock; + precision = 0; + offset_s = 0; + offset = 0; + absolute = false; + freq = 480000000; +}; + +typealias integer { size = 8; align = 8; signed = false; } := uint8_t; +typealias integer { size = 16; align = 8; signed = false; byte_order = le; } := uint16_t; +typealias integer { size = 32; align = 8; signed = false; byte_order = le; } := uint32_t; +typealias integer { size = 8; align = 8; signed = true; } := int8_t; +typealias integer { size = 16; align = 8; signed = true; byte_order = le; } := int16_t; +typealias integer { size = 32; align = 8; signed = true; byte_order = le; } := int32_t; +typealias floating_point { exp_dig = 8; mant_dig = 24; align = 8; byte_order = le; } := ieee_float32_t; +typealias integer { size = 64; align = 8; signed = false; byte_order = le; } := uint64_t; +typealias integer { size = 64; align = 8; signed = false; map = clock.swo_clock.value; } := swo_clock_t; + +typealias enum : uint8_t { + "read" = 0, + "write" = 1 +} := cmsis_dwt_access_t; +typealias enum : uint8_t { + "trace_start" = 0, + "resync" = 1, + "overflow" = 2, + "decode_error" = 3, + "data_loss" = 4 +} := cmsis_trace_status_reason_t; +typealias enum : uint8_t { + "entered" = 0, + "exited" = 1, + "returned" = 2 +} := cmsis_exception_action_t; +typealias enum : uint8_t { + "trace" = 0, + "synthetic" = 1 +} := cmsis_exception_origin_t; +typealias enum : uint8_t { + "CPICNT" = 0, + "EXCCNT" = 1, + "SLEEPCNT" = 2, + "LSUCNT" = 3, + "FOLDCNT" = 4, + "CYCCNT" = 5, +} := cmsis_dwt_event_counter_t; +typealias enum : uint8_t { + "Event0" = 0, + "Event1" = 1, + "Event2" = 2, + "Event3" = 3, + "Event4" = 4, + "Event5" = 5, + "Event6" = 6, + "Event7" = 7, +} := cmsis_pmu_event_counter_t; +typealias enum : uint8_t { + "ITM1" = 1, + "ITM2" = 2, + "ITM3" = 3, + "ITM4" = 4, + "ITM5" = 5, + "ITM6" = 6, + "ITM7" = 7, + "ITM8" = 8, + "ITM9" = 9, + "ITM10" = 10, + "ITM11" = 11, + "ITM12" = 12, + "ITM13" = 13, + "ITM14" = 14, + "ITM15" = 15, + "ITM16" = 16, + "ITM17" = 17, + "ITM18" = 18, + "ITM19" = 19, + "ITM20" = 20, + "ITM21" = 21, + "ITM22" = 22, + "ITM23" = 23, + "ITM24" = 24, + "ITM25" = 25, + "ITM26" = 26, + "ITM27" = 27, + "ITM28" = 28, + "ITM29" = 29, + "ITM30" = 30, + "ITM31" = 31, +} := cmsis_itm_channel_t; +typealias enum : uint8_t { + "DWT0" = 0, + "DWT1" = 1, + "DWT2" = 2, + "DWT3" = 3, + +} := cmsis_dwt_comparator_t; +typealias enum : uint16_t { + "Thread Mode" = 0, + "Reset" = 1, + "NMI" = 2, + "HardFault" = 3, + "MemManage" = 4, + "BusFault" = 5, + "UsageFault" = 6, + "SecureFault" = 7, + "SVCall" = 11, + "DebugMonitor" = 12, + "PendSV" = 14, + "SysTick" = 15, +} := cmsis_exception_number_t; + +stream { + id = 0; + event.header := struct { + uint32_t id; + swo_clock_t timestamp; + }; + event.context := struct { + uint8_t cmsis_trace_bus_id; + }; + packet.context := struct { + uint32_t packet_size; + uint32_t content_size; + swo_clock_t timestamp_begin; + swo_clock_t timestamp_end; + uint32_t events_discarded; + uint32_t packet_seq_num; + }; +}; + +event { + id = 0; + name = "ITM"; + stream_id = 0; + fields := struct { + cmsis_itm_channel_t cmsis_itm_channel; + enum : uint8_t { i8 = 0, u8 = 1, i16 = 2, u16 = 3, i32 = 4, u32 = 5, f32 = 6 } m_cmsisitm_value_type; + variant { + int8_t i8; + uint8_t u8; + int16_t i16; + uint16_t u16; + int32_t i32; + uint32_t u32; + ieee_float32_t f32; + } m_cmsisitm_value; + uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; + +event { + id = 1; + name = "DWT_VALUE"; + stream_id = 0; + fields := struct { + cmsis_dwt_comparator_t cmsis_dwt_comparator; + cmsis_dwt_access_t cmsis_dwt_access; + enum : uint8_t { i8 = 0, u8 = 1, i16 = 2, u16 = 3, i32 = 4, u32 = 5, f32 = 6 } m_cmsisdwt_value_type; + variant { + int8_t i8; + uint8_t u8; + int16_t i16; + uint16_t u16; + int32_t i32; + uint32_t u32; + ieee_float32_t f32; + } m_cmsisdwt_value; + enum : uint8_t { none = 0, u8 = 1, u16 = 2, u32 = 4 } cmsis_dwt_pc_type; + variant { + uint8_t none; + uint8_t u8; + uint16_t u16; + uint32_t u32; + } cmsis_dwt_pc; + enum : uint8_t { none = 0, u8 = 1, u16 = 2, u32 = 4 } cmsis_dwt_address_type; + variant { + uint8_t none; + uint8_t u8; + uint16_t u16; + uint32_t u32; + } cmsis_dwt_address; + uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; + +event { + id = 2; + name = "DWT_ADDR"; + stream_id = 0; + fields := struct { + cmsis_dwt_comparator_t cmsis_dwt_comparator; + enum : uint8_t { none = 0, u8 = 1, u16 = 2, u32 = 4 } cmsis_dwt_pc_type; + variant { + uint8_t none; + uint8_t u8; + uint16_t u16; + uint32_t u32; + } cmsis_dwt_pc; + enum : uint8_t { none = 0, u8 = 1, u16 = 2, u32 = 4 } cmsis_dwt_address_type; + variant { + uint8_t none; + uint8_t u8; + uint16_t u16; + uint32_t u32; + } cmsis_dwt_address; + uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; + +event { + id = 9; + name = "DWT_MATCH"; + stream_id = 0; + fields := struct { + cmsis_dwt_comparator_t cmsis_dwt_comparator; + uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; + +event { + id = 7; + name = "DWT_EVENT"; + stream_id = 0; + fields := struct { + cmsis_dwt_event_counter_t cmsis_dwt_event_counter; + uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; + +event { + id = 8; + name = "PMU_EVENT"; + stream_id = 0; + fields := struct { + cmsis_pmu_event_counter_t cmsis_pmu_event_counter; + uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; + +event { + id = 3; + name = "TRACE_STATUS"; + stream_id = 0; + fields := struct { + cmsis_trace_status_reason_t cmsis_trace_status_reason; + uint32_t cmsis_overflow_count; + }; +}; + +event { + id = 4; + name = "EXCEPTION"; + stream_id = 0; + fields := struct { + cmsis_exception_number_t cmsis_exception_number; + cmsis_exception_action_t cmsis_exception_action; + uint16_t cmsis_exception_number_value; + cmsis_exception_origin_t cmsis_exception_origin; + }; +}; + +event { + id = 5; + name = "GLOBAL_TIMESTAMP"; + stream_id = 0; + fields := struct { + uint64_t cmsis_global_timestamp; + uint8_t cmsis_clock_change; + }; +}; + +event { + id = 6; + name = "PC_SAMPLE"; + stream_id = 0; + fields := struct { + uint8_t cmsis_pc_sample_state; + uint32_t cmsis_pc[cmsis_pc_sample_state]; + uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; diff --git a/tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.ctf/stream_0 b/tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.ctf/stream_0 new file mode 100644 index 0000000000000000000000000000000000000000..5db046c7946853334136028a9e27407c814263fc GIT binary patch literal 65536 zcmeI)^;Z>d+s0uwuq6~k6a+yKK?FoXkPsWYvAeswySux)y9*V?02K?x4lHa?Y}9Ap z%<}a8>iGw}KV0`(44XM-HmuJ=2QC<5Tr*;f=rT9DoZA>O`a`2XZy5HPKz=_k{rv#1 zb^kd?r}nGOXOGN>>Mi|A!`5{3K_hb`qs*sQ^0xBt_oUOGBL1}d@8{L&V~O9L^ZP^5 z=}!~Cc2IBFtCmiG#<2bS{m0+SlBvqi5|1%9e6*QMr$0yhU_lFb9Pz!?EaCCQ*Z0Z_ zKTmww{A}4g3=EK|VR)mx=eRX$!wXyhGod@TC|L*SwzfU~9ZhrU!;x7id!ygiVxZDH&h4`g5Yn7w_Y0ze@DDgTv2!`@mg;~;AzAw<_v?sCtflv9R7iLXtQGQ zbmG3FBj6v2yKg8C|3o~`g%a@3#B-#TgnuD!ZeJSymH4k>W#He4e`-+{{+;-nv61i$ z;!ie}ga06YC!sw2C-JKvD!_jck8`L5R~PiZub(GMREFz_A81_#ZcKdpgsN~8;_J3n zgPRgxdZjvCPkhd&8t^Q{Cp*=Gn-L#XItp%1d~mzka0}u+Cf9*m5^ukwE<7voW{LIS z*@)NuS|4sjysAq>xHa*zk&WQliHCP=47VX3Fs%tZ2l0Zto5F30yCgM(=Ok{I(Hx$O zcs93|a695A6RXAw z|Nr^FZfEhY5AE3-=i3wSGp`RkH}TGg`@$WFw|dYI?nu0$=>T{h;x)Yo!kvg$s4)oc zOuR&&!SKAqgBK2gyAUrFJrwRrJb&^qcs}BeW+UKk#BF>>!t)b1iy8%YC;qeFXt)RQ zkBi5^3lM*Od@S6P_~R$z;Obkszdyh3T1!#X82-wIPtJGOW?(b`=471k09>(W*NLV zaaY?F@Djvxg|38`B%ZbDDtIa4#-molOB4UPZVkK)@znUW@Uq08zgq{7Bz`~F26#E* z*TXl$%M-uQd=tC^@l#_q!z&U$ym1S>67gLZx56tE-}rtTybAFZxp%;;5}#juC%hW* zX{~m_s}mnPemA@Z@gZCGz-tokeR(gu7V%CW_rarxx6X3_UYmG>QU~F6h}UR)2wsBICh=DgDo@MGucvIp( zcAkPaBc6Ue7T%ort8b^_Er=((s;Q`2I(*U+)wLZ%zE{^muq1;>Y%! zhqooZ@8$(~JK|e^T!gnLz9#=Acn9K(D_(|oBtEO_6?iA&6K7q8cP2h!|223Q;{ER= z!n+dhW~8Q?YU%Xdh_@|}q_M%f6K_)GCcFpn+C6T;dlIia_cpv2@zRIxzGUK1 zIz#!%W3Mt*`AFi29>0caaQ23q31|LIwdF@pASmN^rq`}7#pStut zd_3_nF(2R)h!1|24xdQ8S5`G=R7P0ax{UrW57y_)lBzK(d;2sP)^d_D0t zE!CV)^9{rsk5h9#%{LN{+N|b$nr|Xr>5`iBX}+0wsdP2x(|il@5Jxq~(|jxO!X?!l zPxEcWJ=&-_p61($J55w`Jk56yx80`Zc$)7dZgEx3@igB>{Lilh==8gZe|GZ3`Fn`J zEnN`4m-y3mUhsXy?@#uI?xAFPjz!k0w5McM;KzxN&IpE|AU?<~1Rg`YXN6GsN#Y&4guzb{Z$2{| z9!tF5zGCpx#G`IUz|Rnm{8b!&mUuCblJIlH11pz;#}W7HUK$=x+;vVF_<7>?2g|}Q z5VyJ)3BO3()VMr6f%tdN3h+zB-&d^&zfAl^&r0ws#2?P948KY|>2MYJHR1^ms=^bA zpEj)yzfSz9cMbRr;(Kb;geMW-+@}`&Ch=7Zqu{rQFO04YzfF8ravk^`;^WQg!S50u z=35_rk9gmx2JrjDyYz1ee?YwTl1A`{#2cMx41Ywt*3%~NWa1Sqo53FwFX`VL{)BkZ zx-H;OiTe&}34cc1eR(VRbK-eUwT7n<&ymsw{(`uO%&f3mU@{4Mc2r#r*n5l?*C1)fSgE_*k48u1gs-Qn+vA86DA{(<=R;XUE$#MiCq z1^-BV>ABwUPsHcE=>z{ve6npn_!r`%Li@wN5+B%f0Q?*A9-{`rzY}l2eh@r^c(e0^ z;XjDiO&tRNNxZ7vF!(RxWs41m8(IGQ6SMFZBj7sX0b@tPjfofBGzxA)+$CW&+?06k z4`bkZ;@KR=!LtxIDKQ>yM*Lf=32<}bY2zotEr_RVnFO~a{^0Utcvj*!K2CvWBYrW@ zG`JP<*izHs*2IsrodM5IeD|c8a2w*Aw$Fm+Ainb2Y`87)1z+aCa}uAPcOEI(4||f5gbJEAxg49Gp3mR0DbJ zF;S=eXV1UKWInYzu1vq<>Xl*X^wHM1D8s`RpMW3xH8d!efblU3w0Fns{nV9Q+LN=g;EdXNlj>dI5fp`1OE` z@HpZL^%CIm#7_;r1V2yw@QTau3&eNDUV&dEzVXFXcmnYi)`{>-#ODWHhhHW>t>F#$ z72;!uCBd%}AF}!;{2KAzXK%q1iFbN^8-AU5%N%#%H;6X~xd%@oUZcr<_)X&FM?Qex zB3@?QL-=jtLGh2^cZmDEONQSi?w0Eb{2pc(VO#_!Hu{BHqBC62IK?E&Lhrv*X^upA$c}ITfBl zeBY%s_zU7&)8E5i5?|w(4u3^_amkPH*TiSF`2>GMeB#8<@VCTAZTkX$N4)>lukcjj z-9CSVrx9=KoB@ANyh)iK@DIdmxBm%GCti8VFZf5|rFUw#9IET%C*q;kb?Vw}SUUY@ z;(p(Z;a`Xsa5aU0CGK2K5C29yXQwRi@5C*qo53@P>-Lz#e-QtYWC8z4{9T46{1@?O zZrR{Q+5i3bf3JcSTt_^yi#6Ps`1zUH;U>gm_u0Tri66S11Fk2&^Or3=3-Jvex!`8R zmshrfn-ibc-5zd1eCnLvx7X_Qmc+*#binypi4VT#2+u~mm$4Jvig?F@&TwnuEvn^( zXD432mkZp6c=h?N@EpX;9mxl`B_8q64W5&D5xqM+7jbVN54auid^HQe?TP2^>j}?I z+zhyAnTh zA^@I`c=XdixEt}kmO=3R#JBhd!`+Fmu3HrDL447m5O@LNGna?LJ&8{^6$URze0WMY z+>3ZWs|dI^@vcRR!+nUiX;1=QhorqgIuI7bafiOli0u@lvnK!2OAb*hInu zh!-we4jxF{qj7n75#mlGD!_w?+pet$4<>FAR|#H}_@BQspwovC|7=?o=Z6x18(Iw> zM*L~h>hN&lcSqHL7bAXceNA`-@%Zz#;Khl@q(;F@5RbO2120K@N3pu_QpDG{s0S}i zeA(Fg@G`{bZfXE8OMFT~LwF?dKYx)zr!V)Hs~HI#GZO5{oTFrZX(t;gAO)m=6p#W^ zKnh3!DIf);fE17dQa}nw0VyB_q<|EV0#ZNFJ$U+cINahnFq~es7ObXT8kp5g#iC8on^A#OMKM4sHhwV22$IDW z*639U-b*T?5Cp@z1#2N#(3HlH$4Uz{h)RG+;}&gTQ`0R=XNb4SwH}gy-1w zm)u&9e>EmNyfxNBx2|*W%*}YvUgl};5%F6 zZf#K#cEw#~f#XxWCv3n&!M~{`Xt)#MgAJX_1b_aAmOwJn_^(wCX0#t);w{W%A*S-$ zgOth(&dg$y>_%lH^CjHwNIXbF&BWL)1m+(P8OT*2fbq6I0ScL$0QFfn_? zgA-FVw<$aD+c;iNBO&@And^O3$VEi&b3U^ap~lx3fuf=T)ZrbEF*dN5`8)Xi7-J)7 zp3TbRtua=99lc~sFFr|_Dz?Q~4q+Vf$5`#YhYUw$s0$?oc5ew`TKUeS*&`vQgVrCT zulHQ(w^L>|oG6q0)f|kP=J(>as|ktG5YNsFUPkon&2FY1J&1_t4rK`*s|N4R(i| z;^&{Sn*ud8)K@$l@1LuBYT2Lc;qwTdd~H;`Eb4wt82g7UFEJ`QJvls$d16-~l0W#J zl$og5KFPfsV`J@M7m-)fhaM)rwk5L1n6GKXQ@^|>6)pA-_c(OUx!(KMYiy6|_v~wN z7-@Kv@ghUgC0dRVsPijBOaw`qrXUlD=!@$$qC#ciZLTopXePY;GhKV^^md6i zat|TORAfII5UXpJxNlObU}M+DyS>==Pc>XtJNi<-R36d=_To#!Y*P;!r1Cu1kwlH1`v+&}{vmqBrEf}?z!(=oe+f^a zzlfz1^W3|!M|IP8GQUd_YcW;v_p|I{@wfKJm$$gnQPb~hawiN+HuTGDU7wY@Z#wX! z8fpkS=X2f7mu%5X=UQlrQjd)KOR$ChVyE{Nk%R@@ty03##i!h~_?u=6n=%&0+q9Pa zR{Kwt{aC=^%|Ys)D)sFp+&fSSf0IF?`(K&;kM;kbzlgB+mP`qqF7qKt+LL#_yWQxA zPHC$oEPqeuPA(!WstVbW9C;RLuTxxnKXQv!5S~x@eble11BrrAFJ5PdQNp)jf5p?V ztKaFtq01wfTInqtR#aR$Tx{x(`$(Y!JC+mPzXsV)5L-$%3*8X{nCIxq7M`|rt5u`F zjW`S!n+XmZ5G35eD8m};&q!2fv&K}25lna4gG@b+yj)mfrXM(^niroion4Xr@KfU~ z=~7nsdV6ans@O>0xlHGexwh)vL{H)TLCQbkuxc}3+N&86?_Uv)UZA*NToH0qZsK*M z>EvXlGB*K{qDpZBN)?p|{MT78*tA@8H1+jG;|@a&XJY+py|}{8lZd7x?0niyqsOBi zp$V~(cg{2mN57yo%1p1WxLbl}N7v>~8J2YY32NoEt?>=EHyrUbO+VQ%sBa$qX@41( zw!TuwG)b31EYIuhLKyF}{F zK-G8gYJ*4-e(pTdr4GI1MWej!1B!g~GO&4694``lne|L26BTT!ttje7w=~8q) z7{ebj1_|`|s;B(=3I4);h}01cImViw_7@=ES59`nPW2g=h zmB1^h+~=nHpg(T$>*XY{)a^zR7t7f-(n z7E5Tg(fksQcK#YUs4%~*WXFYhKKX2PoxwB%r=G4pj{2~I0Z;djui`;-Df=trD1Th* zW&Cy1xW6!J#=8Oja+e@M>JjUR)R)=Y&i*4ZF`/Debug/ctrace +for stream_id in 01 02; do + stream_dir="$work_dir/split/stream-$stream_id" + printf 'ctrace-run:\n ctrace-refs: []\n' \ + > "$stream_dir/Blinky+Arm.ctrace-run.yml" + "$CTRACE" "$stream_dir" --csv + awk -v stream="$stream_id" \ + 'END { print "stream " stream ": " NR - 1 " semantic rows" }' \ + "$stream_dir/Blinky+Arm.SWO.csv" +done +``` + +The expected row counts, excluding the CSV header, are `stream 01: 213 semantic rows` and +`stream 02: 312 semantic rows`. These counterchecks inspect the generated output; the checked-in reconstructed capture +remains the canonical test artifact. diff --git a/tools/ctrace/test/data/TB-Trace/regenerate_tb_trace.py b/tools/ctrace/test/data/TB-Trace/regenerate_tb_trace.py new file mode 100755 index 000000000..a04201893 --- /dev/null +++ b/tools/ctrace/test/data/TB-Trace/regenerate_tb_trace.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Arm Limited. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# Generated with AI + +"""Regenerate the canonical multi-source formatter capture from its source.""" + +from __future__ import annotations + +import argparse +import hashlib +from dataclasses import dataclass +from pathlib import Path + + +FRAME_SIZE = 16 +FRAME_PAYLOAD_SLOTS = 15 +CAPTURE_SIZE = 4096 +ITM_HARDWARE_SYNC = b"\x00\x00\x00\x00\x00\x80" +SOURCE_SHA256 = "b0fccabe1a326ffe9fadf12d5c3a205d87628985e5e75a99da23c97d7f33d13b" +OUTPUT_SHA256 = "aab49e56a07783b984fa7c6faeea101a51141423e66ba043dbd8d30702012639" + +SOURCE_CM7_ID = 0x01 +SOURCE_CM4_ID = 0x02 +OUTPUT_CM4_ID = 0x01 +OUTPUT_CM7_ID = 0x02 +NULL_ID = 0x00 + + +@dataclass(frozen=True) +class RoutedByte: + """One deformatted payload byte and its formatter source ID.""" + + source_id: int | None + value: int + + +@dataclass(frozen=True) +class DecodedCapture: + """Ordered formatter payload and structural counters.""" + + payload: list[RoutedByte] + id_changes: int + + +def sha256(data: bytes) -> str: + """Return the lowercase SHA-256 digest of data.""" + return hashlib.sha256(data).hexdigest() + + +def decode_frames(data: bytes) -> DecodedCapture: + """Decode memory-aligned CoreSight frames while retaining payload order.""" + if len(data) % FRAME_SIZE: + raise ValueError( + f"input size {len(data)} is not a multiple of {FRAME_SIZE} bytes" + ) + + payload: list[RoutedByte] = [] + current_id: int | None = None + id_changes = 0 + + def emit(value: int) -> None: + payload.append(RoutedByte(current_id, value)) + + for frame_offset in range(0, len(data), FRAME_SIZE): + frame = data[frame_offset : frame_offset + FRAME_SIZE] + flags = frame[15] + + for index in range(0, 14, 2): + first = frame[index] + second = frame[index + 1] + flag = bool(flags & (1 << (index // 2))) + + if first & 1: + new_id = first >> 1 + if new_id != current_id: + if flag: + emit(second) + current_id = new_id + id_changes += 1 + if flag: + continue + emit(second) + continue + + emit(first | int(flag)) + emit(second) + + last = frame[14] + if last & 1: + new_id = last >> 1 + if new_id != current_id: + current_id = new_id + id_changes += 1 + else: + emit(last | ((flags >> 7) & 1)) + + return DecodedCapture(payload, id_changes) + + +def payload_for(records: list[RoutedByte], source_id: int | None) -> bytes: + """Collect payload bytes belonging to one source.""" + return bytes(record.value for record in records if record.source_id == source_id) + + +def run_count(records: list[RoutedByte]) -> int: + """Count source-ID runs in an ordered payload.""" + if not records: + return 0 + return 1 + sum( + left.source_id != right.source_id + for left, right in zip(records, records[1:]) + ) + + +def validate_source(data: bytes, decoded: DecodedCapture) -> set[int]: + """Validate every source property on which reconstruction depends.""" + if len(data) != CAPTURE_SIZE: + raise ValueError(f"source capture must contain {CAPTURE_SIZE} bytes") + if sha256(data) != SOURCE_SHA256: + raise ValueError("source capture SHA-256 does not match the canonical input") + if decoded.id_changes != 252: + raise ValueError("source capture must contain 252 formatter ID changes") + + observed_ids = {record.source_id for record in decoded.payload} + if observed_ids != {None, NULL_ID, SOURCE_CM7_ID, SOURCE_CM4_ID}: + raise ValueError(f"unexpected source IDs: {sorted(repr(value) for value in observed_ids)}") + if len(payload_for(decoded.payload, None)) != 4: + raise ValueError("source capture must contain four bytes before its first formatter ID") + if len(payload_for(decoded.payload, NULL_ID)) != 9: + raise ValueError("source capture must contain nine NULL-source bytes") + if len(payload_for(decoded.payload, SOURCE_CM4_ID)) != 1482: + raise ValueError("source CM4 payload must contain 1482 bytes") + + cm7_indices = { + index + for index, record in enumerate(decoded.payload) + if record.source_id == SOURCE_CM7_ID + } + cm7_payload = payload_for(decoded.payload, SOURCE_CM7_ID) + if len(cm7_payload) != 2093 or not cm7_payload.endswith(ITM_HARDWARE_SYNC): + raise ValueError("source CM7 payload must end in its redundant ITM synchronization packet") + return set(sorted(cm7_indices)[-len(ITM_HARDWARE_SYNC) :]) + + +def transform_payload(decoded: DecodedCapture, removed_cm7_indices: set[int]) -> list[RoutedByte]: + """Apply the documented source-ID, synchronization, and padding changes.""" + transformed = [ + *(RoutedByte(OUTPUT_CM4_ID, value) for value in ITM_HARDWARE_SYNC), + *(RoutedByte(OUTPUT_CM7_ID, value) for value in ITM_HARDWARE_SYNC), + ] + id_map = { + SOURCE_CM4_ID: OUTPUT_CM4_ID, + SOURCE_CM7_ID: OUTPUT_CM7_ID, + } + transformed.extend( + RoutedByte(id_map[record.source_id], record.value) + for index, record in enumerate(decoded.payload) + if record.source_id in id_map and index not in removed_cm7_indices + ) + + available_slots = (CAPTURE_SIZE // FRAME_SIZE) * FRAME_PAYLOAD_SLOTS + # Each source run needs one formatter-ID marker. Appending NULL padding adds + # one more run and therefore one more marker. + null_bytes = available_slots - len(transformed) - run_count(transformed) - 1 + if null_bytes <= 0: + raise ValueError("transformed payload leaves no room for canonical NULL padding") + transformed.extend(RoutedByte(NULL_ID, 0) for _ in range(null_bytes)) + if len(transformed) + run_count(transformed) != available_slots: + raise ValueError("transformed payload does not fill complete formatter frames") + return transformed + + +def source_marker(source_id: int) -> int: + """Encode one formatter source-ID marker.""" + if not 0 <= source_id <= 0x7F: + raise ValueError(f"formatter source ID is out of range: {source_id}") + return (source_id << 1) | 1 + + +def encode_frames(records: list[RoutedByte]) -> bytes: + """Encode an ordered payload into deterministic memory-aligned frames.""" + output = bytearray() + position = 0 + current_id: int | None = None + + while position < len(records): + frame = bytearray(FRAME_SIZE) + flags = 0 + + for index in range(0, 14, 2): + if position >= len(records): + raise ValueError("payload ended before a formatter pair was complete") + record = records[position] + + if record.source_id != current_id: + if record.source_id is None: + raise ValueError("cannot encode payload without a formatter source ID") + frame[index] = source_marker(record.source_id) + current_id = record.source_id + frame[index + 1] = record.value + position += 1 + continue + + if position + 1 < len(records) and records[position + 1].source_id != current_id: + next_id = records[position + 1].source_id + if next_id is None: + raise ValueError("cannot encode payload without a formatter source ID") + frame[index] = source_marker(next_id) + frame[index + 1] = record.value + flags |= 1 << (index // 2) + current_id = next_id + position += 1 + continue + + frame[index] = record.value & 0xFE + flags |= (record.value & 1) << (index // 2) + position += 1 + if position >= len(records) or records[position].source_id != current_id: + raise ValueError("payload cannot fill a complete formatter pair") + frame[index + 1] = records[position].value + position += 1 + + if position >= len(records): + raise ValueError("payload ended before the final formatter slot") + record = records[position] + if record.source_id != current_id: + if record.source_id is None: + raise ValueError("cannot encode payload without a formatter source ID") + frame[14] = source_marker(record.source_id) + current_id = record.source_id + else: + frame[14] = record.value & 0xFE + flags |= (record.value & 1) << 7 + position += 1 + + frame[15] = flags + output.extend(frame) + + return bytes(output) + + +def regenerate(source: bytes) -> bytes: + """Return the canonical reconstructed capture for source.""" + decoded = decode_frames(source) + removed_cm7_indices = validate_source(source, decoded) + records = transform_payload(decoded, removed_cm7_indices) + output = encode_frames(records) + + if len(output) != CAPTURE_SIZE: + raise ValueError(f"reconstructed capture has unexpected size {len(output)}") + if sha256(output) != OUTPUT_SHA256: + raise ValueError("reconstructed capture SHA-256 does not match the canonical output") + if decode_frames(output).payload != records: + raise ValueError("reconstructed formatter stream does not round-trip") + return output + + +def parse_arguments() -> argparse.Namespace: + """Parse command-line arguments.""" + fixture_dir = Path(__file__).resolve().parent + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--source", + type=Path, + default=fixture_dir.parent / "Blinky+Arm" / "Blinky+Arm.TB.raw", + help="source hardware capture (default: adjacent Blinky+Arm fixture)", + ) + parser.add_argument("--output", type=Path, required=True, help="reconstructed capture to write") + return parser.parse_args() + + +def main() -> int: + """Regenerate, validate, and write the canonical capture.""" + args = parse_arguments() + output = regenerate(args.source.read_bytes()) + args.output.write_bytes(output) + decoded = decode_frames(output) + + print(f"wrote {len(output)} bytes ({len(output) // FRAME_SIZE} frames) to {args.output}") + print(f"SHA-256: {sha256(output)}") + print(f"formatter ID changes: {decoded.id_changes}") + for source_id in (NULL_ID, OUTPUT_CM4_ID, OUTPUT_CM7_ID): + payload = payload_for(decoded.payload, source_id) + print(f"formatter ID 0x{source_id:02x}: {len(payload)} payload bytes") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/ctrace/test/data/TB-Trace/split_tb_trace.py b/tools/ctrace/test/data/TB-Trace/split_tb_trace.py new file mode 100755 index 000000000..54928fc59 --- /dev/null +++ b/tools/ctrace/test/data/TB-Trace/split_tb_trace.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Arm Limited. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# Generated with AI + +"""Split memory-aligned CoreSight formatter frames into per-ID ITM streams. + +The default output preserves the demultiplexed payload bytes exactly. The +optional analysis sync is synthetic and exists only to let an ITM decoder start +at byte zero when the captured excerpt does not begin with a hardware sync. +""" + +from __future__ import annotations + +import argparse +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path + + +FRAME_SIZE = 16 +ITM_HARDWARE_SYNC = b"\x00\x00\x00\x00\x00\x80" +MIN_SOURCE_ID = 0x01 +MAX_SOURCE_ID = 0x6F + + +@dataclass +class SplitResult: + """Payload bytes collected for each formatter ID.""" + + streams: dict[int, bytearray] + unassigned: bytearray + id_changes: int + + +def split_frames(data: bytes) -> SplitResult: + """Demultiplex memory-aligned 16-byte CoreSight formatter frames.""" + if len(data) % FRAME_SIZE: + raise ValueError( + f"input size {len(data)} is not a multiple of {FRAME_SIZE} bytes" + ) + + streams: dict[int, bytearray] = defaultdict(bytearray) + unassigned = bytearray() + current_id: int | None = None + id_changes = 0 + + def emit(value: int) -> None: + if current_id is None: + unassigned.append(value) + else: + streams[current_id].append(value) + + for frame_offset in range(0, len(data), FRAME_SIZE): + frame = data[frame_offset : frame_offset + FRAME_SIZE] + flags = frame[15] + + for index in range(0, 14, 2): + first = frame[index] + second = frame[index + 1] + flag = bool(flags & (1 << (index // 2))) + + if first & 1: + new_id = first >> 1 + if new_id != current_id: + if flag: + emit(second) + current_id = new_id + id_changes += 1 + if flag: + continue + emit(second) + continue + + emit(first | int(flag)) + emit(second) + + last = frame[14] + if last & 1: + new_id = last >> 1 + if new_id != current_id: + current_id = new_id + id_changes += 1 + else: + emit(last | ((flags >> 7) & 1)) + + return SplitResult(dict(streams), unassigned, id_changes) + + +def sync_offsets(data: bytes | bytearray) -> list[int]: + """Return every hardware ITM synchronization offset in a byte stream.""" + offsets: list[int] = [] + offset = 0 + while (offset := data.find(ITM_HARDWARE_SYNC, offset)) >= 0: + offsets.append(offset) + offset += len(ITM_HARDWARE_SYNC) + return offsets + + +def write_outputs( + result: SplitResult, + output_dir: Path, + solution_set: str, + prepend_analysis_sync: bool, +) -> None: + """Write valid sources as independent raw ITM streams.""" + output_dir.mkdir(parents=True, exist_ok=True) + + if result.unassigned: + (output_dir / "unassigned.bin").write_bytes(result.unassigned) + + for source_id, payload in sorted(result.streams.items()): + if not MIN_SOURCE_ID <= source_id <= MAX_SOURCE_ID: + (output_dir / f"non-source-id-{source_id:02x}.bin").write_bytes(payload) + continue + + stream_dir = output_dir / f"stream-{source_id:02x}" + stream_dir.mkdir(parents=True, exist_ok=True) + output = bytes(payload) + if prepend_analysis_sync and not output.startswith(ITM_HARDWARE_SYNC): + output = ITM_HARDWARE_SYNC + output + (stream_dir / f"{solution_set}.SWO.raw").write_bytes(output) + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("input", type=Path, help="memory-aligned *.TB.raw file") + parser.add_argument( + "--output-dir", + type=Path, + required=True, + help="directory for stream-/.SWO.raw files", + ) + parser.add_argument( + "--prepend-analysis-sync", + action="store_true", + help="prepend a synthetic ITM sync where the stream does not start with one", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_arguments() + data = args.input.read_bytes() + result = split_frames(data) + solution_set = args.input.name.removesuffix(".TB.raw") + write_outputs( + result, + args.output_dir, + solution_set, + args.prepend_analysis_sync, + ) + + print(f"input: {len(data)} bytes, {len(data) // FRAME_SIZE} frames") + print(f"formatter ID changes: {result.id_changes}") + print(f"unassigned before first ID: {len(result.unassigned)} bytes") + for source_id, payload in sorted(result.streams.items()): + kind = "source" if MIN_SOURCE_ID <= source_id <= MAX_SOURCE_ID else "non-source" + offsets = ", ".join(str(value) for value in sync_offsets(payload)) or "none" + print( + f"{kind} 0x{source_id:02x}: {len(payload)} bytes, " + f"ITM sync offsets: {offsets}" + ) + if args.prepend_analysis_sync: + print("analysis outputs have a synthetic leading ITM sync where required") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp index 4372647ad..4036542d4 100644 --- a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp +++ b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp @@ -131,6 +131,123 @@ std::size_t countOccurrences(std::string_view text, std::string_view value) return count; } +std::string normalizeGeneratedTextLineEndings(std::string text, std::string_view artifact) +{ + std::string normalized; + normalized.reserve(text.size()); + for (std::size_t offset = 0U; offset < text.size(); ++offset) { + if (text[offset] != '\r') { + normalized.push_back(text[offset]); + continue; + } + if (offset + 1U >= text.size() || text[offset + 1U] != '\n') { + throw std::runtime_error(std::string(artifact) + " contains a bare carriage return"); + } + } + return normalized; +} + +unsigned uuidHexValue(char value) +{ + if (value >= '0' && value <= '9') { + return static_cast(value - '0'); + } + if (value >= 'a' && value <= 'f') { + return static_cast(value - 'a') + 10U; + } + if (value >= 'A' && value <= 'F') { + return static_cast(value - 'A') + 10U; + } + throw std::runtime_error("CTF metadata trace UUID is not hexadecimal"); +} + +std::array normalizeCtfMetadataTraceUuid(std::string& metadata) +{ + constexpr std::string_view traceMarker{"trace {"}; + constexpr std::string_view uuidMarker{" uuid = \""}; + constexpr std::string_view normalizedUuid{"00000000-0000-0000-0000-000000000000"}; + const auto traceStart = metadata.find(traceMarker); + const auto traceEnd = metadata.find("\n};", traceStart); + const auto uuidMarkerPosition = metadata.find(uuidMarker, traceStart); + if (traceStart == std::string::npos || traceEnd == std::string::npos || uuidMarkerPosition == std::string::npos || + uuidMarkerPosition >= traceEnd) { + throw std::runtime_error("CTF metadata trace declaration has no UUID"); + } + + const auto uuidStart = uuidMarkerPosition + uuidMarker.size(); + if (uuidStart + normalizedUuid.size() + 2U > metadata.size() || + metadata.compare(uuidStart + normalizedUuid.size(), 2U, "\";") != 0) { + throw std::runtime_error("CTF metadata trace UUID is not in canonical form"); + } + + std::array uuid{}; + std::size_t textOffset = 0U; + for (std::size_t byte = 0U; byte < uuid.size(); ++byte) { + if (byte == 4U || byte == 6U || byte == 8U || byte == 10U) { + if (metadata[uuidStart + textOffset] != '-') { + throw std::runtime_error("CTF metadata trace UUID is not in canonical form"); + } + ++textOffset; + } + const auto high = uuidHexValue(metadata[uuidStart + textOffset++]); + const auto low = uuidHexValue(metadata[uuidStart + textOffset++]); + uuid[byte] = static_cast((high << 4U) | low); + } + if ((uuid[6U] & 0xf0U) != 0x40U || (uuid[8U] & 0xc0U) != 0x80U) { + throw std::runtime_error("CTF metadata trace UUID is not an RFC 4122 version-4 UUID"); + } + + metadata.replace(uuidStart, normalizedUuid.size(), normalizedUuid.data(), normalizedUuid.size()); + return uuid; +} + +void normalizeCtfStreamTraceUuid(std::vector& stream, const std::array& uuid) +{ + constexpr std::size_t uuidOffset = sizeof(std::uint32_t); + if (stream.empty()) { + throw std::runtime_error("CTF stream is empty"); + } + + std::size_t packetStart = 0U; + while (packetStart < stream.size()) { + if (CtfTestSupport::kCtfEventOffset > stream.size() - packetStart || + CtfTestSupport::readLe32(stream, packetStart) != CtfSchema::Magic) { + throw std::runtime_error("CTF stream has an invalid packet header"); + } + + const auto streamUuid = stream.begin() + static_cast(packetStart + uuidOffset); + if (!std::equal(uuid.begin(), uuid.end(), streamUuid)) { + throw std::runtime_error("CTF packet UUID does not match its metadata trace UUID"); + } + std::fill(streamUuid, streamUuid + static_cast(uuid.size()), 0U); + + const auto packetBits = CtfTestSupport::readLe32(stream, packetStart + CtfTestSupport::kCtfPacketHeaderSize); + if (packetBits % 8U != 0U) { + throw std::runtime_error("CTF packet size is not byte-aligned"); + } + const auto packetBytes = static_cast(packetBits / 8U); + if (packetBytes < CtfTestSupport::kCtfEventOffset || packetBytes > stream.size() - packetStart) { + throw std::runtime_error("CTF packet size exceeds the stream"); + } + packetStart += packetBytes; + } +} + +template +void expectMatchesGolden(const Container& expected, const Container& actual, std::string_view artifact) +{ + ASSERT_EQ(expected.size(), actual.size()) << artifact << " size differs from golden file"; + const auto mismatch = std::mismatch(expected.begin(), expected.end(), actual.begin()); + if (mismatch.first == expected.end()) { + return; + } + const auto offset = static_cast(std::distance(expected.begin(), mismatch.first)); + const auto expectedByte = static_cast(static_cast(*mismatch.first)); + const auto actualByte = static_cast(static_cast(*mismatch.second)); + ADD_FAILURE() << artifact << " differs from golden file at byte " << offset << ": expected 0x" << std::hex + << expectedByte << ", actual 0x" << actualByte; +} + TEST_F(CtraceIntegTests, GeneratesAllOutputs) { writeFile(workDirectory() / "Minimal.ctrace-run.yml", R"yml(ctrace-run: @@ -432,19 +549,45 @@ TEST_F(CtraceIntegTests, GeneratesRequestedOutputsAfterDecoderError) expectNonEmptyFile(workDirectory() / "Minimal.SWO.traceanalysis.xml"); } -TEST_F(CtraceIntegTests, ConvertsBlinkyFixtureAndSkipsUnsupportedTraceBusInput) +TEST_F(CtraceIntegTests, ConvertsBlinkyFixtureToGoldenOutputsAndSkipsUnsupportedTraceBusInput) { const auto fixtureDirectory = testDataDirectory() / "Blinky+Arm"; copyFixtureFile(fixtureDirectory, "Blinky+Arm.SWO.raw"); copyFixtureFile(fixtureDirectory, "Blinky+Arm.TB.raw"); - copyFixtureFile(fixtureDirectory, "Blinky+Arm.ctrace-run.yml"); - const auto result = run({"ctrace", workDirectory().string(), "--target", "Blinky+Arm", "--csv"}); + // The legacy pyTS configuration predates timestamps.clock. CTF requires it, and the captured CM7 ran at 480 MHz. + auto traceRun = readTextFile(fixtureDirectory / "Blinky+Arm.ctrace-run.yml"); + constexpr std::string_view legacyTimestampBlock{ + " timestamps:\n itm-prescaler: 1\n - pname: CM4"}; + constexpr std::string_view ctfTimestampBlock{ + " timestamps:\n clock: 480000000\n itm-prescaler: 1\n - pname: CM4"}; + const auto timestampPosition = traceRun.find(legacyTimestampBlock); + ASSERT_NE(std::string::npos, timestampPosition); + ASSERT_EQ(std::string::npos, traceRun.find(legacyTimestampBlock, timestampPosition + 1U)); + traceRun.replace(timestampPosition, legacyTimestampBlock.size(), ctfTimestampBlock.data(), ctfTimestampBlock.size()); + writeFile(workDirectory() / "Blinky+Arm.ctrace-run.yml", traceRun); + + const auto result = run({"ctrace", workDirectory().string(), "--target", "Blinky+Arm", "--all"}); EXPECT_EQ(1, result.exitCode) << result.stderrText; expectContains(result.stderrText, "skipping raw trace channel that is not implemented yet:"); expectContains(result.stderrText, "channel=TB"); EXPECT_EQ(readTextFile(fixtureDirectory / "Blinky+Arm.SWO.csv"), readTextFile(workDirectory() / "Blinky+Arm.SWO.csv")); + + const auto goldenDirectory = fixtureDirectory / "expected"; + auto metadata = + normalizeGeneratedTextLineEndings(readTextFile(workDirectory() / "Blinky+Arm.ctf" / "metadata"), "CTF metadata"); + const auto traceUuid = normalizeCtfMetadataTraceUuid(metadata); + auto stream = readBinaryFile(workDirectory() / "Blinky+Arm.ctf" / "stream_0"); + normalizeCtfStreamTraceUuid(stream, traceUuid); + expectMatchesGolden(readTextFile(goldenDirectory / "Blinky+Arm.ctf" / "metadata"), metadata, "CTF metadata"); + expectMatchesGolden(readBinaryFile(goldenDirectory / "Blinky+Arm.ctf" / "stream_0"), stream, + "CTF binary stream"); + expectMatchesGolden(readTextFile(goldenDirectory / "Blinky+Arm.SWO.traceanalysis.xml"), + normalizeGeneratedTextLineEndings( + readTextFile(workDirectory() / "Blinky+Arm.SWO.traceanalysis.xml"), "Trace Compass XML"), + "Trace Compass XML"); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Blinky+Arm.TB.csv")); EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Blinky+Arm.TB.traceanalysis.xml")); EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Blinky+Arm.TB.ctf")); diff --git a/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp b/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp index 648188bfa..4d25e105c 100644 --- a/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp +++ b/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp @@ -27,6 +27,15 @@ using OpenCsdSessionTestSupport::ScriptedDecoderHarness; using OpenCsdTestSupport::CollectingOpenCsdElementSink; +TEST(CtraceUnitTests, testOpenCsdItmDecoderConstructsDefaultSession) +{ + CollectingOpenCsdElementSink sink; + OpenCsdItmDecoder decoder(sink); + + EXPECT_EQ(decoder.finish().bytesIn, 0U); + EXPECT_FALSE(sink.hasIssue(TraceIssueCode::OpenCsdInitializationError)); +} + TEST(CtraceUnitTests, testOpenCsdItmDecoderChunksAndFinishesOnce) { ScriptedDecoderHarness harness; diff --git a/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp b/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp index 27682fe32..e757867b7 100644 --- a/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp +++ b/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp @@ -199,6 +199,37 @@ TEST(CtraceUnitTests, testCtraceRunMetaWarnsForSingleSetupIdentityConflicts) EXPECT_FALSE(meta.timestampClockHz().has_value()); } +TEST(CtraceUnitTests, testCtraceRunMetaBindsOneNamedReferenceToUnnamedSetup) +{ + TraceRunConfig config; + config.setups.push_back(makeTimestampSetup(std::nullopt, 100U)); + config.references.push_back(makeReference("itm", "core", 1U, {1U})); + + const auto meta = CtraceRunMeta::fromConfig(config); + + ASSERT_EQ(meta.sources().size(), 1U); + EXPECT_EQ(meta.sources().front().processorName, std::optional("core")); + ASSERT_EQ(meta.timestampsByTraceBusId().size(), 1U); + EXPECT_EQ(meta.timestampsByTraceBusId().at(1U).processorName, std::optional("core")); + EXPECT_EQ(meta.timestampsByTraceBusId().at(1U).clockHz, std::optional(100U)); +} + +TEST(CtraceUnitTests, testCtraceRunMetaBindsStreamlessTimestampToInternalRoute) +{ + TraceRunConfig config; + config.setups.push_back(makeTimestampSetup("core", 100U)); + config.references.push_back(makeReference("itm", "core", std::nullopt, {}, "core/timestamps")); + + const auto meta = CtraceRunMeta::fromConfig(config); + + EXPECT_TRUE(meta.sources().empty()); + ASSERT_EQ(meta.timestampsByTraceBusId().size(), 1U); + EXPECT_EQ(meta.timestampsByTraceBusId().at(0U).processorName, std::optional("core")); + EXPECT_EQ(meta.timestampsByTraceBusId().at(0U).clockHz, std::optional(100U)); + ASSERT_EQ(meta.timestampPrescalersByTraceBusId().size(), 1U); + EXPECT_EQ(meta.timestampPrescalersByTraceBusId().at(0U), 1U); +} + TEST(CtraceUnitTests, testCtraceRunMetaMapsDistinctProcessorSettings) { TraceRunConfig config; diff --git a/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp b/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp index e0fd23d05..f5d3a54cd 100644 --- a/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp +++ b/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp @@ -310,7 +310,7 @@ TEST(CtraceUnitTests, TraceRunReaderRejectsMalformedReferenceRoutes) } } -TEST(CtraceUnitTests, TraceRunReaderIgnoresNullOptionalReferenceValues) +TEST(CtraceUnitTests, TraceRunReaderIgnoresNullAndNonScalarOptionalReferenceValues) { TraceRunFixture file("ctrace-run-reader-null-optional-reference-test"); const auto config = file.read(R"yml(ctrace-run: @@ -325,13 +325,14 @@ TEST(CtraceUnitTests, TraceRunReaderIgnoresNullOptionalReferenceValues) address: null data-type: null size: null - label: null + label: [] info: null warning: null error: null - type: itm ctrace-ref: itm source: [1, null] + label: null info: [note, null] warning: [null] error: [null] @@ -355,6 +356,7 @@ TEST(CtraceUnitTests, TraceRunReaderIgnoresNullOptionalReferenceValues) EXPECT_TRUE(emptyRoute.error.empty()); EXPECT_EQ(config.references[1].sources, (std::vector{1U})); + EXPECT_FALSE(config.references[1].label.has_value()); EXPECT_EQ(config.references[1].info, (std::vector{"note"})); EXPECT_TRUE(config.references[1].warning.empty()); EXPECT_TRUE(config.references[1].error.empty()); From 704ad442f7fe17de7a8ca507ef36615972bd1776 Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Wed, 9 Sep 2026 19:54:09 +0200 Subject: [PATCH 02/31] feat(ctrace): normalize trace-run input routes --- .../ctrace/docs/multicore-multisource-plan.md | 4 +- .../ctrace/src/control/TraceDirectoryJob.cpp | 3 + tools/ctrace/src/tracerun/CtraceRunMeta.cpp | 1125 ++++++++++++++--- tools/ctrace/src/tracerun/CtraceRunMeta.h | 44 +- tools/ctrace/src/tracerun/TraceRunConfig.h | 33 +- .../src/tracerun/YmlTraceRunConfigReader.cpp | 230 +++- .../test/integration/src/CtraceIntegTests.cpp | 3 +- .../src/control/TraceDirectoryJobTests.cpp | 29 + .../src/output/OutputRequirementsTests.cpp | 120 +- .../src/output/ctf/CtfBundleOutputTests.cpp | 34 +- .../unit/src/tracerun/CtraceRunMetaTests.cpp | 745 ++++++++++- .../tracerun/TraceRunConfigReaderTests.cpp | 392 +++++- 12 files changed, 2408 insertions(+), 354 deletions(-) diff --git a/tools/ctrace/docs/multicore-multisource-plan.md b/tools/ctrace/docs/multicore-multisource-plan.md index 91277d395..9061c3876 100644 --- a/tools/ctrace/docs/multicore-multisource-plan.md +++ b/tools/ctrace/docs/multicore-multisource-plan.md @@ -536,8 +536,8 @@ Phase 0 -> Phase 1 -> Phase 2 -> Phase 3 -> Phase 4 | Phase | Deliverable | Status | | :--- | :--- | :--- | | 0 | Baseline, fixtures, goldens, coverage gate | Complete | -| 1 | Trace-run declaration and route normalization | Next | -| 2 | Raw-input discovery and preflight | Pending | +| 1 | Trace-run declaration and route normalization | Complete | +| 2 | Raw-input discovery and preflight | Next | | 3 | Route-aware semantic state, diagnostics, and CSV | Pending | | 4 | CTF descriptors and metadata model | Pending | | 5 | Multi-stream CTF bundle and Trace Compass policy | Pending | diff --git a/tools/ctrace/src/control/TraceDirectoryJob.cpp b/tools/ctrace/src/control/TraceDirectoryJob.cpp index de7c9cf8a..7fc2b3ad1 100644 --- a/tools/ctrace/src/control/TraceDirectoryJob.cpp +++ b/tools/ctrace/src/control/TraceDirectoryJob.cpp @@ -118,6 +118,9 @@ void TraceDirectoryJob::run() reportConsumedReferenceDiagnostics(config, m_diagnostics); const auto ctraceRunMeta = CtraceRunMeta::fromConfig(config); reportTraceRunWarnings(ctraceRunMeta, m_diagnostics); + if (config.traceFormat == TraceRunFormat::Formatted) { + throw std::runtime_error("formatted trace input is not enabled yet"); + } const auto rawInputs = TraceRunDiscovery::rawInputs(configFile); bool processedSolutionSet = false; for (const auto& rawInput : rawInputs) { diff --git a/tools/ctrace/src/tracerun/CtraceRunMeta.cpp b/tools/ctrace/src/tracerun/CtraceRunMeta.cpp index f5567fa83..aebfade87 100644 --- a/tools/ctrace/src/tracerun/CtraceRunMeta.cpp +++ b/tools/ctrace/src/tracerun/CtraceRunMeta.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include /** @brief Accumulates normalized metadata for one processor. */ @@ -26,15 +28,56 @@ struct ProcessorMeta { std::optional timestampClockHz; std::optional timestampClockError; std::optional timestampPrescaler; + bool itmEnableConflict = false; std::optional itmEnableMask; + std::optional itmEnableError; }; +/** @brief Derives a processor name from a one-segment `[pname/]feature` reference path. */ +static std::optional referencePathProcessorName(const TraceRunReference& reference) +{ + const auto separator = reference.ctraceRef.find('/'); + if (separator == 0U || separator == std::string::npos || separator != reference.ctraceRef.rfind('/')) { + return std::nullopt; + } + + const auto leaf = std::string_view(reference.ctraceRef).substr(separator + 1U); + const auto indexedLeaf = [&](const std::string_view prefix) { + return leaf.size() > prefix.size() && leaf.substr(0U, prefix.size()) == prefix && + std::all_of(leaf.begin() + static_cast(prefix.size()), leaf.end(), + [](const char character) { return character >= '0' && character <= '9'; }); + }; + const auto processorScoped = + (reference.type == "itm" && (leaf == "itm" || leaf == "timestamps")) || + (reference.type == "dwt" && (leaf == "timestamps" || leaf == "synchronization" || indexedLeaf("data#"))) || + (reference.type == "exception" && leaf == "exceptions") || + ((reference.type == "event" || reference.type == "pmu") && indexedLeaf("events#")) || + (reference.type == "pcsample" && leaf == "pcsampling") || (reference.type == "overflow" && leaf == "overflow") || + (reference.type == "global_ts" && leaf == "timesync"); + if (!processorScoped) { + return std::nullopt; + } + return reference.ctraceRef.substr(0U, separator); +} + +/** @brief Resolves processor evidence without repeating a previously performed consistency check. */ +static std::optional uncheckedReferenceProcessorName(const TraceRunReference& reference) +{ + const auto explicitName = TraceRunSchema::normalizedProcessorName(reference.processorName); + return explicitName.has_value() ? explicitName : referencePathProcessorName(reference); +} + +/** @brief Resolves and cross-checks explicit and path-derived processor evidence. */ +static std::optional checkedReferenceProcessorName(const TraceRunConfig& config, + const TraceRunReference& reference); + /** @brief Identifies one processor and its optional Trace Bus ID. */ struct ProcessorIdentity { bool multipleProcessors = false; std::optional singleProcessorName; bool constrainedBySetups = false; std::set setupNames; + std::set singleProcessorStreams; /** @brief Tests whether a reference is consistent with the active setups. */ bool accepts(const TraceRunReference& reference) const @@ -42,13 +85,29 @@ struct ProcessorIdentity { if (!constrainedBySetups || setupNames.empty()) { return true; } - const auto name = TraceRunSchema::normalizedProcessorName(reference.processorName); + const auto name = uncheckedReferenceProcessorName(reference); + if (!multipleProcessors && singleProcessorName.has_value() && setupNames.size() > 1U) { + if (name.has_value()) { + return name == singleProcessorName; + } + return reference.stream.has_value() && + singleProcessorStreams.find(*reference.stream) != singleProcessorStreams.end(); + } if (!name.has_value()) { return setupNames.size() == 1U; } return setupNames.find(*name) != setupNames.end(); } + /** @brief Tests whether one active setup belongs to the selected SINGLE processor. */ + bool acceptsSetup(const TraceRunSetup& setup) const + { + if (!constrainedBySetups || multipleProcessors || !singleProcessorName.has_value() || setupNames.size() <= 1U) { + return true; + } + return TraceRunSchema::normalizedProcessorName(setup.processorName) == singleProcessorName; + } + /** @brief Resolves an optional processor name to its canonical binding name. */ std::optional canonicalName(const std::optional& name) const { @@ -57,10 +116,23 @@ struct ProcessorIdentity { } return TraceRunSchema::normalizedProcessorName(name); } + + /** @brief Resolves a reference's canonical processor binding. */ + std::optional canonicalReferenceName(const TraceRunReference& reference) const + { + return multipleProcessors ? uncheckedReferenceProcessorName(reference) : singleProcessorName; + } }; using ReferenceProblem = TraceRunSchema::ReferenceProblem; +/** @brief Tests whether producer diagnostics permit discarding only invalid source metadata. */ +static bool isDiscardableSourceProblem(const TraceRunReference& reference, ReferenceProblem problem) +{ + return (!reference.stream.has_value() || CoreSight::isAtbTraceId(*reference.stream)) && !reference.error.empty() && + (problem == ReferenceProblem::DuplicateSource || problem == ReferenceProblem::InvalidItmSource); +} + /** @brief Formats a trace-run validation error with source location. */ static std::string configError(const TraceRunConfig& config, std::size_t line, const std::string& message) { @@ -71,6 +143,32 @@ static std::string configError(const TraceRunConfig& config, std::size_t line, c return location + ": " + message; } +/** @brief Merges one optional clock fragment without treating an absent scalar as a conflict. */ +static void mergeTimestampClock(std::optional& clockHz, std::optional& clockError, + const std::optional& candidateClock, + const std::optional& candidateError, + const std::string_view conflictMessage) +{ + if (candidateError.has_value()) { + if (clockError.has_value() && clockError != candidateError) { + clockError = conflictMessage; + } else if (!clockError.has_value()) { + clockError = candidateError; + } + clockHz.reset(); + return; + } + if (clockError.has_value() || !candidateClock.has_value()) { + return; + } + if (clockHz.has_value() && clockHz != candidateClock) { + clockHz.reset(); + clockError = conflictMessage; + } else { + clockHz = candidateClock; + } +} + /** @brief Builds warning context for one trace-run reference. */ static std::vector> warningContext(const TraceRunReference& reference) { @@ -115,30 +213,44 @@ static std::string referenceProblemMessage(const TraceRunConfig& config, const T : "ITM source must be between 0 and 31"); } +static bool setupContainsReference(const TraceRunSetup& setup, const TraceRunReference& reference); + /** @brief Tests whether a setup contributes metadata to any consumed route. */ static bool consumesSetup(const TraceRunConfig& config, const TraceRunSetup& setup) { + if (setup.disabled) { + return false; + } if (setup.timestamps.has_value() || setup.itm.has_value()) { return true; } for (const auto& reference : config.references) { - if (!TraceRunSchema::isUsableReference(reference) || reference.type != "dwt" || - !TraceRunSchema::processorNamesMayBind(setup.processorName, reference.processorName)) { + if (!TraceRunSchema::consumesReferenceMetadata(reference.type)) { continue; } - const auto index = reference.dataSetupIndex; - if (index.has_value() && *index < setup.data.size()) { + if (!setup.featurePaths.empty() && setupContainsReference(setup, reference)) { return true; } + if (reference.type == "dwt" && TraceRunSchema::isUsableReference(reference) && + TraceRunSchema::processorNamesMayBind(setup.processorName, uncheckedReferenceProcessorName(reference))) { + const auto index = reference.dataSetupIndex; + if (setup.dataError.has_value() || + (index.has_value() && *index < setup.data.size() && setup.data[*index].present)) { + return true; + } + } } return false; } -/** @brief Tests whether a reference can bind a processor to one stream. */ -static bool isUsableStreamBinding(const TraceRunReference& reference) +/** @brief Tests whether retained reference metadata can identify one processor. */ +static bool isUsableProcessorBinding(const TraceRunReference& reference) { - return TraceRunSchema::contributesStreamBinding(reference) && - TraceRunSchema::referenceProblem(reference) == ReferenceProblem::None; + const auto problem = TraceRunSchema::referenceProblem(reference); + const auto structurallyUsable = problem == ReferenceProblem::None || isDiscardableSourceProblem(reference, problem); + return structurallyUsable && TraceRunSchema::consumesReferenceMetadata(reference.type) && + (uncheckedReferenceProcessorName(reference).has_value() || reference.stream.has_value() || + TraceRunSchema::contributesStreamBinding(reference)); } /** @brief Resolves the unambiguous processor identity of a trace-run file. */ @@ -146,38 +258,29 @@ static ProcessorIdentity processorIdentity(const TraceRunConfig& config, std::ve { // Active setups define the authoritative processor set when present. std::set setupNames; - std::set> uniqueSetupNames; bool unnamedSetup = false; std::size_t setupCount = 0U; - const TraceRunSetup* singleActiveSetup = nullptr; for (const auto& setup : config.setups) { if (!consumesSetup(config, setup)) { continue; } - ++setupCount; - singleActiveSetup = &setup; const auto name = TraceRunSchema::normalizedProcessorName(setup.processorName); - if (!uniqueSetupNames.insert(name).second) { - throw std::runtime_error(configError(config, setup.line, - (setup.line > 0U ? "duplicate active 'ctrace-setup' for pname '" - : "duplicate active ctrace-setup for pname '") + - name.value_or("") + "'")); - } if (name.has_value()) { setupNames.insert(*name); } else { unnamedSetup = true; } } + setupCount = !setupNames.empty() ? setupNames.size() : (unnamedSetup ? 1U : 0U); // Only usable stream bindings may contribute fallback processor identities. std::set referenceNames; bool unnamedReference = false; for (const auto& reference : config.references) { - if (!isUsableStreamBinding(reference)) { + if (!isUsableProcessorBinding(reference)) { continue; } - const auto name = TraceRunSchema::normalizedProcessorName(reference.processorName); + const auto name = checkedReferenceProcessorName(config, reference); if (name.has_value()) { referenceNames.insert(*name); } else { @@ -191,18 +294,45 @@ static ProcessorIdentity processorIdentity(const TraceRunConfig& config, std::ve throw std::runtime_error(config.path + ": pname is required for every ctrace-setup in a multi-processor configuration"); } + std::set matchingReferenceNames; for (const auto& reference : config.references) { - if (!isUsableStreamBinding(reference)) { + if (!isUsableProcessorBinding(reference)) { continue; } - const auto name = TraceRunSchema::normalizedProcessorName(reference.processorName); - if (!name.has_value()) { + const auto name = checkedReferenceProcessorName(config, reference); + if (name.has_value() && setupNames.find(*name) == setupNames.end()) { addRootInconsistency(warnings, - "ignoring ctrace-ref without pname because multiple ctrace-setup processors are active", + "ignoring ctrace-ref pname '" + *name + "' because it has no matching ctrace-setup", warningContext(reference)); - } else if (setupNames.find(*name) == setupNames.end()) { - addRootInconsistency(warnings, "ignoring ctrace-ref pname '" + *name + - "' because it has no matching ctrace-setup", + } else if (name.has_value()) { + matchingReferenceNames.insert(*name); + } + } + if (matchingReferenceNames.size() == 1U) { + const auto selectedName = *matchingReferenceNames.begin(); + std::set selectedStreams; + for (const auto& reference : config.references) { + if (!isUsableProcessorBinding(reference) || checkedReferenceProcessorName(config, reference) != selectedName || + !reference.stream.has_value()) { + continue; + } + selectedStreams.insert(*reference.stream); + } + for (const auto& reference : config.references) { + if (!isUsableProcessorBinding(reference) || checkedReferenceProcessorName(config, reference).has_value()) { + continue; + } + if (!reference.stream.has_value() || selectedStreams.find(*reference.stream) == selectedStreams.end()) { + addRootInconsistency(warnings, "ignoring ctrace-ref without pname because its processor binding is ambiguous", + warningContext(reference)); + } + } + return {false, selectedName, true, setupNames, selectedStreams}; + } + for (const auto& reference : config.references) { + if (isUsableProcessorBinding(reference) && !checkedReferenceProcessorName(config, reference).has_value()) { + addRootInconsistency(warnings, + "ignoring ctrace-ref without pname because multiple ctrace-setup processors are active", warningContext(reference)); } } @@ -210,25 +340,25 @@ static ProcessorIdentity processorIdentity(const TraceRunConfig& config, std::ve } if (setupCount == 1U) { - const auto setupName = TraceRunSchema::normalizedProcessorName(singleActiveSetup->processorName); + const auto setupName = setupNames.empty() ? std::nullopt : std::optional(*setupNames.begin()); if (setupName.has_value()) { for (const auto& reference : config.references) { - if (!isUsableStreamBinding(reference)) { + if (!isUsableProcessorBinding(reference)) { continue; } - const auto name = TraceRunSchema::normalizedProcessorName(reference.processorName); + const auto name = checkedReferenceProcessorName(config, reference); if (name.has_value() && *name != *setupName) { - addRootInconsistency(warnings, "ignoring ctrace-ref pname '" + *name + - "' because it does not match ctrace-setup pname '" + *setupName + "'", + addRootInconsistency(warnings, + "ignoring ctrace-ref pname '" + *name + + "' because it does not match ctrace-setup pname '" + *setupName + "'", warningContext(reference)); } } return {false, setupName, true, setupNames}; } if (referenceNames.size() > 1U) { - addRootInconsistency(warnings, - "ignoring conflicting ctrace-ref pnames because the single ctrace-setup has no pname", - {{"ctraceRefPnames", std::to_string(referenceNames.size())}}); + throw std::runtime_error(config.path + + ": unformatted SINGLE trace requires one unambiguous processor metadata binding"); } return { false, @@ -253,31 +383,64 @@ static ProcessorIdentity processorIdentity(const TraceRunConfig& config, std::ve }; } -/** @brief Resolves the required data setup index of one prevalidated DWT route. */ -static const TraceRunDataSetup* referencedDataSetup(const TraceRunConfig& config, const TraceRunReference& reference, - std::size_t index) +/** @brief Resolves compatible data metadata from every matching active setup fragment. */ +static std::optional referencedDataSetup(const TraceRunConfig& config, + const TraceRunReference& reference, std::size_t index) { + std::optional resolved; + std::optional effectiveSize; + bool conflict = false; for (const auto& setup : config.setups) { - if (!TraceRunSchema::processorNamesMayBind(setup.processorName, reference.processorName)) { + if (setup.disabled || + !TraceRunSchema::processorNamesMayBind(setup.processorName, uncheckedReferenceProcessorName(reference))) { + continue; + } + TraceRunDataSetup malformedContainer; + malformedContainer.sizeError = setup.dataError; + const auto* candidate = + setup.dataError.has_value() ? &malformedContainer : (index < setup.data.size() ? &setup.data[index] : nullptr); + if (candidate == nullptr || !candidate->present) { continue; } - const auto* candidate = index < setup.data.size() ? &setup.data[index] : nullptr; - if (candidate != nullptr) { - return candidate; + if (!resolved.has_value()) { + resolved = *candidate; + effectiveSize = candidate->size.value_or(TraceRunSchema::kDefaultDwtDataSize); + } else { + const auto candidateSize = candidate->size.value_or(TraceRunSchema::kDefaultDwtDataSize); + if (*effectiveSize != candidateSize) { + conflict = true; + } + } + if (candidate->sizeError.has_value()) { + if (resolved->sizeError.has_value() && resolved->sizeError != candidate->sizeError) { + conflict = true; + } else if (!resolved->sizeError.has_value()) { + resolved->sizeError = candidate->sizeError; + } } } - return nullptr; + if (resolved.has_value()) { + resolved->size = effectiveSize; + } + if (conflict) { + resolved->size.reset(); + resolved->sizeError = "conflicting active ctrace-setup data.size values"; + } + return resolved; } /** @brief Converts one validated reference into normalized source metadata. */ static CtraceRunSourceMeta sourceMeta(const TraceRunConfig& config, const TraceRunReference& reference, std::uint32_t source, const ProcessorIdentity& processorIdentity) { - const auto* dataSetup = - reference.type == "dwt" ? referencedDataSetup(config, reference, *reference.dataSetupIndex) : nullptr; CtraceRunSourceMeta meta; meta.type = reference.type; - meta.processorName = processorIdentity.canonicalName(reference.processorName); + meta.processorName = processorIdentity.canonicalReferenceName(reference); + auto boundReference = reference; + boundReference.processorName = meta.processorName; + const auto dataSetup = reference.type == "dwt" + ? referencedDataSetup(config, boundReference, *reference.dataSetupIndex) + : std::optional{}; meta.traceBusId = static_cast(reference.stream.value_or(0U)); meta.source = source; meta.label = reference.label; @@ -294,7 +457,7 @@ static CtraceRunSourceMeta sourceMeta(const TraceRunConfig& config, const TraceR if (reference.dataSize.has_value() || reference.dataSizeError.has_value()) { meta.dataSize = reference.dataSize.value_or(TraceRunSchema::kDefaultDwtDataSize); meta.dataSizeError = reference.dataSizeError; - } else if (dataSetup != nullptr) { + } else if (dataSetup.has_value()) { meta.dataSize = dataSetup->size.value_or(TraceRunSchema::kDefaultDwtDataSize); meta.dataSizeError = dataSetup->sizeError; } @@ -314,20 +477,28 @@ static ProcessorMeta& processorMeta(std::vector& processors, cons return processors.back(); } -template -/** @brief Returns a setting only when all relevant processors agree. */ -static std::optional commonProcessorSetting(const std::vector& processors, - const std::optional ProcessorMeta::* member) +/** @brief Returns a timestamp clock only when every processor candidate supplies the same value. */ +static std::optional commonTimestampClock(const std::vector& processors) { - std::optional common; + std::optional common; for (const auto& processor : processors) { - if (!processor.timestampsEnabled) { - continue; - } - const auto& candidate = processor.*member; - if (!candidate.has_value()) { + const auto candidate = processor.timestampsEnabled ? processor.timestampClockHz : std::nullopt; + if (!candidate.has_value() || (common.has_value() && common != candidate)) { return std::nullopt; } + common = candidate; + } + return common; +} + +/** @brief Returns a prescaler only when every processor candidate agrees after applying the default. */ +static std::optional commonTimestampPrescaler(const std::vector& processors) +{ + std::optional common; + for (const auto& processor : processors) { + const auto candidate = processor.timestampsEnabled + ? processor.timestampPrescaler.value_or(TraceRunSchema::kDefaultTimestampPrescaler) + : TraceRunSchema::kDefaultTimestampPrescaler; if (common.has_value() && common != candidate) { return std::nullopt; } @@ -336,183 +507,672 @@ static std::optional commonProcessorSetting(const std::vector& processors, - const std::optional& name) +/** @brief Returns the ITM enable mask only when all processor candidates agree. */ +static std::optional commonItmEnableMask(const std::vector& processors) { - const auto found = std::find_if(processors.begin(), processors.end(), - [&](const ProcessorMeta& processor) { return processor.name == name; }); - return found != processors.end() ? &*found : nullptr; + std::optional common; + for (const auto& processor : processors) { + if (!processor.itmEnableMask.has_value() || (common.has_value() && common != processor.itmEnableMask)) { + return std::nullopt; + } + common = processor.itmEnableMask; + } + return common; } -/** @brief Stores the result of binding one processor to a trace stream. */ -struct ResolvedStreamBinding { - std::size_t line = 0U; - std::uint8_t traceBusId = 0U; - std::optional processorName; - std::string ctraceRef; - const ProcessorMeta* processor = nullptr; -}; +/** @brief Retains a common clock error or describes ambiguous SINGLE clock candidates. */ +static std::optional commonTimestampClockError(const std::vector& processors) +{ + bool found = false; + std::optional clockHz; + std::optional clockError; + for (const auto& processor : processors) { + const auto candidateClock = processor.timestampsEnabled ? processor.timestampClockHz : std::nullopt; + const auto candidateError = processor.timestampsEnabled ? processor.timestampClockError : std::nullopt; + if (found && (clockHz != candidateClock || clockError != candidateError)) { + return "unformatted SINGLE trace has ambiguous timestamps.clock values across processor candidates"; + } + found = true; + clockHz = candidateClock; + clockError = candidateError; + } + return clockError; +} -/** @brief Builds warning context for one resolved stream binding. */ -static std::vector> warningContext(const ResolvedStreamBinding& binding) +/** @brief Returns the feature leaf selected by one ctrace reference path. */ +static std::string_view referenceLeaf(const std::string_view& path) { - std::vector> context{ - {"ctraceRef", binding.ctraceRef}, - {"stream", std::to_string(binding.traceBusId)}, - }; - if (binding.line > 0U) { - context.emplace_back("line", std::to_string(binding.line)); + const auto separator = path.rfind('/'); + return separator == std::string_view::npos ? path : path.substr(separator + 1U); +} + +/** @brief Tests whether a ctrace reference uses the specified `[pname/]feature` path form. */ +static bool hasFeaturePath(const TraceRunReference& reference, const std::string_view& leaf) +{ + const auto separator = reference.ctraceRef.find('/'); + if (separator == std::string::npos) { + return reference.ctraceRef == leaf; } - if (binding.processorName.has_value()) { - context.emplace_back("pname", *binding.processorName); + return separator > 0U && separator == reference.ctraceRef.rfind('/') && + std::string_view(reference.ctraceRef).substr(separator + 1U) == leaf; +} + +/** @brief Tests whether a feature leaf contains one non-empty decimal index. */ +static bool isIndexedFeature(const std::string_view& leaf, const std::string_view& prefix) +{ + if (leaf.size() <= prefix.size() || leaf.substr(0U, prefix.size()) != prefix) { + return false; } - return context; + return std::all_of(leaf.begin() + static_cast(prefix.size()), leaf.end(), + [](const char character) { return character >= '0' && character <= '9'; }); +} + +/** @brief Tests whether a reference path denotes the authoritative processor ITM anchor. */ +static bool hasProcessorItmPath(const TraceRunReference& reference) +{ + return hasFeaturePath(reference, "itm"); +} + +/** @brief Tests whether one reference is an authoritative processor ITM anchor. */ +static bool isProcessorItmAnchor(const TraceRunReference& reference) +{ + return hasProcessorItmPath(reference) && reference.type == "itm"; +} + +/** @brief Tests whether one reference is permitted to establish a formatted ITM route without an anchor. */ +static bool isFormattedRouteFallback(const TraceRunReference& reference) +{ + const auto leaf = referenceLeaf(reference.ctraceRef); + const auto validPath = hasFeaturePath(reference, leaf); + return validPath && + ((reference.type == "dwt" && isIndexedFeature(leaf, "data#") && reference.dataSetupIndex.has_value()) || + ((reference.type == "itm" || reference.type == "dwt") && leaf == "timestamps") || + (reference.type == "exception" && leaf == "exceptions") || + ((reference.type == "event" || reference.type == "pmu") && isIndexedFeature(leaf, "events#")) || + (reference.type == "pcsample" && leaf == "pcsampling") || + (reference.type == "dwt" && leaf == "synchronization")); +} + +/** @brief Tests whether one reference may describe an already established ITM route. */ +static bool describesFormattedRoute(const TraceRunReference& reference) +{ + return isProcessorItmAnchor(reference) || isFormattedRouteFallback(reference) || + (reference.type == "overflow" && hasFeaturePath(reference, "overflow")) || + (reference.type == "global_ts" && hasFeaturePath(reference, "timesync")); } -/** @brief Resolves validated processor-to-stream bindings for all references. */ -static std::vector resolveStreamBindings(const TraceRunConfig& config, - const ProcessorIdentity& processorIdentity, - const std::vector& processors) +/** @brief Retains all producer diagnostics independently from normalized route validity. */ +static void appendReferenceDiagnostics(const TraceRunReference& reference, + std::vector& diagnostics) { - std::vector bindings; + const auto append = [&](CtraceRunReferenceDiagnostic::Severity severity, const std::vector& messages) { + for (const auto& message : messages) { + diagnostics.push_back({ + severity, + message, + reference.ctraceRef, + TraceRunSchema::normalizedProcessorName(reference.processorName), + reference.stream, + reference.line, + }); + } + }; + append(CtraceRunReferenceDiagnostic::Severity::Info, reference.info); + append(CtraceRunReferenceDiagnostic::Severity::Warning, reference.warning); + append(CtraceRunReferenceDiagnostic::Severity::Error, reference.error); +} + +/** @brief Retains all producer diagnostics independently from normalized route validity. */ +static std::vector collectReferenceDiagnostics(const TraceRunConfig& config) +{ + std::vector diagnostics; for (const auto& reference : config.references) { - if (!isUsableStreamBinding(reference) || !processorIdentity.accepts(reference)) { + appendReferenceDiagnostics(reference, diagnostics); + } + return diagnostics; +} + +/** @brief Indexes active setup fragments by their optional processor identity. */ +struct ActiveSetupIndex { + std::vector fragments; + std::set namedProcessors; + bool hasUnnamedProcessor = false; + + std::size_t processorGroupCount() const + { + return !namedProcessors.empty() ? namedProcessors.size() : (hasUnnamedProcessor ? 1U : 0U); + } +}; + +/** @brief Collects active setup fragments without treating repeated processor fragments as duplicates. */ +static ActiveSetupIndex activeSetupIndex(const TraceRunConfig& config) +{ + ActiveSetupIndex index; + for (const auto& setup : config.setups) { + if (!consumesSetup(config, setup)) { continue; } - const auto processorName = processorIdentity.canonicalName(reference.processorName); - const auto traceBusId = static_cast(reference.stream.value_or(0U)); - bindings.push_back({ - reference.line, - traceBusId, - processorName, - reference.ctraceRef, - findProcessor(processors, processorName), - }); + index.fragments.push_back(&setup); + const auto name = TraceRunSchema::normalizedProcessorName(setup.processorName); + if (name.has_value()) { + index.namedProcessors.insert(*name); + } else { + index.hasUnnamedProcessor = true; + } } - return bindings; + return index; } -static std::map -buildTimestampsByTraceBusId(const std::vector& bindings, - std::vector& warnings) +/** @brief Resolves and cross-checks processor evidence carried by one formatted reference. */ +static std::optional checkedReferenceProcessorName(const TraceRunConfig& config, + const TraceRunReference& reference) { - std::map result; - for (const auto& binding : bindings) { - CtraceRunTimestampMeta candidate; - candidate.processorName = binding.processorName; - if (binding.processor != nullptr && binding.processor->timestampsEnabled) { - candidate.clockHz = binding.processor->timestampClockHz; - candidate.clockError = binding.processor->timestampClockError; + const auto explicitName = TraceRunSchema::normalizedProcessorName(reference.processorName); + const auto pathName = referencePathProcessorName(reference); + if (explicitName.has_value() && pathName.has_value() && explicitName != pathName) { + throw std::runtime_error( + configError(config, reference.line, "ctrace-ref path processor conflicts with pname '" + *explicitName + "'")); + } + return explicitName.has_value() ? explicitName : pathName; +} + +/** @brief Resolves a formatted reference to an explicit or uniquely inferred processor identity. */ +static std::optional formattedProcessorName(const TraceRunConfig& config, const ActiveSetupIndex& setups, + const TraceRunReference& reference) +{ + const auto referenceName = checkedReferenceProcessorName(config, reference); + if (referenceName.has_value()) { + if (setups.fragments.empty() || setups.namedProcessors.find(*referenceName) != setups.namedProcessors.end() || + (setups.namedProcessors.empty() && setups.hasUnnamedProcessor)) { + return referenceName; } + throw std::runtime_error( + configError(config, reference.line, + "ctrace-ref pname '" + *referenceName + "' has no matching active ctrace-setup processor")); + } + if (setups.processorGroupCount() == 1U) { + return setups.namedProcessors.empty() ? std::nullopt : std::optional(*setups.namedProcessors.begin()); + } + if (setups.processorGroupCount() == 0U) { + return std::nullopt; + } + throw std::runtime_error(configError( + config, reference.line, + "pname is required for a formatted ctrace-ref when multiple active ctrace-setup processors are available")); +} - const auto [found, inserted] = result.emplace(binding.traceBusId, candidate); - if (inserted || (found->second.processorName == candidate.processorName && - found->second.clockHz == candidate.clockHz && found->second.clockError == candidate.clockError)) { - continue; +/** @brief Tests whether a setup feature path resolves one reference within the same fragment. */ +static bool setupContainsReference(const TraceRunSetup& setup, const TraceRunReference& reference) +{ + const auto referenceName = uncheckedReferenceProcessorName(reference); + const auto setupName = TraceRunSchema::normalizedProcessorName(setup.processorName); + if (referenceName.has_value() && setupName.has_value() && referenceName != setupName) { + return false; + } + auto referencePath = reference.ctraceRef; + if (referencePath.find('/') == std::string::npos && referenceName.has_value()) { + referencePath = *referenceName + "/" + referencePath; + } + const auto matchesFeaturePath = [](const std::string_view setupPath, const std::string_view referencePath) { + return setupPath == referencePath || + (referencePath.size() > setupPath.size() && referencePath.substr(0U, setupPath.size()) == setupPath && + (referencePath[setupPath.size()] == '/' || referencePath[setupPath.size()] == '#')); + }; + if (std::any_of(setup.featurePaths.begin(), setup.featurePaths.end(), + [&](const std::string& featurePath) { return matchesFeaturePath(featurePath, referencePath); })) { + return true; + } + if (!setupName.has_value()) { + auto relativePath = std::string_view(referencePath); + auto separator = relativePath.find('/'); + if (referenceName.has_value()) { + const auto expectedPrefix = *referenceName + "/"; + separator = relativePath.substr(0U, expectedPrefix.size()) == expectedPrefix ? expectedPrefix.size() - 1U + : std::string_view::npos; + } + if (separator != std::string_view::npos) { + relativePath.remove_prefix(separator + 1U); + if (std::any_of(setup.featurePaths.begin(), setup.featurePaths.end(), + [&](const std::string& featurePath) { return matchesFeaturePath(featurePath, relativePath); })) { + return true; + } } - addRootInconsistency(warnings, - "ignoring conflicting ctrace-setup timestamps.clock assignment for CoreSight Trace Bus ID " + - std::to_string(binding.traceBusId), - warningContext(binding)); } - return result; + const auto mayBindUnnamedPath = + !setupName.has_value() || (!referenceName.has_value() && reference.ctraceRef.find('/') == std::string::npos); + return mayBindUnnamedPath && + std::any_of(setup.featurePaths.begin(), setup.featurePaths.end(), [&](const std::string& featurePath) { + return referenceLeaf(featurePath) == referenceLeaf(reference.ctraceRef); + }); } -static std::map -buildTimestampPrescalersByTraceBusId(const std::vector& bindings, - std::vector& warnings) +/** @brief Rejects a generated reference that resolves exclusively to disabled setup fragments. */ +static void validateDisabledReferences(const TraceRunConfig& config) { - std::map result; - for (const auto& binding : bindings) { - const auto prescaler = binding.processor != nullptr && binding.processor->timestampPrescaler.has_value() - ? *binding.processor->timestampPrescaler - : TraceRunSchema::kDefaultTimestampPrescaler; - const auto [found, inserted] = result.emplace(binding.traceBusId, prescaler); - if (!inserted && found->second != prescaler) { - addRootInconsistency( - warnings, - "ignoring conflicting ctrace-setup timestamps.itm-prescaler assignment for CoreSight Trace Bus ID " + - std::to_string(binding.traceBusId), - warningContext(binding)); + for (const auto& reference : config.references) { + const TraceRunSetup* disabledMatch = nullptr; + bool activeMatch = false; + for (const auto& setup : config.setups) { + if (!setupContainsReference(setup, reference)) { + continue; + } + if (setup.disabled) { + disabledMatch = &setup; + } else { + activeMatch = true; + } + } + if (disabledMatch != nullptr && !activeMatch) { + throw std::runtime_error(configError(config, reference.line, + "ctrace-ref '" + reference.ctraceRef + + "' resolves only to disabled ctrace-setup fragment " + + std::to_string(disabledMatch->ordinal))); } } - return result; } -/** @brief Returns the common ITM enable mask across relevant processors. */ -static std::optional commonItmEnableMask(const std::vector& processors) +/** @brief Validates all routing-relevant fields retained from one formatted reference. */ +static void validateFormattedReference(const TraceRunConfig& config, const TraceRunReference& reference) { - std::optional common; - for (const auto& processor : processors) { - if (!processor.itmEnableMask.has_value()) { - return std::nullopt; + if (referenceLeaf(reference.ctraceRef) == "itm" && !hasProcessorItmPath(reference)) { + throw std::runtime_error( + configError(config, reference.line, "processor ITM route anchor path must use '[pname/]itm'")); + } + if (hasProcessorItmPath(reference) && reference.type != "itm") { + throw std::runtime_error( + configError(config, reference.line, "processor ITM route anchor must use reference type 'itm'")); + } + if (hasFeaturePath(reference, "timestamps") && reference.type != "itm" && reference.type != "dwt") { + throw std::runtime_error( + configError(config, reference.line, "timestamps reference must use type 'itm' or transitional type 'dwt'")); + } + const auto pathSeparator = reference.ctraceRef.find('/'); + const auto processorName = TraceRunSchema::normalizedProcessorName(reference.processorName); + if (processorName.has_value() && pathSeparator != std::string::npos && + pathSeparator == reference.ctraceRef.rfind('/') && + std::string_view(reference.ctraceRef).substr(0U, pathSeparator) != *processorName && + describesFormattedRoute(reference)) { + throw std::runtime_error( + configError(config, reference.line, "ctrace-ref path processor conflicts with pname '" + *processorName + "'")); + } + const auto problem = TraceRunSchema::referenceProblem(reference); + if (problem != ReferenceProblem::None && !isDiscardableSourceProblem(reference, problem)) { + throw std::runtime_error(referenceProblemMessage(config, reference, problem)); + } + if (isProcessorItmAnchor(reference) && !reference.stream.has_value()) { + throw std::runtime_error( + configError(config, reference.line, "processor ITM route anchor requires a CoreSight Trace Bus ID")); + } +} + +/** @brief Stores one formatted route while its binding evidence is accumulated. */ +struct FormattedRouteState { + CtraceRunRoute route; +}; + +/** @brief Registers a bound processor route and rejects one processor mapped to two ITM IDs. */ +static void registerBoundRoute(const TraceRunConfig& config, const TraceRunReference& reference, + const CtraceRunRoute& route, std::map& boundRoutes) +{ + if (!route.processorName.has_value()) { + return; + } + const auto [found, inserted] = boundRoutes.emplace(*route.processorName, *route.traceBusId); + if (!inserted && found->second != *route.traceBusId) { + throw std::runtime_error(configError(config, reference.line, + "processor '" + *route.processorName + + "' has ITM routes bound to multiple CoreSight Trace Bus IDs")); + } +} + +/** @brief Adds compatible evidence to one formatted route or rejects an ID-to-processor conflict. */ +static FormattedRouteState& mergeFormattedRoute(const TraceRunConfig& config, const TraceRunReference& reference, + const std::optional& processorName, + std::map& routes, + std::map& boundRoutes) +{ + const auto traceBusId = static_cast(*reference.stream); + auto [found, inserted] = routes.emplace(traceBusId, FormattedRouteState{}); + auto& state = found->second; + if (inserted) { + state.route.traceBusId = traceBusId; + state.route.processorName = processorName; + } else if (state.route.processorName.has_value() && processorName.has_value() && + state.route.processorName != processorName) { + throw std::runtime_error(configError(config, reference.line, + "CoreSight Trace Bus ID " + std::to_string(traceBusId) + + " has conflicting ITM processor bindings")); + } else if (!state.route.processorName.has_value() && processorName.has_value()) { + state.route.processorName = processorName; + } + registerBoundRoute(config, reference, state.route, boundRoutes); + return state; +} + +/** @brief Returns setup fragments that unambiguously supply metadata for one route. */ +static std::vector routeSetupFragments(const ActiveSetupIndex& setups, + const CtraceRunRoute& route) +{ + std::vector matches; + if (route.processorName.has_value()) { + for (const auto* setup : setups.fragments) { + if (TraceRunSchema::normalizedProcessorName(setup->processorName) == route.processorName) { + matches.push_back(setup); + } } - if (common.has_value() && common != processor.itmEnableMask) { - return std::nullopt; + const auto unnamedCanBind = setups.hasUnnamedProcessor && setups.namedProcessors.size() <= 1U && + (setups.namedProcessors.empty() || + setups.namedProcessors.find(*route.processorName) != setups.namedProcessors.end()); + if (!unnamedCanBind) { + return matches; } - common = processor.itmEnableMask; + } else if (setups.processorGroupCount() != 1U || !setups.hasUnnamedProcessor) { + return matches; } - return common; + + for (const auto* setup : setups.fragments) { + if (!TraceRunSchema::normalizedProcessorName(setup->processorName).has_value()) { + matches.push_back(setup); + } + } + return matches; } -static std::map -buildItmEnableMasksByTraceBusId(const std::vector& bindings, - std::vector& warnings) +/** @brief Applies compatible active setup fragments to one normalized route. */ +static void applyRouteSetupMetadata(const TraceRunConfig& config, CtraceRunRoute& route, + const std::vector& setups, + std::vector& warnings) { - std::map> candidates; - for (const auto& binding : bindings) { - const auto enableMask = binding.processor != nullptr ? binding.processor->itmEnableMask : std::nullopt; - const auto [found, inserted] = candidates.emplace(binding.traceBusId, enableMask); - if (!inserted && found->second != enableMask) { - addRootInconsistency(warnings, - "ignoring conflicting ctrace-setup itm.enable assignment for CoreSight Trace Bus ID " + - std::to_string(binding.traceBusId), - warningContext(binding)); + bool timestampSeen = false; + bool enableMaskConflict = false; + std::optional clockHz; + std::optional clockError; + std::optional prescaler; + std::optional enableMask; + std::optional enableError; + + for (const auto* setup : setups) { + if (setup->timestamps.has_value()) { + const auto& timestamps = *setup->timestamps; + const auto candidatePrescaler = + timestamps.timestampPrescaler.value_or(TraceRunSchema::kDefaultTimestampPrescaler); + if (!TraceRunSchema::isTimestampPrescaler(candidatePrescaler)) { + throw std::runtime_error( + configError(config, timestamps.line, + timestamps.line > 0U ? "'timestamps.itm-prescaler' must be one of 1, 4, 16, or 64" + : "ctrace-setup timestamps.itm-prescaler must be one of 1, 4, 16, or 64")); + } + if (prescaler.has_value() && *prescaler != candidatePrescaler) { + throw std::runtime_error( + configError(config, timestamps.line, + "conflicting timestamps.itm-prescaler values for one formatted processor ITM route")); + } + prescaler = candidatePrescaler; + + mergeTimestampClock(clockHz, clockError, timestamps.clockHz, timestamps.clockError, + "conflicting active ctrace-setup timestamps.clock values"); + timestampSeen = true; + } + if (setup->itm.has_value()) { + if (setup->itm->enableError.has_value()) { + if (enableError.has_value() && enableError != setup->itm->enableError) { + enableMaskConflict = true; + } else { + enableError = setup->itm->enableError; + } + continue; + } + const auto candidateMask = setup->itm->enableMask; + if (!candidateMask.has_value()) { + continue; + } + if (enableMask.has_value() && *enableMask != *candidateMask) { + if (!enableMaskConflict) { + addRootInconsistency( + warnings, "ignoring conflicting ctrace-setup itm.enable assignment for one formatted ITM route", + {{"pname", route.processorName.value_or("")}, {"line", std::to_string(setup->line)}}); + } + enableMaskConflict = true; + continue; + } + enableMask = *candidateMask; } } - std::map result; - for (const auto& [traceBusId, enableMask] : candidates) { - if (enableMask.has_value()) { - result.emplace(traceBusId, *enableMask); + route.timestampsConfigured = timestampSeen; + route.timestampPrescaler = prescaler.value_or(TraceRunSchema::kDefaultTimestampPrescaler); + if (enableMaskConflict) { + route.itmEnableError = "conflicting active ctrace-setup itm.enable values"; + } else if (enableError.has_value()) { + route.itmEnableError = enableError; + } else { + route.itmEnableMask = enableMask; + } + route.timestampClockHz = clockHz; + route.timestampClockError = clockError; +} + +/** @brief Converts one validated formatted source reference into route-local source metadata. */ +static CtraceRunSourceMeta formattedSourceMeta(const TraceRunConfig& config, const TraceRunReference& reference, + std::uint32_t source, const CtraceRunRoute& route) +{ + ProcessorIdentity identity; + identity.multipleProcessors = true; + auto boundReference = reference; + boundReference.processorName = route.processorName; + auto meta = sourceMeta(config, boundReference, source, identity); + meta.processorName = route.processorName; + meta.traceBusId = *route.traceBusId; + return meta; +} + +/** @brief Finds the established route described by a streamless compatible reference. */ +static std::optional streamlessRouteId(const TraceRunConfig& config, const ActiveSetupIndex& setups, + const TraceRunReference& reference, + const std::map& states, + const std::map& boundRoutes) +{ + const auto processorName = formattedProcessorName(config, setups, reference); + if (processorName.has_value()) { + const auto bound = boundRoutes.find(*processorName); + if (bound != boundRoutes.end()) { + return bound->second; + } + if (states.size() != 1U || states.begin()->second.route.processorName.has_value()) { + return std::nullopt; } } - return result; + return states.size() == 1U ? std::optional(states.begin()->first) : std::nullopt; } -/** @brief Tests whether processors require different timestamp prescalers. */ -static bool containsDistinctProcessorPrescalers(const std::vector& processors) +/** @brief Applies processor evidence from a streamless reference to its unique formatted route. */ +static void bindStreamlessRoute(const TraceRunConfig& config, const TraceRunReference& reference, + std::uint8_t traceBusId, const std::optional& processorName, + std::map& states, + std::map& boundRoutes) { - std::optional first; - for (const auto& processor : processors) { - if (!processor.timestampsEnabled || !processor.timestampPrescaler.has_value()) { - continue; + auto& route = states.at(traceBusId).route; + if (!route.processorName.has_value() && processorName.has_value()) { + route.processorName = processorName; + } + registerBoundRoute(config, reference, route, boundRoutes); +} + +/** @brief Builds the strict formatted route catalogue without constructing decoder objects. */ +static std::vector formattedRoutes(const TraceRunConfig& config, + std::vector& warnings) +{ + const auto setups = activeSetupIndex(config); + if (setups.namedProcessors.size() > 1U && setups.hasUnnamedProcessor) { + throw std::runtime_error( + config.path + ": pname is required for active ctrace-setup fragments in a multi-processor configuration"); + } + if (setups.namedProcessors.empty() && setups.hasUnnamedProcessor) { + std::set referenceNames; + for (const auto& reference : config.references) { + if (!describesFormattedRoute(reference)) { + continue; + } + const auto name = checkedReferenceProcessorName(config, reference); + if (name.has_value()) { + referenceNames.insert(*name); + } } - if (first.has_value() && first != processor.timestampPrescaler) { - return true; + if (referenceNames.size() > 1U) { + throw std::runtime_error(config.path + + ": one unnamed ctrace-setup processor cannot bind multiple formatted pnames"); } - first = processor.timestampPrescaler; } - return false; + std::map states; + std::map boundRoutes; + + for (const auto& reference : config.references) { + validateFormattedReference(config, reference); + } + + // Anchors are authoritative, so validate and register all of them before considering compatibility fallbacks. + for (const auto& reference : config.references) { + if (isProcessorItmAnchor(reference)) { + mergeFormattedRoute(config, reference, formattedProcessorName(config, setups, reference), states, boundRoutes); + } + } + for (const auto& reference : config.references) { + if (isFormattedRouteFallback(reference) && reference.stream.has_value()) { + mergeFormattedRoute(config, reference, formattedProcessorName(config, setups, reference), states, boundRoutes); + } + } + + if (setups.namedProcessors.empty() && setups.hasUnnamedProcessor && states.size() > 1U) { + throw std::runtime_error(config.path + + ": one unnamed ctrace-setup processor cannot bind multiple formatted ITM routes"); + } + + if (states.empty()) { + throw std::runtime_error(config.path + + ": formatted trace input requires an ITM route anchor or supported feature fallback"); + } + + // Every compatible description must resolve to an established route and agree with its processor binding. + for (const auto& reference : config.references) { + if (reference.stream.has_value()) { + const auto traceBusId = static_cast(*reference.stream); + if (states.find(traceBusId) == states.end()) { + throw std::runtime_error(configError(config, reference.line, + "ctrace-ref describes CoreSight Trace Bus ID " + + std::to_string(traceBusId) + + " without an ITM route anchor or supported feature fallback")); + } + mergeFormattedRoute(config, reference, formattedProcessorName(config, setups, reference), states, boundRoutes); + } else if (describesFormattedRoute(reference)) { + const auto processorName = formattedProcessorName(config, setups, reference); + const auto routeId = streamlessRouteId(config, setups, reference, states, boundRoutes); + if (!routeId.has_value()) { + throw std::runtime_error(configError( + config, reference.line, "streamless ctrace-ref cannot be associated with one formatted ITM route")); + } + bindStreamlessRoute(config, reference, *routeId, processorName, states, boundRoutes); + } + } + + std::vector routes; + routes.reserve(states.size()); + for (auto& [traceBusId, state] : states) { + auto& route = state.route; + applyRouteSetupMetadata(config, route, routeSetupFragments(setups, route), warnings); + + for (const auto& reference : config.references) { + const auto matchesStream = reference.stream.has_value() && *reference.stream == traceBusId; + const auto streamlessRoute = !reference.stream.has_value() && describesFormattedRoute(reference) + ? streamlessRouteId(config, setups, reference, states, boundRoutes) + : std::nullopt; + const auto describesRoute = matchesStream || (streamlessRoute.has_value() && *streamlessRoute == traceBusId); + if (describesRoute && TraceRunSchema::isUsableReference(reference)) { + for (const auto source : reference.sources) { + route.sources.push_back(formattedSourceMeta(config, reference, source, route)); + } + } + const auto hasDiagnostics = !reference.info.empty() || !reference.warning.empty() || !reference.error.empty(); + if (hasDiagnostics && describesRoute) { + appendReferenceDiagnostics(reference, route.referenceDiagnostics); + } + } + routes.push_back(std::move(route)); + } + return routes; } CtraceRunMeta CtraceRunMeta::fromConfig(const TraceRunConfig& config) { CtraceRunMeta ctraceRunMeta; ctraceRunMeta.m_configPath = config.path; + ctraceRunMeta.m_referenceDiagnostics = collectReferenceDiagnostics(config); + validateDisabledReferences(config); + + if (TraceRunSchema::effectiveTraceFormat(config.traceFormat) == TraceRunFormat::Formatted) { + ctraceRunMeta.m_routes = formattedRoutes(config, ctraceRunMeta.m_warnings); + + bool commonClockValid = true; + bool commonPrescalerValid = true; + bool commonEnableMaskValid = true; + std::optional commonClock; + std::optional commonPrescaler; + std::optional commonEnableMask; + for (const auto& route : ctraceRunMeta.m_routes) { + const auto traceBusId = *route.traceBusId; + ctraceRunMeta.m_timestampsByTraceBusId.emplace( + traceBusId, CtraceRunTimestampMeta{route.processorName, route.timestampClockHz, route.timestampClockError}); + ctraceRunMeta.m_timestampPrescalersByTraceBusId.emplace(traceBusId, route.timestampPrescaler); + if (route.itmEnableMask.has_value()) { + ctraceRunMeta.m_itmEnableMasksByTraceBusId.emplace(traceBusId, *route.itmEnableMask); + } + ctraceRunMeta.m_sources.insert(ctraceRunMeta.m_sources.end(), route.sources.begin(), route.sources.end()); + if (route.timestampClockError.has_value()) { + ctraceRunMeta.m_timestampClockErrors.push_back(*route.timestampClockError); + } + + if (route.timestampClockError.has_value() || !route.timestampClockHz.has_value()) { + commonClockValid = false; + } else if (commonClock.has_value() && commonClock != route.timestampClockHz) { + commonClockValid = false; + } else { + commonClock = route.timestampClockHz; + } + if (commonPrescaler.has_value() && *commonPrescaler != route.timestampPrescaler) { + commonPrescalerValid = false; + } else { + commonPrescaler = route.timestampPrescaler; + } + if (!route.itmEnableMask.has_value()) { + commonEnableMaskValid = false; + } else if (commonEnableMask.has_value() && commonEnableMask != route.itmEnableMask) { + commonEnableMaskValid = false; + } else { + commonEnableMask = route.itmEnableMask; + } + } + ctraceRunMeta.m_timestampClockHz = commonClockValid ? commonClock : std::nullopt; + ctraceRunMeta.m_timestampPrescaler = commonPrescalerValid ? commonPrescaler : std::nullopt; + ctraceRunMeta.m_itmEnableMask = commonEnableMaskValid ? commonEnableMask : std::nullopt; + ctraceRunMeta.m_processorCount = ctraceRunMeta.m_routes.size(); + ctraceRunMeta.m_distinctProcessorPrescalers = !commonPrescalerValid; + return ctraceRunMeta; + } for (const auto& reference : config.references) { + const auto problem = TraceRunSchema::referenceProblem(reference); + if (problem == ReferenceProblem::InvalidStream) { + throw std::runtime_error(referenceProblemMessage(config, reference, problem)); + } if (!TraceRunSchema::hasConsumedRouteShape(reference) && !TraceRunSchema::contributesStreamBinding(reference)) { continue; } - const auto problem = TraceRunSchema::referenceProblem(reference); - if (problem != ReferenceProblem::None && reference.error.empty()) { + if (problem != ReferenceProblem::None && !isDiscardableSourceProblem(reference, problem)) { throw std::runtime_error(referenceProblemMessage(config, reference, problem)); } } + const auto identity = processorIdentity(config, ctraceRunMeta.m_warnings); for (const auto& setup : config.setups) { - if (!consumesSetup(config, setup) || !setup.timestamps.has_value() || + if (!identity.acceptsSetup(setup) || !consumesSetup(config, setup) || !setup.timestamps.has_value() || !setup.timestamps->timestampPrescaler.has_value() || TraceRunSchema::isTimestampPrescaler(*setup.timestamps->timestampPrescaler)) { continue; @@ -523,33 +1183,58 @@ CtraceRunMeta CtraceRunMeta::fromConfig(const TraceRunConfig& config) : "ctrace-setup timestamps.itm-prescaler must be one of 1, 4, 16, or 64")); } - const auto identity = processorIdentity(config, ctraceRunMeta.m_warnings); std::vector processors; for (const auto& setup : config.setups) { - if (!consumesSetup(config, setup)) { + if (!identity.acceptsSetup(setup) || !consumesSetup(config, setup)) { continue; } const auto processorName = identity.canonicalName(setup.processorName); auto& processor = processorMeta(processors, processorName); if (setup.timestamps.has_value()) { + const auto prescaler = setup.timestamps->timestampPrescaler.value_or(TraceRunSchema::kDefaultTimestampPrescaler); + if (processor.timestampsEnabled && processor.timestampPrescaler != prescaler) { + throw std::runtime_error( + configError(config, setup.timestamps->line, + "unformatted SINGLE trace has conflicting timestamps.itm-prescaler values for one processor")); + } + mergeTimestampClock(processor.timestampClockHz, processor.timestampClockError, setup.timestamps->clockHz, + setup.timestamps->clockError, "conflicting active ctrace-setup timestamps.clock values"); processor.timestampsEnabled = true; - processor.timestampClockHz = setup.timestamps->clockHz; - processor.timestampClockError = setup.timestamps->clockError; - processor.timestampPrescaler = - setup.timestamps->timestampPrescaler.value_or(TraceRunSchema::kDefaultTimestampPrescaler); + processor.timestampPrescaler = prescaler; if (setup.timestamps->clockError.has_value()) { ctraceRunMeta.m_timestampClockErrors.push_back(*setup.timestamps->clockError); } } if (setup.itm.has_value()) { - processor.itmEnableMask = setup.itm->enableMask; + if (setup.itm->enableError.has_value()) { + if (processor.itmEnableError.has_value() && processor.itmEnableError != setup.itm->enableError) { + processor.itmEnableConflict = true; + processor.itmEnableError.reset(); + } else if (!processor.itmEnableConflict) { + processor.itmEnableError = setup.itm->enableError; + } + processor.itmEnableMask.reset(); + continue; + } + if (!setup.itm->enableMask.has_value() || processor.itmEnableError.has_value() || processor.itmEnableConflict) { + continue; + } + if (processor.itmEnableMask.has_value() && processor.itmEnableMask != setup.itm->enableMask) { + processor.itmEnableConflict = true; + processor.itmEnableMask.reset(); + addRootInconsistency(ctraceRunMeta.m_warnings, + "ignoring conflicting ctrace-setup itm.enable values for unformatted SINGLE trace", + {{"pname", processorName.value_or("")}}); + } else { + processor.itmEnableMask = setup.itm->enableMask; + } } } for (const auto& reference : config.references) { - if (isUsableStreamBinding(reference) && identity.accepts(reference)) { - (void)processorMeta(processors, identity.canonicalName(reference.processorName)); + if (isUsableProcessorBinding(reference) && identity.accepts(reference)) { + (void)processorMeta(processors, identity.canonicalReferenceName(reference)); } if (!TraceRunSchema::isUsableReference(reference) || !identity.accepts(reference)) { continue; @@ -558,18 +1243,62 @@ CtraceRunMeta CtraceRunMeta::fromConfig(const TraceRunConfig& config) ctraceRunMeta.m_sources.push_back(sourceMeta(config, reference, source, identity)); } } - const auto streamBindings = resolveStreamBindings(config, identity, processors); - ctraceRunMeta.m_timestampClockHz = commonProcessorSetting(processors, &ProcessorMeta::timestampClockHz); - ctraceRunMeta.m_timestampsByTraceBusId = - buildTimestampsByTraceBusId(streamBindings, ctraceRunMeta.m_warnings); - ctraceRunMeta.m_timestampPrescaler = commonProcessorSetting(processors, &ProcessorMeta::timestampPrescaler); - ctraceRunMeta.m_timestampPrescalersByTraceBusId = - buildTimestampPrescalersByTraceBusId(streamBindings, ctraceRunMeta.m_warnings); + const auto timestampPrescaler = commonTimestampPrescaler(processors); + ctraceRunMeta.m_distinctProcessorPrescalers = !processors.empty() && !timestampPrescaler.has_value(); + if (ctraceRunMeta.m_distinctProcessorPrescalers) { + throw std::runtime_error( + config.path + ": unformatted SINGLE trace cannot choose between different timestamps.itm-prescaler values"); + } + for (auto& source : ctraceRunMeta.m_sources) { + source.traceBusId = 0U; + } + ctraceRunMeta.m_timestampClockHz = commonTimestampClock(processors); + ctraceRunMeta.m_timestampPrescaler = timestampPrescaler; ctraceRunMeta.m_itmEnableMask = commonItmEnableMask(processors); - ctraceRunMeta.m_itmEnableMasksByTraceBusId = - buildItmEnableMasksByTraceBusId(streamBindings, ctraceRunMeta.m_warnings); ctraceRunMeta.m_processorCount = processors.size(); - ctraceRunMeta.m_distinctProcessorPrescalers = containsDistinctProcessorPrescalers(processors); + const auto clockError = commonTimestampClockError(processors); + if (clockError.has_value() && + std::find(ctraceRunMeta.m_timestampClockErrors.begin(), ctraceRunMeta.m_timestampClockErrors.end(), + *clockError) == ctraceRunMeta.m_timestampClockErrors.end()) { + ctraceRunMeta.m_timestampClockErrors.push_back(*clockError); + } + if (!processors.empty() && std::any_of(processors.begin(), processors.end(), + [](const ProcessorMeta& processor) { return processor.timestampsEnabled; })) { + const auto processorName = processors.size() == 1U ? processors.front().name : std::nullopt; + ctraceRunMeta.m_timestampsByTraceBusId.emplace( + 0U, CtraceRunTimestampMeta{processorName, ctraceRunMeta.m_timestampClockHz, clockError}); + ctraceRunMeta.m_timestampPrescalersByTraceBusId.emplace( + 0U, ctraceRunMeta.m_timestampPrescaler.value_or(TraceRunSchema::kDefaultTimestampPrescaler)); + } + if (ctraceRunMeta.m_itmEnableMask.has_value()) { + ctraceRunMeta.m_itmEnableMasksByTraceBusId.emplace(0U, *ctraceRunMeta.m_itmEnableMask); + } + + CtraceRunRoute syntheticRoute; + syntheticRoute.sources = ctraceRunMeta.m_sources; + syntheticRoute.referenceDiagnostics = ctraceRunMeta.m_referenceDiagnostics; + syntheticRoute.timestampsConfigured = std::any_of( + processors.begin(), processors.end(), [](const ProcessorMeta& processor) { return processor.timestampsEnabled; }); + syntheticRoute.timestampClockHz = ctraceRunMeta.m_timestampClockHz; + syntheticRoute.timestampPrescaler = + ctraceRunMeta.m_timestampPrescaler.value_or(TraceRunSchema::kDefaultTimestampPrescaler); + syntheticRoute.itmEnableMask = ctraceRunMeta.m_itmEnableMask; + syntheticRoute.timestampClockError = clockError; + if (processors.size() == 1U) { + syntheticRoute.processorName = processors.front().name; + syntheticRoute.timestampsConfigured = processors.front().timestampsEnabled; + syntheticRoute.timestampClockHz = processors.front().timestampClockHz; + syntheticRoute.timestampClockError = processors.front().timestampClockError; + syntheticRoute.timestampPrescaler = + processors.front().timestampPrescaler.value_or(TraceRunSchema::kDefaultTimestampPrescaler); + syntheticRoute.itmEnableMask = processors.front().itmEnableMask; + if (processors.front().itmEnableConflict) { + syntheticRoute.itmEnableError = "conflicting active ctrace-setup itm.enable values"; + } else { + syntheticRoute.itmEnableError = processors.front().itmEnableError; + } + } + ctraceRunMeta.m_routes.push_back(std::move(syntheticRoute)); return ctraceRunMeta; } @@ -629,6 +1358,16 @@ const std::vector& CtraceRunMeta::sources() const return m_sources; } +const std::vector& CtraceRunMeta::routes() const +{ + return m_routes; +} + +const std::vector& CtraceRunMeta::referenceDiagnostics() const +{ + return m_referenceDiagnostics; +} + const std::vector& CtraceRunMeta::warnings() const { return m_warnings; diff --git a/tools/ctrace/src/tracerun/CtraceRunMeta.h b/tools/ctrace/src/tracerun/CtraceRunMeta.h index d6e1992d2..5e6bba0df 100644 --- a/tools/ctrace/src/tracerun/CtraceRunMeta.h +++ b/tools/ctrace/src/tracerun/CtraceRunMeta.h @@ -46,6 +46,42 @@ struct CtraceRunWarning { std::vector> context; }; +/** @brief Identifies a protocol carried by one normalized trace route. */ +enum class CtraceRunProtocol { + Itm, +}; + +/** @brief Retains one producer diagnostic attached to a ctrace reference. */ +struct CtraceRunReferenceDiagnostic { + enum class Severity { + Info, + Warning, + Error, + }; + + Severity severity = Severity::Info; + std::string message; + std::string ctraceRef; + std::optional processorName; + std::optional stream; + std::size_t line = 0U; +}; + +/** @brief Describes one normalized protocol route and its processor metadata. */ +struct CtraceRunRoute { + CtraceRunProtocol protocol = CtraceRunProtocol::Itm; + std::optional traceBusId; + std::optional processorName; + bool timestampsConfigured = false; + std::optional timestampClockHz; + std::optional timestampClockError; + std::uint32_t timestampPrescaler = 1U; + std::optional itmEnableMask; + std::optional itmEnableError; + std::vector sources; + std::vector referenceDiagnostics; +}; + /** @brief Provides validated trace-run metadata consumed by decoding and output. */ class CtraceRunMeta { public: @@ -81,6 +117,10 @@ class CtraceRunMeta { std::size_t processorCount() const; /** @brief Returns all normalized source routes. */ const std::vector& sources() const; + /** @brief Returns the normalized protocol-route catalogue. */ + const std::vector& routes() const; + /** @brief Returns all producer diagnostics retained from consumed references. */ + const std::vector& referenceDiagnostics() const; /** @brief Returns non-fatal inconsistencies ignored during normalization. */ const std::vector& warnings() const; @@ -96,7 +136,9 @@ class CtraceRunMeta { std::size_t m_processorCount = 0; bool m_distinctProcessorPrescalers = false; std::vector m_sources; + std::vector m_routes; + std::vector m_referenceDiagnostics; std::vector m_warnings; }; -#endif // CTRACE_SRC_TRACERUN_CTRACERUNMETA_H +#endif // CTRACE_SRC_TRACERUN_CTRACERUNMETA_H diff --git a/tools/ctrace/src/tracerun/TraceRunConfig.h b/tools/ctrace/src/tracerun/TraceRunConfig.h index 3979b74fd..5098a1949 100644 --- a/tools/ctrace/src/tracerun/TraceRunConfig.h +++ b/tools/ctrace/src/tracerun/TraceRunConfig.h @@ -18,6 +18,12 @@ #include #include +/** @brief Identifies the byte format declared for one trace-run group. */ +enum class TraceRunFormat { + Unformatted, + Formatted, +}; + namespace TraceRunSchema { inline constexpr std::array kTimestampPrescalers{{ @@ -30,6 +36,12 @@ inline constexpr std::uint32_t kDefaultTimestampPrescaler = 1U; inline constexpr std::string_view kDefaultDwtDataType = "unsigned"; inline constexpr std::uint8_t kDefaultDwtDataSize = 4U; +/** @brief Resolves an absent trace-format declaration to its compatibility default. */ +constexpr TraceRunFormat effectiveTraceFormat(const std::optional& traceFormat) +{ + return traceFormat.value_or(TraceRunFormat::Unformatted); +} + /** @brief Tests whether a fixed array contains a value. */ template constexpr bool contains(const std::array& values, const Value& candidate) @@ -69,7 +81,8 @@ constexpr bool supportsSource(const std::string_view& type) /** @brief Tests whether ctrace consumes metadata for a reference type. */ constexpr bool consumesReferenceMetadata(const std::string_view& type) { - return type == "dwt" || type == "itm" || type == "event" || type == "pmu" || type == "pcsample"; + return type == "dwt" || type == "event" || type == "exception" || type == "itm" || type == "pmu" || + type == "overflow" || type == "pcsample" || type == "global_ts"; } /** @brief Tests whether an ITM stimulus port number is valid. */ @@ -181,12 +194,16 @@ inline bool isProcessorItmReference(const TraceRunReference& reference) inline bool contributesStreamBinding(const TraceRunReference& reference) { return (reference.type == "dwt" || reference.type == "itm") && - (!reference.sources.empty() || isTimestampReference(reference) || isProcessorItmReference(reference)); + (!reference.sources.empty() || isDwtDataReference(reference) || isTimestampReference(reference) || + isProcessorItmReference(reference)); } /** @brief Returns the first structural problem detected in a reference. */ inline ReferenceProblem referenceProblem(const TraceRunReference& reference) { + if (reference.stream.has_value() && !CoreSight::isAtbTraceId(*reference.stream)) { + return ReferenceProblem::InvalidStream; + } for (std::size_t left = 0U; left < reference.sources.size(); ++left) { for (std::size_t right = left + 1U; right < reference.sources.size(); ++right) { if (reference.sources[left] == reference.sources[right]) { @@ -194,9 +211,6 @@ inline ReferenceProblem referenceProblem(const TraceRunReference& reference) } } } - if (reference.stream.has_value() && !CoreSight::isAtbTraceId(*reference.stream)) { - return ReferenceProblem::InvalidStream; - } if (reference.type == "itm") { for (const auto source : reference.sources) { if (!isItmSource(source)) { @@ -227,11 +241,13 @@ struct TraceRunTimestampSetup { struct TraceRunDataSetup { std::optional size = std::nullopt; std::optional sizeError = std::nullopt; + bool present = true; }; /** @brief Stores ITM stimulus-port configuration copied from one trace setup. */ struct TraceRunItmSetup { - std::uint32_t enableMask = 0U; + std::optional enableMask; + std::optional enableError; }; /** @brief Stores the ctrace setup metadata consumed by the decoder. */ @@ -240,12 +256,17 @@ struct TraceRunSetup { std::optional timestamps; std::optional itm; std::vector data; + std::optional dataError; std::size_t line = 0U; + bool disabled = false; + std::size_t ordinal = 0U; + std::vector featurePaths; }; /** @brief Stores a parsed `*.ctrace-run.yml` input. */ struct TraceRunConfig { std::string path; + std::optional traceFormat; std::vector references; std::vector setups; }; diff --git a/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp b/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp index e495aefc3..b5699cd67 100644 --- a/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp +++ b/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp @@ -14,6 +14,8 @@ #include "yaml-cpp/node/type.h" #include "yaml-cpp/yaml.h" // IWYU pragma: keep +#include +#include #include #include #include @@ -155,9 +157,10 @@ static std::optional deferredUnsignedAttribute(const std::string& } /** @brief Parses an optional unsigned reference field and defers validation errors. */ -static std::optional deferredReferenceUnsignedAttribute( - const std::string& path, const Node& element, const std::string_view& name, std::uint64_t maximum, - std::optional& error) +static std::optional deferredReferenceUnsignedAttribute(const std::string& path, const Node& element, + const std::string_view& name, + std::uint64_t maximum, + std::optional& error) { const auto node = childNode(element, name); if (!node || node.IsNull()) { @@ -212,6 +215,20 @@ static std::optional dwtDataIndex(const std::string_view& ctraceRef return index; } +/** @brief Resolves the processor named explicitly or by `[pname/]data#`. */ +static std::optional dataReferenceProcessorName(const TraceRunReference& reference) +{ + const auto explicitName = TraceRunSchema::normalizedProcessorName(reference.processorName); + if (explicitName.has_value() || !reference.dataSetupIndex.has_value()) { + return explicitName; + } + const auto separator = reference.ctraceRef.find('/'); + if (separator == 0U || separator == std::string::npos || separator != reference.ctraceRef.rfind('/')) { + return std::nullopt; + } + return reference.ctraceRef.substr(0U, separator); +} + /** @brief Collects data setup indices consumed by matching references. */ static std::set referencedDataSetupIndices(const std::vector& references, const std::optional& setupProcessorName) @@ -221,7 +238,7 @@ static std::set referencedDataSetupIndices(const std::vector parseTraceFormat(const std::string& path, const Node& root) +{ + const auto node = childNode(root, "trace-format"); + if (!node || node.IsNull()) { + return std::nullopt; + } + if (!node.IsScalar()) { + fail(path, node, "'trace-format' must be a scalar value"); + } + if (node.Scalar() == "unformatted") { + return TraceRunFormat::Unformatted; + } + if (node.Scalar() == "formatted") { + return TraceRunFormat::Formatted; + } + fail(path, node, "'trace-format' must be 'unformatted' or 'formatted'"); +} + /** @brief Parses scalar or sequence source identifiers from one reference. */ static std::vector parseSources(const std::string& path, const Node& reference) { @@ -370,45 +406,25 @@ static std::optional parseReference(const std::string& path, return static_cast(*stream); }; - const auto parseRoute = [&]() { - reference.processorName = processorNameAttribute(path, element); - reference.stream = parseStream(); - reference.sources = parseSources(path, element); - if (reference.type == "dwt") { - reference.address = deferredReferenceUnsignedAttribute(path, element, "address", - std::numeric_limits::max(), - reference.addressError); - reference.dataType = deferredReferenceStringAttribute(element, "data-type", reference.dataTypeError); - reference.dataSize = deferredReferenceUnsignedAttribute(path, element, "size", - std::numeric_limits::max(), - reference.dataSizeError); - } - reference.label = optionalAttribute(element, "label"); - }; - - if (!TraceRunSchema::supportsSource(reference.type)) { - // These source types currently contribute diagnostics only. Preserve - // a valid processor name for log context, but do not validate fields - // that no ctrace decoder or output consumes. - reference.processorName = bestEffortProcessorName(element); - return reference; - } - - if (!diagnostics.error.empty()) { - auto diagnosticReference = reference; - diagnosticReference.processorName = bestEffortProcessorName(element); + reference.processorName = processorNameAttribute(path, element); + reference.stream = parseStream(); + if (TraceRunSchema::supportsSource(reference.type)) { try { - parseRoute(); - if (TraceRunSchema::isUsableReference(reference) || TraceRunSchema::isItmChannelZero(reference)) { - return reference; - } - return diagnosticReference; + reference.sources = parseSources(path, element); } catch (const std::runtime_error&) { - return diagnosticReference; + if (diagnostics.error.empty()) { + throw; + } } } - - parseRoute(); + if (reference.type == "dwt") { + reference.address = deferredReferenceUnsignedAttribute( + path, element, "address", std::numeric_limits::max(), reference.addressError); + reference.dataType = deferredReferenceStringAttribute(element, "data-type", reference.dataTypeError); + reference.dataSize = deferredReferenceUnsignedAttribute( + path, element, "size", std::numeric_limits::max(), reference.dataSizeError); + } + reference.label = optionalAttribute(element, "label"); return reference; } @@ -481,26 +497,40 @@ static std::optional parseItmSetup(const std::string& path, co return std::nullopt; } if (!itmNode.IsMap()) { - fail(path, itmNode, "'itm' must be a map containing 'enable'"); + TraceRunItmSetup setup; + setup.enableError = "'itm' must be a map containing 'enable'"; + return setup; } const auto enableNode = childNode(itmNode, "enable"); if (!enableNode || enableNode.IsNull()) { return std::nullopt; } if (!enableNode.IsScalar() || enableNode.Scalar().empty()) { - fail(path, enableNode, "'itm.enable' must be a scalar unsigned integer"); + TraceRunItmSetup setup; + setup.enableError = "'itm.enable' must be a scalar unsigned integer"; + return setup; + } + TraceRunItmSetup setup; + try { + setup.enableMask = static_cast( + unsignedValue(path, enableNode, "itm.enable", enableNode.Scalar(), std::numeric_limits::max())); + } catch (const std::runtime_error& error) { + setup.enableError = error.what(); } - return TraceRunItmSetup{static_cast( - unsignedValue(path, enableNode, "itm.enable", enableNode.Scalar(), std::numeric_limits::max()))}; + return setup; } /** @brief Parses only DWT data setups referenced by consumed routes. */ static std::vector parseReferencedDataSetups(const std::string& path, const Node& element, - const std::set& referencedIndices) + const std::set& referencedIndices, + std::optional& dataError) { - const auto dataNode = - referencedIndices.empty() ? Node(YAML::NodeType::Undefined) : childNode(element, "data"); - if (!dataNode || !dataNode.IsSequence()) { + const auto dataNode = referencedIndices.empty() ? Node(YAML::NodeType::Undefined) : childNode(element, "data"); + if (!dataNode || dataNode.IsNull()) { + return {}; + } + if (!dataNode.IsSequence()) { + dataError = "'data' must be an array"; return {}; } @@ -509,12 +539,20 @@ static std::vector parseReferencedDataSetups(const std::strin bool foundReferencedEntry = false; for (const auto& item : dataNode) { if (referencedIndices.find(index++) == referencedIndices.end()) { - dataSetups.emplace_back(); + TraceRunDataSetup data; + data.present = false; + dataSetups.push_back(std::move(data)); continue; } - foundReferencedEntry = true; TraceRunDataSetup data; + if (item.IsNull()) { + data.present = false; + dataSetups.push_back(std::move(data)); + continue; + } + foundReferencedEntry = true; if (!item.IsMap()) { + data.sizeError = "each 'data' entry must be a map"; dataSetups.push_back(std::move(data)); continue; } @@ -522,25 +560,97 @@ static std::vector parseReferencedDataSetups(const std::strin if (size && !size.IsScalar() && !size.IsNull()) { data.sizeError = "'data.size' must be a scalar unsigned integer"; } else if (size && !size.IsNull()) { - data.size = deferredUnsignedAttribute(path, item, "size", std::numeric_limits::max(), - data.sizeError); + data.size = + deferredUnsignedAttribute(path, item, "size", std::numeric_limits::max(), data.sizeError); } dataSetups.push_back(std::move(data)); } return foundReferencedEntry ? dataSetups : std::vector{}; } +/** @brief Returns the generated reference paths represented by one setup fragment. */ +static std::vector setupFeaturePaths(const Node& element, const std::optional& processorName) +{ + struct Feature { + std::string_view name; + bool repeated; + }; + constexpr std::array features{{ + {"timestamps", false}, + {"timesync", false}, + {"data", true}, + {"exceptions", false}, + {"events", true}, + {"itm", false}, + {"pcsampling", false}, + {"synchronization", false}, + {"instructions", false}, + {"tracehalt", false}, + }}; + + const auto prefix = processorName.has_value() ? *processorName + "/" : std::string{}; + std::vector paths; + for (const auto& feature : features) { + const auto node = childNode(element, feature.name); + if (!node || ((feature.name == "data" || feature.name == "itm") && node.IsNull())) { + continue; + } + if (feature.repeated && node.IsSequence()) { + for (std::size_t index = 0U; index < node.size(); ++index) { + if (node[index].IsNull()) { + continue; + } + paths.push_back(prefix + std::string(feature.name) + "#" + std::to_string(index)); + } + continue; + } + paths.push_back(prefix + std::string(feature.name)); + } + return paths; +} + +/** @brief Tests whether an active setup contains metadata or a feature reference consumed by ctrace. */ +static bool hasRelevantSetupContent(const Node& element, const std::vector& references) +{ + if (childNode(element, "timestamps")) { + return true; + } + const auto itm = childNode(element, "itm"); + const auto itmEnable = itm && itm.IsMap() ? childNode(itm, "enable") : Node(YAML::NodeType::Undefined); + if (itm && !itm.IsNull() && (!itm.IsMap() || (itmEnable && !itmEnable.IsNull()))) { + return true; + } + + const auto featurePaths = setupFeaturePaths(element, std::nullopt); + const auto matches = [](const std::string_view featurePath, std::string_view referencePath) { + const auto processorSeparator = referencePath.find('/'); + if (processorSeparator != std::string_view::npos) { + referencePath.remove_prefix(processorSeparator + 1U); + } + return referencePath == featurePath || + (referencePath.size() > featurePath.size() && referencePath.substr(0U, featurePath.size()) == featurePath && + (referencePath[featurePath.size()] == '/' || referencePath[featurePath.size()] == '#')); + }; + return std::any_of(featurePaths.begin(), featurePaths.end(), [&](const std::string& featurePath) { + return std::any_of(references.begin(), references.end(), [&](const TraceRunReference& reference) { + return TraceRunSchema::consumesReferenceMetadata(reference.type) && matches(featurePath, reference.ctraceRef); + }); + }); +} + /** @brief Parses one trace setup and its consumed metadata groups. */ static TraceRunSetup parseSetup(const std::string& path, const Node& element, - const std::vector& references) + const std::vector& references, std::size_t ordinal) { TraceRunSetup setup; setup.line = lineNumber(element); + setup.ordinal = ordinal; setup.processorName = processorNameAttribute(path, element); + setup.featurePaths = setupFeaturePaths(element, setup.processorName); const auto referencedDataIndices = referencedDataSetupIndices(references, setup.processorName); setup.timestamps = parseTimestampSetup(path, element); setup.itm = parseItmSetup(path, element); - setup.data = parseReferencedDataSetups(path, element, referencedDataIndices); + setup.data = parseReferencedDataSetups(path, element, referencedDataIndices, setup.dataError); return setup; } @@ -555,7 +665,9 @@ static std::vector parseSetups(const std::string& path, const Nod requireSequence(path, setupNode, "ctrace-setup"); std::vector setups; + std::size_t ordinal = 0U; for (const auto& item : setupNode) { + const auto currentOrdinal = ordinal++; if (item.IsNull()) { continue; } @@ -567,12 +679,19 @@ static std::vector parseSetups(const std::string& path, const Nod // Its value therefore has no schema that ctrace needs to validate. const auto disable = childNode(item, "disable"); if (disable) { + TraceRunSetup setup; + setup.line = lineNumber(item); + setup.disabled = true; + setup.ordinal = currentOrdinal; + setup.processorName = bestEffortProcessorName(item); + setup.featurePaths = setupFeaturePaths(item, setup.processorName); + setups.push_back(std::move(setup)); continue; } - auto setup = parseSetup(path, item, references); - if (!setup.timestamps.has_value() && !setup.itm.has_value() && setup.data.empty()) { + if (!hasRelevantSetupContent(item, references)) { continue; } + auto setup = parseSetup(path, item, references, currentOrdinal); setups.push_back(std::move(setup)); } return setups; @@ -609,6 +728,7 @@ TraceRunConfig YmlTraceRunConfigReader::read(const std::string& path) const TraceRunConfig config; config.path = path; + config.traceFormat = parseTraceFormat(path, root); config.references = parseReferences(path, root); config.setups = parseSetups(path, root, config.references); return config; diff --git a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp index 4036542d4..b388daaba 100644 --- a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp +++ b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp @@ -523,7 +523,8 @@ TEST_F(CtraceIntegTests, ReportsDiagnosticsFromConsumedTraceRunReferences) expectContains(result.stderrText, "[error] target could not enable ITM channel zero:"); expectContains(result.stderrText, "[error] target rejected the fallback configuration:"); expectContains(result.stderrText, "ctraceRef=core/itm, type=itm, pname=core"); - expectNotContains(result.stderrText, "ignored reference diagnostic"); + expectContains(result.stderrText, "[error] ignored reference diagnostic:"); + expectContains(result.stderrText, "ctraceRef=core/exceptions, type=exception"); expectNonEmptyFile(workDirectory() / "Diagnostics.SWO.csv"); expectNonEmptyFile(workDirectory() / "Diagnostics.ctf" / "metadata"); diff --git a/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp b/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp index 031324db3..f47c50b4b 100644 --- a/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp +++ b/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp @@ -160,6 +160,34 @@ TEST(CtraceUnitTests, testTraceDirectoryBatchCheckAndExplicitConfig) << "check-only trace directory should fail on decoder error packets"; } +TEST(CtraceUnitTests, testTraceDirectoryRejectsFormattedInputBeforeRawFrontendAndOutput) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-directory-formatted-guard-test"); + const auto traceDir = temporaryPath.path() / ".trace"; + writeTraceInputs(traceDir, {"Formatted"}); + writeTestFile(traceDir / "Formatted.TB.raw"); + + TraceRunConfig config; + config.traceFormat = TraceRunFormat::Formatted; + config.references.push_back(TraceRunTestSupport::makeReference("itm", "core", 1U, {}, "core/itm")); + + CliOptions options; + options.traceDir = traceDir.string(); + options.targetName = "Formatted"; + options.outputFormat = OutputFormat::All; + + CollectingDiagnosticSink diagnostics; + TestTraceRunConfigReader reader(config); + TraceDirectoryJob(options, diagnostics, reader).run(); + + EXPECT_TRUE(diagnostics.containsMessage("formatted trace input is not enabled yet")); + EXPECT_FALSE(diagnostics.containsMessage("CTF output requires timestamps.clock")); + EXPECT_FALSE(diagnostics.containsMessage("skipping raw trace channel")); + EXPECT_FALSE(std::filesystem::exists(traceDir / "Formatted.SWO.csv")); + EXPECT_FALSE(std::filesystem::exists(traceDir / "Formatted.ctf")); + EXPECT_FALSE(std::filesystem::exists(traceDir / "Formatted.SWO.traceanalysis.xml")); +} + TEST(CtraceUnitTests, testTraceDirectoryReportsGenerationDiagnosticsAndMissingSwo) { const TemporaryTestPath temporaryPath("ctrace-trace-directory-diagnostics-test"); @@ -292,6 +320,7 @@ TEST(CtraceUnitTests, testFileDecodeJobReportsPerStreamPrescalers) writeTestFile(rawPath); TraceRunConfig config; + config.traceFormat = TraceRunFormat::Formatted; config.setups = { TraceRunTestSupport::makeTimestampSetup("first", 100U, 4U), TraceRunTestSupport::makeTimestampSetup("second", 100U, 16U), diff --git a/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp b/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp index 9c851a62c..912fe8f88 100644 --- a/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp +++ b/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp @@ -59,12 +59,13 @@ TEST(CtraceUnitTests, testBackendRequirementsUsePerStreamMetadata) { TraceRunConfig multicore; multicore.path = "Multicore.ctrace-run.yml"; + multicore.traceFormat = TraceRunFormat::Formatted; auto core0 = TraceRunTestSupport::makeTimestampSetup("core0", 400000000U, 1U); auto core1 = TraceRunTestSupport::makeTimestampSetup("core1", 400000000U, 4U); multicore.setups = {core0, core1}; - const auto core0Itm = TraceRunTestSupport::makeReference("itm", "core0", 1U, {1U}, "opaque/core0-route"); - const auto core1Itm = TraceRunTestSupport::makeReference("itm", "core1", 2U, {1U}, "opaque/core1-route"); + const auto core0Itm = TraceRunTestSupport::makeReference("itm", "core0", 1U, {1U}, "core0/itm"); + const auto core1Itm = TraceRunTestSupport::makeReference("itm", "core1", 2U, {1U}, "core1/itm"); multicore.references = {core0Itm, core1Itm}; core1.timestamps = TraceRunTimestampSetup{400000000U, std::nullopt}; @@ -185,8 +186,7 @@ TEST(CtraceUnitTests, testOutputRequirementsAreBackendSpecific) missingType.ctf->coreClockHz == 400000000U && missingType.ctf->sources.size() == 1U && missingType.ctf->sources[0].dataType == "unsigned" && missingType.ctf->sources[0].dataSize == 4U)) << "output preflight must resolve artifact paths, clock, routes, and defaults"; - ASSERT_TRUE(missingTypeDiagnostics.events().empty()) - << "missing optional data-type must not produce diagnostics"; + ASSERT_TRUE(missingTypeDiagnostics.events().empty()) << "missing optional data-type must not produce diagnostics"; config.references[0].dataTypeError = "data-type must be scalar"; CollectingDiagnosticSink malformedTypeDiagnostics; @@ -206,8 +206,8 @@ TEST(CtraceUnitTests, testOutputRequirementsAreBackendSpecific) config.references[0].dataType = "signed"; config.references[0].dataSize = 1U; CollectingDiagnosticSink currentMetadataDiagnostics; - const auto currentMetadata = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, - currentMetadataDiagnostics); + const auto currentMetadata = + planOutputs(allRequest, "BackendRequirements.SWO.raw", config, currentMetadataDiagnostics); ASSERT_TRUE(currentMetadata.ctf.has_value() && currentMetadata.ctf->sources[0].dataType == "signed" && currentMetadata.ctf->sources[0].dataSize == 1U) << "reference data-type/size must be retained for CTF"; @@ -215,8 +215,8 @@ TEST(CtraceUnitTests, testOutputRequirementsAreBackendSpecific) config.references[0].addressError = "address must be unsigned"; CollectingDiagnosticSink malformedAddressDiagnostics; - const auto malformedAddress = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, - malformedAddressDiagnostics); + const auto malformedAddress = + planOutputs(allRequest, "BackendRequirements.SWO.raw", config, malformedAddressDiagnostics); ASSERT_TRUE(malformedAddress.csv.has_value() && !malformedAddress.ctf.has_value()) << "malformed address must disable only CTF"; malformedAddressDiagnostics.singleEvent(); @@ -268,16 +268,23 @@ TEST(CtraceUnitTests, testOutputPreflightRejectsAmbiguousRoutesForCtfOnly) { TraceRunConfig config; config.path = "AmbiguousRoutes.ctrace-run.yml"; + config.traceFormat = TraceRunFormat::Formatted; auto setup = TraceRunTestSupport::makeTimestampSetup(std::nullopt, 400000000U, 1U); setup.data.push_back(TraceRunDataSetup{4U}); - config.setups.push_back(setup); + auto routeOneSetup = setup; + routeOneSetup.processorName = "core0"; + auto routeTwoSetup = setup; + routeTwoSetup.processorName = "core1"; + config.setups = {routeOneSetup, routeTwoSetup}; - auto first = TraceRunTestSupport::makeReference("dwt", std::nullopt, 1U, {0U}, "opaque/dwt-a"); + auto first = TraceRunTestSupport::makeReference("dwt", "core0", 1U, {0U}, "core0/data#0"); first.dataSetupIndex = 0U; first.label = "core-one"; TraceRunReference second = first; second.label = "core-two"; - config.references = {first, second}; + const auto firstAnchor = TraceRunTestSupport::makeReference("itm", "core0", 1U, {}, "core0/itm"); + const auto secondAnchor = TraceRunTestSupport::makeReference("itm", "core1", 2U, {}, "core1/itm"); + config.references = {first, second, firstAnchor, secondAnchor}; auto allRequest = outputRequest(true, true); CollectingDiagnosticSink diagnostics; @@ -287,6 +294,8 @@ TEST(CtraceUnitTests, testOutputPreflightRejectsAmbiguousRoutesForCtfOnly) diagnostics.singleEvent(); config.references[1].stream = 2U; + config.references[1].processorName = "core1"; + config.references[1].ctraceRef = "core1/data#0"; config.references[1].label = "core-one"; CollectingDiagnosticSink routeDiagnostics; const auto routePlan = planOutputs(allRequest, "captures/AmbiguousRoutes.SWO.raw", config, routeDiagnostics); @@ -297,16 +306,16 @@ TEST(CtraceUnitTests, testOutputPreflightRejectsAmbiguousRoutesForCtfOnly) TraceRunConfig processorConfig; processorConfig.path = "AmbiguousProcessors.ctrace-run.yml"; - auto core0Setup = setup; - core0Setup.processorName = "core0"; - auto core1Setup = setup; - core1Setup.processorName = "core1"; - processorConfig.setups = {core0Setup, core1Setup}; + processorConfig.traceFormat = TraceRunFormat::Formatted; + processorConfig.setups = config.setups; auto core0Reference = first; core0Reference.processorName = "core0"; + core0Reference.ctraceRef = "core0/data#0"; auto core1Reference = first; core1Reference.processorName = "core1"; - processorConfig.references = {core0Reference, core1Reference}; + core1Reference.ctraceRef = "core1/data#0"; + core1Reference.stream = 2U; + processorConfig.references = {core0Reference, core1Reference, firstAnchor, secondAnchor}; CollectingDiagnosticSink processorDiagnostics; const auto processorPlan = planOutputs(outputRequest(true, false), "captures/AmbiguousProcessors.SWO.raw", processorConfig, processorDiagnostics); @@ -324,7 +333,8 @@ TEST(CtraceUnitTests, testOutputPreflightRejectsAmbiguousRoutesForCtfOnly) config.setups[0].data.push_back(TraceRunDataSetup{2U}); config.references[1].stream = 1U; - config.references[1].ctraceRef = "opaque/dwt-b"; + config.references[1].processorName = "core0"; + config.references[1].ctraceRef = "core0/data#1"; config.references[1].dataSetupIndex = 1U; CollectingDiagnosticSink sizeDiagnostics; const auto csvPlan = @@ -361,10 +371,61 @@ TEST(CtraceUnitTests, testOutputRequirementsValidateDefaultClockWithoutRoutes) zeroDiagnostics.singleEvent(); } +TEST(CtraceUnitTests, testOutputRequirementsDeferUnformattedSingleClockAmbiguityToCtf) +{ + TraceRunConfig config; + config.path = "SingleCandidates.ctrace-run.yml"; + config.setups = { + TraceRunTestSupport::makeTimestampSetup("first", 100U, 4U), + TraceRunTestSupport::makeTimestampSetup("second", 200U, 4U), + }; + config.references = { + TraceRunTestSupport::makeReference("itm", "first", 1U, {1U}), + TraceRunTestSupport::makeReference("itm", "second", 1U, {2U}), + }; + + CollectingDiagnosticSink checkDiagnostics; + const auto checkPlan = planOutputs(outputRequest(false, false), "captures/Single.SWO.raw", config, checkDiagnostics); + EXPECT_FALSE(checkPlan.hasRequestedOutputs()); + EXPECT_TRUE(checkDiagnostics.events().empty()); + + CollectingDiagnosticSink csvDiagnostics; + const auto csvPlan = planOutputs(outputRequest(true, false), "captures/Single.SWO.raw", config, csvDiagnostics); + EXPECT_TRUE(csvPlan.csv.has_value()); + EXPECT_TRUE(csvDiagnostics.events().empty()); + + CollectingDiagnosticSink allDiagnostics; + const auto allPlan = planOutputs(outputRequest(true, true), "captures/Single.SWO.raw", config, allDiagnostics); + EXPECT_TRUE(allPlan.csv.has_value()); + EXPECT_FALSE(allPlan.ctf.has_value()); + EXPECT_EQ(allDiagnostics.singleEvent().message, "CTF output cannot use the configured timestamps.clock"); + + config.setups[1].timestamps->clockHz = 100U; + CollectingDiagnosticSink equivalentDiagnostics; + const auto equivalent = + planOutputs(outputRequest(true, true), "captures/Single.SWO.raw", config, equivalentDiagnostics); + EXPECT_TRUE(equivalent.csv.has_value()); + EXPECT_TRUE(equivalent.ctf.has_value()); + EXPECT_TRUE(equivalentDiagnostics.events().empty()); + + TraceRunSetup noTimestamps; + noTimestamps.processorName = "second"; + noTimestamps.itm = TraceRunItmSetup{1U}; + config.setups[0].timestamps->timestampPrescaler = 1U; + config.setups[1] = noTimestamps; + CollectingDiagnosticSink missingCandidateDiagnostics; + const auto missingCandidate = + planOutputs(outputRequest(true, true), "captures/Single.SWO.raw", config, missingCandidateDiagnostics); + EXPECT_TRUE(missingCandidate.csv.has_value()); + EXPECT_FALSE(missingCandidate.ctf.has_value()); + EXPECT_EQ(missingCandidateDiagnostics.singleEvent().message, "CTF output cannot use the configured timestamps.clock"); +} + TEST(CtraceUnitTests, testOutputRequirementsRejectUnknownStreamWithMultipleClocks) { TraceRunConfig config; config.path = "Multicore.ctrace-run.yml"; + config.traceFormat = TraceRunFormat::Formatted; config.setups = { TraceRunTestSupport::makeTimestampSetup("first", 100U, 1U), TraceRunTestSupport::makeTimestampSetup("second", 200U, 1U), @@ -380,6 +441,29 @@ TEST(CtraceUnitTests, testOutputRequirementsRejectUnknownStreamWithMultipleClock const auto plan = planOutputs(ctfRequest, "Multicore.SWO.raw", config, diagnostics); ASSERT_FALSE(plan.ctf.has_value()); diagnostics.singleEvent(); + + config.setups[1].timestamps->clockHz = 100U; + CollectingDiagnosticSink commonDiagnostics; + const auto commonPlan = planOutputs(ctfRequest, "Multicore.SWO.raw", config, commonDiagnostics); + ASSERT_TRUE(commonPlan.ctf.has_value()); + EXPECT_EQ(commonPlan.ctf->coreClockHz, 100U); + EXPECT_TRUE(commonDiagnostics.events().empty()); + + config.setups[0].timestamps->clockHz.reset(); + config.setups[0].timestamps->clockError = "invalid processor clock"; + CollectingDiagnosticSink malformedDiagnostics; + const auto malformedPlan = planOutputs(ctfRequest, "Multicore.SWO.raw", config, malformedDiagnostics); + ASSERT_FALSE(malformedPlan.ctf.has_value()); + EXPECT_EQ(malformedDiagnostics.singleEvent().message, "CTF output cannot use the configured timestamps.clock"); + + for (auto& setup : config.setups) { + setup.timestamps->clockError.reset(); + setup.timestamps->clockHz = 0U; + } + CollectingDiagnosticSink zeroDiagnostics; + const auto zeroPlan = planOutputs(ctfRequest, "Multicore.SWO.raw", config, zeroDiagnostics); + ASSERT_FALSE(zeroPlan.ctf.has_value()); + EXPECT_EQ(zeroDiagnostics.singleEvent().message, "CTF output requires timestamps.clock to be greater than zero"); } TEST(CtraceUnitTests, testOutputRequirementsRejectsInputWithoutArtifactName) diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp index 237ef6d66..cd2d7ad8b 100644 --- a/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp +++ b/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp @@ -174,8 +174,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputOverflowClosesExceptionUntilReturn) output.stop(); ASSERT_TRUE(readCtfExceptionRecords(outputDir / "stream_0") == - std::vector({{0U, 0U, 1U}, {0U, 1U, 1U}, {15U, 0U, 0U}, {15U, 1U, 1U}, - {0U, 2U, 0U}})) + std::vector({{0U, 0U, 1U}, {0U, 1U, 1U}, {15U, 0U, 0U}, {15U, 1U, 1U}, {0U, 2U, 0U}})) << "CTF overflow must close the active exception without inventing Thread Mode before its return"; } @@ -186,21 +185,24 @@ TEST(CtraceUnitTests, testCtfBundleOutputUsesCtraceRunMeta) TraceRunConfig traceRun; traceRun.path = "Board.ctrace-run.yml"; - auto signedByteReference = TraceRunTestSupport::makeReference("dwt", std::nullopt, 7U, {0U}, "opaque/signed-byte"); + traceRun.traceFormat = TraceRunFormat::Formatted; + auto signedByteReference = TraceRunTestSupport::makeReference("dwt", "core", 7U, {0U}, "core/data#0"); signedByteReference.dataSetupIndex = 0U; signedByteReference.label = "Sine"; signedByteReference.dataType = "signed"; signedByteReference.dataSize = 1U; traceRun.references.push_back(signedByteReference); - auto reference = TraceRunTestSupport::makeReference("dwt", std::nullopt, std::nullopt, {2U}, "opaque/current"); + auto reference = TraceRunTestSupport::makeReference("dwt", "core", std::nullopt, {2U}, "core/data#2"); reference.dataSetupIndex = 2U; reference.label = "Current\n\t\"\\\x01"; reference.address = 0x24000e88U; reference.dataType = "signed"; reference.dataSize = 4U; traceRun.references.push_back(reference); + traceRun.references.push_back(TraceRunTestSupport::makeReference("itm", "core", 7U, {}, "core/itm")); TraceRunSetup setup; + setup.processorName = "core"; setup.timestamps = TraceRunTimestampSetup{280000000U, 1U}; traceRun.setups.push_back(std::move(setup)); @@ -293,15 +295,23 @@ TEST(CtraceUnitTests, testCtfBundleOutputDefaultsDwtValueType) TraceRunConfig traceRun; traceRun.path = "ambiguous-streams.ctrace-run.yml"; - auto first = TraceRunTestSupport::makeReference("dwt", std::nullopt, 1U, {0U}, "opaque/dwt-route"); + traceRun.traceFormat = TraceRunFormat::Formatted; + auto first = TraceRunTestSupport::makeReference("dwt", "core-one", 1U, {0U}, "core-one/data#0"); first.dataSetupIndex = 0U; first.label = "core-one"; first.dataType = "signed"; first.dataSize = 4U; TraceRunReference second = first; + second.processorName = "core-two"; + second.ctraceRef = "core-two/data#0"; second.stream = 2U; second.label = "core-two"; - traceRun.references = {first, second}; + traceRun.references = { + first, + second, + TraceRunTestSupport::makeReference("itm", "core-one", 1U, {}, "core-one/itm"), + TraceRunTestSupport::makeReference("itm", "core-two", 2U, {}, "core-two/itm"), + }; const auto meta = CtraceRunMeta::fromConfig(traceRun); ASSERT_TRUE(meta.sources().size() == 2U && meta.sources().front().traceBusId == 1U && meta.sources().front().label == std::optional("core-one")) @@ -333,9 +343,9 @@ TEST(CtraceUnitTests, testCtfWarningsRemainVisibleWithoutResettingContext) context.writeEvent(warning); context.writeEvent(exceptionPacket(54U, ExceptionAction::Entered, 20U)); context.stop(); - ASSERT_TRUE(readCtfExceptionRecords(contextDir / "stream_0") == - std::vector({{0U, 0U, 1U}, {0U, 1U, 1U}, {15U, 0U, 0U}, {15U, 1U, 1U}, - {54U, 0U, 0U}})) + ASSERT_TRUE( + readCtfExceptionRecords(contextDir / "stream_0") == + std::vector({{0U, 0U, 1U}, {0U, 1U, 1U}, {15U, 0U, 0U}, {15U, 1U, 1U}, {54U, 0U, 0U}})) << "a decoder warning must not reset the active CTF exception context"; auto dataLossOptions = makeCtfBundleConfig(dataLossDir, 1000000U); @@ -346,9 +356,9 @@ TEST(CtraceUnitTests, testCtfWarningsRemainVisibleWithoutResettingContext) dataLoss.writeEvent(issuePacket(TraceIssueCode::DataLoss, "decoder data loss")); dataLoss.writeEvent(exceptionPacket(15U, ExceptionAction::Returned, 20U)); dataLoss.stop(); - ASSERT_TRUE(readCtfExceptionRecords(dataLossDir / "stream_0") == - std::vector({{0U, 0U, 1U}, {0U, 1U, 1U}, {15U, 0U, 0U}, {15U, 1U, 1U}, - {15U, 2U, 0U}})) + ASSERT_TRUE( + readCtfExceptionRecords(dataLossDir / "stream_0") == + std::vector({{0U, 0U, 1U}, {0U, 1U, 1U}, {15U, 0U, 0U}, {15U, 1U, 1U}, {15U, 2U, 0U}})) << "filtered data-loss must still reset the CTF exception context"; } diff --git a/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp b/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp index e757867b7..6341b866e 100644 --- a/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp +++ b/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp @@ -26,6 +26,24 @@ static bool metaRejects(const TraceRunConfig& config, std::string_view message) return throwsWithMessage([&config] { (void)CtraceRunMeta::fromConfig(config); }, message); } +/** @brief Creates one explicitly formatted configuration for route-normalization tests. */ +static TraceRunConfig formattedConfig(std::vector references, std::vector setups = {}) +{ + TraceRunConfig config; + config.path = "trace.yml"; + config.traceFormat = TraceRunFormat::Formatted; + config.references = std::move(references); + config.setups = std::move(setups); + return config; +} + +/** @brief Creates a reference whose ctrace path is explicit at the call site. */ +static TraceRunReference routeReference(std::string type, std::string path, std::optional processorName, + std::optional stream, std::vector sources = {}) +{ + return makeReference(std::move(type), std::move(processorName), stream, std::move(sources), std::move(path)); +} + TEST(CtraceUnitTests, testTimestampPrescalerMetadataDefaults) { const auto ctraceRunMeta = CtraceRunMeta::fromConfig(TraceRunConfig{}); @@ -62,6 +80,8 @@ TEST(CtraceUnitTests, testCtraceRunMetaRejectsInvalidReferences) cases.push_back({duplicateDwtSource, "duplicate value in source array"}); cases.push_back( {makeReference("itm", std::nullopt, 0U, {1U}), "stream must be a CoreSight ATB trace ID between 1 and 111"}); + cases.push_back( + {routeReference("event", "events#0", "core", 0U), "stream must be a CoreSight ATB trace ID between 1 and 111"}); cases.push_back({makeReference("itm", std::nullopt, 1U, {32U}), "ITM source must be between 0 and 31"}); for (const auto& testCase : cases) { @@ -77,17 +97,21 @@ TEST(CtraceUnitTests, testCtraceRunMetaRejectsInvalidReferences) TraceRunConfig diagnosed; diagnosed.references.push_back(makeReference("itm", std::nullopt, 0U, {99U})); diagnosed.references.front().error = {"producer rejected this route"}; - EXPECT_NO_THROW((void)CtraceRunMeta::fromConfig(diagnosed)); + EXPECT_TRUE(metaRejects(diagnosed, "stream must be a CoreSight ATB trace ID between 1 and 111")); + + diagnosed.references.front() = makeReference("itm", std::nullopt, 0U, {1U, 1U}); + diagnosed.references.front().error = {"producer rejected duplicate sources"}; + EXPECT_TRUE(metaRejects(diagnosed, "stream must be a CoreSight ATB trace ID between 1 and 111")); TraceRunConfig diagnosedBinding; diagnosedBinding.path = "trace.yml"; diagnosedBinding.setups.push_back(makeTimestampSetup("core")); - diagnosedBinding.references.push_back(makeReference("itm", "unusable", 0U, {99U})); + diagnosedBinding.references.push_back(makeReference("itm", "core", 1U, {}, "core/itm")); diagnosedBinding.references.front().error = {"producer rejected this route"}; const auto meta = CtraceRunMeta::fromConfig(diagnosedBinding); EXPECT_EQ(meta.processorCount(), 1U); EXPECT_TRUE(meta.sources().empty()); - EXPECT_TRUE(meta.timestampsByTraceBusId().empty()); + EXPECT_EQ(meta.referenceDiagnostics().size(), 1U); } TEST(CtraceUnitTests, testCtraceRunMetaExpandsItmAndDwtSourceArrays) @@ -125,42 +149,154 @@ TEST(CtraceUnitTests, testCtraceRunMetaRejectsDuplicateSetups) { TraceRunConfig duplicate; duplicate.path = "trace.yml"; - duplicate.setups = {makeTimestampSetup(std::nullopt), makeTimestampSetup(std::nullopt)}; - EXPECT_TRUE(metaRejects(duplicate, "duplicate active ctrace-setup for pname ''")); - duplicate.setups.back().line = 9U; - EXPECT_TRUE(metaRejects(duplicate, "duplicate active 'ctrace-setup' for pname ''")); + duplicate.setups = {makeTimestampSetup(std::nullopt, 100U, 4U), makeTimestampSetup(std::nullopt, 100U, 4U)}; + const auto merged = CtraceRunMeta::fromConfig(duplicate); + EXPECT_EQ(merged.timestampClockHz(), std::optional(100U)); + EXPECT_EQ(merged.timestampPrescaler(), std::optional(4U)); + + duplicate.setups.back().timestamps->timestampPrescaler = 16U; + duplicate.setups.back().timestamps->line = 9U; + EXPECT_TRUE(metaRejects(duplicate, "conflicting timestamps.itm-prescaler values for one processor")); TraceRunConfig unnamedMultiProcessor; unnamedMultiProcessor.path = "trace.yml"; - unnamedMultiProcessor.setups = {makeTimestampSetup("a"), makeTimestampSetup(std::nullopt)}; + unnamedMultiProcessor.setups = {makeTimestampSetup("a"), makeTimestampSetup("b"), makeTimestampSetup(std::nullopt)}; EXPECT_TRUE(metaRejects(unnamedMultiProcessor, "pname is required for every ctrace-setup in a multi-processor configuration")); + + TraceRunConfig inferredFragment; + inferredFragment.setups = {makeTimestampSetup("core", 100U, 4U), makeTimestampSetup(std::nullopt, 100U, 4U)}; + const auto inferred = CtraceRunMeta::fromConfig(inferredFragment); + EXPECT_EQ(inferred.processorCount(), 1U); + EXPECT_EQ(inferred.routes().front().processorName, std::optional("core")); + + TraceRunConfig conflictingClock; + conflictingClock.setups = {makeTimestampSetup("core", 100U, 4U), makeTimestampSetup("core", 200U, 4U)}; + const auto deferredClock = CtraceRunMeta::fromConfig(conflictingClock); + EXPECT_FALSE(deferredClock.timestampClockHz().has_value()); + ASSERT_EQ(deferredClock.timestampClockErrors().size(), 1U); + EXPECT_EQ(deferredClock.routes().front().timestampClockError, + std::optional("conflicting active ctrace-setup timestamps.clock values")); + + auto missingClock = makeTimestampSetup("core", std::nullopt, 4U); + for (const auto reverse : {false, true}) { + TraceRunConfig complementConfig; + complementConfig.setups = reverse ? std::vector{makeTimestampSetup("core", 100U, 4U), missingClock} + : std::vector{missingClock, makeTimestampSetup("core", 100U, 4U)}; + const auto complemented = CtraceRunMeta::fromConfig(complementConfig); + EXPECT_EQ(complemented.timestampClockHz(), std::optional(100U)); + EXPECT_FALSE(complemented.routes().front().timestampClockError.has_value()); + } + + auto firstError = makeTimestampSetup("core", std::nullopt, 4U); + firstError.timestamps->clockError = "first clock error"; + auto secondError = firstError; + secondError.timestamps->clockError = "second clock error"; + TraceRunConfig conflictingErrors; + conflictingErrors.setups = {firstError, secondError}; + const auto deferredErrors = CtraceRunMeta::fromConfig(conflictingErrors); + EXPECT_EQ(deferredErrors.routes().front().timestampClockError, + std::optional("conflicting active ctrace-setup timestamps.clock values")); } -TEST(CtraceUnitTests, testCtraceRunMetaWarnsForCrossRootProcessorIdentityConflicts) +TEST(CtraceUnitTests, testCtraceRunMetaNormalizesAmbiguousUnformattedProcessorIdentities) { TraceRunConfig multiUnnamed; multiUnnamed.path = "trace.yml"; multiUnnamed.setups = {makeTimestampSetup("a"), makeTimestampSetup("b")}; multiUnnamed.references.push_back(makeReference("itm", std::nullopt, 1U, {1U})); multiUnnamed.references.front().line = 17U; - auto diagnosedReference = makeReference("itm", "a", 0U, {99U}); - diagnosedReference.error = {"producer rejected this route"}; - multiUnnamed.references.push_back(diagnosedReference); const auto unnamedMeta = CtraceRunMeta::fromConfig(multiUnnamed); EXPECT_EQ(unnamedMeta.processorCount(), 2U); EXPECT_TRUE(unnamedMeta.sources().empty()); - ASSERT_EQ(unnamedMeta.warnings().size(), 1U); - EXPECT_NE(unnamedMeta.warnings().front().message.find("without pname"), std::string::npos); - ASSERT_EQ(unnamedMeta.warnings().front().context.size(), 4U); - EXPECT_EQ(unnamedMeta.warnings().front().context[2], (std::pair{"line", "17"})); + EXPECT_FALSE(unnamedMeta.routes().front().processorName.has_value()); TraceRunConfig multiUnmatched = multiUnnamed; multiUnmatched.references.front().processorName = "c"; const auto unmatchedMeta = CtraceRunMeta::fromConfig(multiUnmatched); + EXPECT_EQ(unmatchedMeta.processorCount(), 2U); EXPECT_TRUE(unmatchedMeta.sources().empty()); - ASSERT_EQ(unmatchedMeta.warnings().size(), 1U); - EXPECT_NE(unmatchedMeta.warnings().front().message.find("no matching ctrace-setup"), std::string::npos); + + TraceRunConfig selected; + selected.setups = { + makeTimestampSetup("a", 100U, 4U), + makeTimestampSetup("b", 200U, 16U), + }; + selected.references = {makeReference("itm", "a", 5U, {1U}, "a/itm")}; + const auto selectedMeta = CtraceRunMeta::fromConfig(selected); + EXPECT_EQ(selectedMeta.processorCount(), 1U); + EXPECT_EQ(selectedMeta.routes().front().processorName, std::optional("a")); + EXPECT_EQ(selectedMeta.routes().front().timestampClockHz, std::optional(100U)); + EXPECT_EQ(selectedMeta.routes().front().timestampPrescaler, 4U); + EXPECT_EQ(selectedMeta.sources().front().traceBusId, 0U); + + selected.references.push_back(makeReference("itm", std::nullopt, 5U, {2U}, "messages")); + const auto inferredReferenceMeta = CtraceRunMeta::fromConfig(selected); + ASSERT_EQ(inferredReferenceMeta.sources().size(), 2U); + EXPECT_EQ(inferredReferenceMeta.sources().back().processorName, std::optional("a")); + + selected.references.back().stream = 6U; + const auto ambiguousReferenceMeta = CtraceRunMeta::fromConfig(selected); + ASSERT_EQ(ambiguousReferenceMeta.sources().size(), 1U); + ASSERT_EQ(ambiguousReferenceMeta.warnings().size(), 1U); + EXPECT_NE(ambiguousReferenceMeta.warnings().front().message.find("processor binding is ambiguous"), + std::string::npos); + + selected.references.push_back(routeReference("event", "unused", std::nullopt, std::nullopt)); + const auto ignoredReferenceMeta = CtraceRunMeta::fromConfig(selected); + EXPECT_EQ(ignoredReferenceMeta.sources().size(), 1U); + + TraceRunConfig eventBinding; + eventBinding.setups = selected.setups; + eventBinding.references = {routeReference("event", "a/events#0", "a", 5U)}; + const auto eventMeta = CtraceRunMeta::fromConfig(eventBinding); + EXPECT_EQ(eventMeta.routes().front().processorName, std::optional("a")); + + eventBinding.setups.clear(); + eventBinding.references.push_back(routeReference("exception", "b/exceptions", "b", 6U)); + const auto eventCandidates = CtraceRunMeta::fromConfig(eventBinding); + EXPECT_EQ(eventCandidates.processorCount(), 2U); + EXPECT_FALSE(eventCandidates.routes().front().processorName.has_value()); + + TraceRunConfig pathOnly; + pathOnly.path = "trace.yml"; + pathOnly.references = { + routeReference("itm", "a/itm", std::nullopt, 1U, {1U}), + routeReference("itm", "b/itm", std::nullopt, 2U, {2U}), + }; + const auto pathOnlyMeta = CtraceRunMeta::fromConfig(pathOnly); + EXPECT_EQ(pathOnlyMeta.processorCount(), 2U); + ASSERT_EQ(pathOnlyMeta.sources().size(), 2U); + EXPECT_FALSE(pathOnlyMeta.routes().front().processorName.has_value()); + + TraceRunConfig pathMismatch; + pathMismatch.path = "trace.yml"; + pathMismatch.setups = {makeTimestampSetup("a")}; + pathMismatch.references = {routeReference("itm", "b/itm", std::nullopt, 1U, {1U})}; + const auto mismatchMeta = CtraceRunMeta::fromConfig(pathMismatch); + EXPECT_TRUE(mismatchMeta.sources().empty()); + + pathMismatch.references.front().processorName = "a"; + EXPECT_TRUE(metaRejects(pathMismatch, "path processor conflicts with pname")); + + TraceRunConfig opaquePath; + opaquePath.references = {routeReference("itm", "opaque/printf-route", std::nullopt, 1U, {1U})}; + const auto opaqueMeta = CtraceRunMeta::fromConfig(opaquePath); + ASSERT_EQ(opaqueMeta.sources().size(), 1U); + EXPECT_FALSE(opaqueMeta.sources().front().processorName.has_value()); + + TraceRunConfig pathBoundData; + TraceRunSetup foreignData; + foreignData.processorName = "core"; + foreignData.data = {TraceRunDataSetup{2U}}; + pathBoundData.setups = {foreignData}; + auto otherData = routeReference("dwt", "other/data#0", std::nullopt, 1U, {0U}); + otherData.dataSetupIndex = 0U; + pathBoundData.references = {otherData}; + const auto pathBoundMeta = CtraceRunMeta::fromConfig(pathBoundData); + ASSERT_EQ(pathBoundMeta.sources().size(), 1U); + EXPECT_EQ(pathBoundMeta.routes().front().processorName, std::optional("other")); + EXPECT_EQ(pathBoundMeta.sources().front().dataSize, TraceRunSchema::kDefaultDwtDataSize); } TEST(CtraceUnitTests, testCtraceRunMetaWarnsForSingleSetupIdentityConflicts) @@ -180,10 +316,7 @@ TEST(CtraceUnitTests, testCtraceRunMetaWarnsForSingleSetupIdentityConflicts) makeReference("itm", "a", 1U, {1U}), makeReference("itm", "b", 2U, {2U}), }; - const auto unnamedMeta = CtraceRunMeta::fromConfig(unnamedSetup); - EXPECT_EQ(unnamedMeta.processorCount(), 1U); - EXPECT_EQ(unnamedMeta.sources().size(), 2U); - ASSERT_EQ(unnamedMeta.warnings().size(), 1U); + EXPECT_TRUE(metaRejects(unnamedSetup, "unformatted SINGLE trace requires one unambiguous processor")); TraceRunConfig referencesOnly; referencesOnly.path = "trace.yml"; @@ -194,9 +327,10 @@ TEST(CtraceUnitTests, testCtraceRunMetaWarnsForSingleSetupIdentityConflicts) EXPECT_TRUE(metaRejects(referencesOnly, "pname is required for every ctrace-ref")); referencesOnly.references.pop_back(); - const auto meta = CtraceRunMeta::fromConfig(referencesOnly); - EXPECT_EQ(meta.processorCount(), 2U); - EXPECT_FALSE(meta.timestampClockHz().has_value()); + const auto mergedReferences = CtraceRunMeta::fromConfig(referencesOnly); + EXPECT_EQ(mergedReferences.processorCount(), 2U); + EXPECT_EQ(mergedReferences.sources().size(), 2U); + EXPECT_FALSE(mergedReferences.routes().front().processorName.has_value()); } TEST(CtraceUnitTests, testCtraceRunMetaBindsOneNamedReferenceToUnnamedSetup) @@ -210,8 +344,8 @@ TEST(CtraceUnitTests, testCtraceRunMetaBindsOneNamedReferenceToUnnamedSetup) ASSERT_EQ(meta.sources().size(), 1U); EXPECT_EQ(meta.sources().front().processorName, std::optional("core")); ASSERT_EQ(meta.timestampsByTraceBusId().size(), 1U); - EXPECT_EQ(meta.timestampsByTraceBusId().at(1U).processorName, std::optional("core")); - EXPECT_EQ(meta.timestampsByTraceBusId().at(1U).clockHz, std::optional(100U)); + EXPECT_EQ(meta.timestampsByTraceBusId().at(0U).processorName, std::optional("core")); + EXPECT_EQ(meta.timestampsByTraceBusId().at(0U).clockHz, std::optional(100U)); } TEST(CtraceUnitTests, testCtraceRunMetaBindsStreamlessTimestampToInternalRoute) @@ -230,7 +364,7 @@ TEST(CtraceUnitTests, testCtraceRunMetaBindsStreamlessTimestampToInternalRoute) EXPECT_EQ(meta.timestampPrescalersByTraceBusId().at(0U), 1U); } -TEST(CtraceUnitTests, testCtraceRunMetaMapsDistinctProcessorSettings) +TEST(CtraceUnitTests, testCtraceRunMetaMergesCompatibleUnformattedProcessorSettings) { TraceRunConfig config; config.path = "trace.yml"; @@ -244,15 +378,37 @@ TEST(CtraceUnitTests, testCtraceRunMetaMapsDistinctProcessorSettings) }; const auto meta = CtraceRunMeta::fromConfig(config); - EXPECT_FALSE(meta.timestampClockHz().has_value()); + EXPECT_EQ(meta.processorCount(), 2U); EXPECT_EQ(meta.timestampPrescaler(), std::optional(4U)); + EXPECT_FALSE(meta.timestampClockHz().has_value()); EXPECT_FALSE(meta.itmEnableMask().has_value()); - ASSERT_EQ(meta.itmEnableMasksByTraceBusId().size(), 1U); - EXPECT_EQ(meta.itmEnableMasksByTraceBusId().at(5U), 1U); - ASSERT_EQ(meta.timestampsByTraceBusId().size(), 1U); - EXPECT_EQ(meta.timestampsByTraceBusId().at(5U).clockHz, std::optional(100U)); - EXPECT_FALSE(meta.timestampsByTraceBusId().at(5U).clockError.has_value()); - EXPECT_EQ(meta.warnings().size(), 2U); + ASSERT_EQ(meta.routes().size(), 1U); + EXPECT_FALSE(meta.routes().front().processorName.has_value()); + EXPECT_EQ(meta.routes().front().timestampClockError, + std::optional( + "unformatted SINGLE trace has ambiguous timestamps.clock values across processor candidates")); + ASSERT_EQ(meta.sources().size(), 2U); + EXPECT_EQ(meta.sources()[0].traceBusId, 0U); + EXPECT_EQ(meta.sources()[1].traceBusId, 0U); + + config.setups[1] = makeTimestampSetup("b", 100U, 4U, 1U); + const auto equivalent = CtraceRunMeta::fromConfig(config); + EXPECT_EQ(equivalent.timestampClockHz(), std::optional(100U)); + EXPECT_EQ(equivalent.itmEnableMask(), std::optional(1U)); + + TraceRunSetup defaulted; + defaulted.processorName = "b"; + defaulted.itm = TraceRunItmSetup{1U}; + config.setups[1] = defaulted; + EXPECT_TRUE(metaRejects(config, "different timestamps.itm-prescaler values")); + + config.setups[0].timestamps->timestampPrescaler = 1U; + const auto missingClock = CtraceRunMeta::fromConfig(config); + EXPECT_EQ(missingClock.timestampPrescaler(), std::optional(1U)); + EXPECT_FALSE(missingClock.timestampClockHz().has_value()); + EXPECT_EQ(missingClock.routes().front().timestampClockError, + std::optional( + "unformatted SINGLE trace has ambiguous timestamps.clock values across processor candidates")); } TEST(CtraceUnitTests, testCtraceRunMetaMapsDistinctPrescalersPerStream) @@ -266,6 +422,11 @@ TEST(CtraceUnitTests, testCtraceRunMetaMapsDistinctPrescalersPerStream) makeReference("itm", "a", 5U, {1U}), makeReference("itm", "b", 6U, {2U}), }; + EXPECT_TRUE(metaRejects(config, "different timestamps.itm-prescaler values")); + + config.traceFormat = TraceRunFormat::Formatted; + config.references[0].ctraceRef = "a/itm"; + config.references[1].ctraceRef = "b/itm"; const auto meta = CtraceRunMeta::fromConfig(config); EXPECT_TRUE(meta.hasDistinctProcessorPrescalers()); EXPECT_FALSE(meta.timestampPrescaler().has_value()); @@ -275,9 +436,7 @@ TEST(CtraceUnitTests, testCtraceRunMetaMapsDistinctPrescalersPerStream) config.path = "trace.yml"; config.references.back().stream = 5U; config.references.back().line = 23U; - const auto conflictingMeta = CtraceRunMeta::fromConfig(config); - EXPECT_EQ(conflictingMeta.timestampPrescalersByTraceBusId().at(5U), 4U); - EXPECT_EQ(conflictingMeta.warnings().size(), 2U); + EXPECT_TRUE(metaRejects(config, "conflicting ITM processor bindings")); } TEST(CtraceUnitTests, testCtraceRunMetaResolvesDwtDataAndDefaults) @@ -325,6 +484,30 @@ TEST(CtraceUnitTests, testCtraceRunMetaIgnoresInactiveSetups) const auto meta = CtraceRunMeta::fromConfig(config); EXPECT_EQ(meta.processorCount(), 1U); EXPECT_EQ(meta.sources().front().dataType, std::string(TraceRunSchema::kDefaultDwtDataType)); + + TraceRunConfig dataOnly; + TraceRunSetup dataSetup; + dataSetup.processorName = "core"; + dataSetup.data.resize(1U); + dataOnly.setups.push_back(dataSetup); + auto dataReference = makeReference("dwt", "core", 1U, {0U}, "core/data#0"); + dataReference.dataSetupIndex = 0U; + dataOnly.references.push_back(dataReference); + const auto dataMeta = CtraceRunMeta::fromConfig(dataOnly); + EXPECT_EQ(dataMeta.processorCount(), 1U); + ASSERT_EQ(dataMeta.sources().size(), 1U); + + TraceRunConfig unrelatedFeature; + unrelatedFeature.setups.push_back(makeTimestampSetup("core", 100U)); + TraceRunSetup ignoredSetup; + ignoredSetup.processorName = "other"; + ignoredSetup.featurePaths = {"other/instructions"}; + unrelatedFeature.setups.push_back(ignoredSetup); + unrelatedFeature.references.push_back(makeReference("itm", "core", 1U, {1U}, "core/itm")); + unrelatedFeature.references.push_back(routeReference("unsupported", "other/instructions", "other", 2U)); + const auto unrelatedMeta = CtraceRunMeta::fromConfig(unrelatedFeature); + EXPECT_EQ(unrelatedMeta.processorCount(), 1U); + EXPECT_EQ(unrelatedMeta.routes().front().processorName, std::optional("core")); } TEST(CtraceUnitTests, testCtraceRunMetaDoesNotExposeDwtControlReferencesAsDataSources) @@ -340,3 +523,489 @@ TEST(CtraceUnitTests, testCtraceRunMetaDoesNotExposeDwtControlReferencesAsDataSo EXPECT_EQ(meta.sources().front().source, 0U); EXPECT_EQ(meta.processorCount(), 1U); } + +TEST(CtraceUnitTests, testCtraceRunMetaCreatesOneSyntheticUnformattedRoute) +{ + for (const auto format : + {std::optional{}, std::optional{TraceRunFormat::Unformatted}}) { + TraceRunConfig config; + config.traceFormat = format; + config.references = {routeReference("itm", "messages", "core", 7U, {1U})}; + + const auto meta = CtraceRunMeta::fromConfig(config); + + ASSERT_EQ(meta.routes().size(), 1U); + const auto& route = meta.routes().front(); + EXPECT_EQ(route.protocol, CtraceRunProtocol::Itm); + EXPECT_FALSE(route.traceBusId.has_value()); + EXPECT_FALSE(route.timestampsConfigured); + EXPECT_EQ(route.timestampPrescaler, TraceRunSchema::kDefaultTimestampPrescaler); + ASSERT_EQ(route.sources.size(), 1U); + EXPECT_EQ(route.sources.front().traceBusId, 0U); + ASSERT_EQ(meta.sources().size(), 1U); + EXPECT_EQ(meta.sources().front().traceBusId, 0U) << "SINGLE accessors must expose the transport channel"; + } + + const auto emptyMeta = CtraceRunMeta::fromConfig(TraceRunConfig{}); + ASSERT_EQ(emptyMeta.routes().size(), 1U); + EXPECT_FALSE(emptyMeta.routes().front().traceBusId.has_value()); +} + +TEST(CtraceUnitTests, testCtraceRunMetaBuildsFormattedAnchorRoutesAndMetadata) +{ + auto firstSetup = makeTimestampSetup("first", 100U, 4U, 3U); + firstSetup.data.resize(1U); + firstSetup.data.front().size = 2U; + auto secondSetup = makeTimestampSetup("second", 200U, 16U, 5U); + + auto firstAnchor = routeReference("itm", "first/itm", "first", 1U, {1U}); + firstAnchor.info = {"producer info"}; + firstAnchor.warning = {"producer warning"}; + firstAnchor.error = {"producer error"}; + auto data = routeReference("dwt", "first/data#0", "first", 1U, {2U}); + data.dataSetupIndex = 0U; + data.address = 0x20000000U; + auto secondAnchor = routeReference("itm", "second/itm", "second", 111U); + const auto config = formattedConfig({firstAnchor, data, secondAnchor}, {firstSetup, secondSetup}); + + const auto meta = CtraceRunMeta::fromConfig(config); + + ASSERT_EQ(meta.routes().size(), 2U); + const auto& first = meta.routes()[0]; + EXPECT_EQ(first.traceBusId, std::optional(1U)); + EXPECT_EQ(first.processorName, std::optional("first")); + EXPECT_TRUE(first.timestampsConfigured); + EXPECT_EQ(first.timestampClockHz, std::optional(100U)); + EXPECT_EQ(first.timestampPrescaler, 4U); + EXPECT_EQ(first.itmEnableMask, std::optional(3U)); + ASSERT_EQ(first.sources.size(), 2U); + EXPECT_EQ(first.sources[0].type, "itm"); + EXPECT_EQ(first.sources[1].type, "dwt"); + EXPECT_EQ(first.sources[1].dataSize, 2U); + ASSERT_EQ(first.referenceDiagnostics.size(), 3U); + EXPECT_EQ(first.referenceDiagnostics[0].severity, CtraceRunReferenceDiagnostic::Severity::Info); + EXPECT_EQ(first.referenceDiagnostics[1].severity, CtraceRunReferenceDiagnostic::Severity::Warning); + EXPECT_EQ(first.referenceDiagnostics[2].severity, CtraceRunReferenceDiagnostic::Severity::Error); + EXPECT_EQ(meta.referenceDiagnostics().size(), 3U); + + const auto& second = meta.routes()[1]; + EXPECT_EQ(second.traceBusId, std::optional(111U)); + EXPECT_EQ(second.processorName, std::optional("second")); + EXPECT_EQ(second.timestampClockHz, std::optional(200U)); + EXPECT_EQ(second.timestampPrescaler, 16U); +} + +TEST(CtraceUnitTests, testCtraceRunMetaAcceptsOnlyConstrainedFormattedFallbacks) +{ + struct Fallback { + const char* type; + const char* path; + bool data; + }; + const std::vector accepted{ + {"dwt", "data#0", true}, {"itm", "timestamps", false}, + {"dwt", "timestamps", false}, {"exception", "exceptions", false}, + {"event", "events#0", false}, {"pmu", "events#0", false}, + {"pcsample", "pcsampling", false}, {"dwt", "synchronization", false}, + }; + for (const auto& fallback : accepted) { + auto reference = routeReference(fallback.type, fallback.path, std::nullopt, 1U); + if (fallback.data) { + reference.dataSetupIndex = 0U; + } + const auto meta = CtraceRunMeta::fromConfig(formattedConfig({reference})); + ASSERT_EQ(meta.routes().size(), 1U) << fallback.type << " / " << fallback.path; + EXPECT_EQ(meta.routes().front().traceBusId, std::optional(1U)); + } + + const std::vector rejected{ + routeReference("overflow", "overflow", std::nullopt, 1U), + routeReference("global_ts", "timesync", std::nullopt, 1U), + routeReference("dwt", "instructions", std::nullopt, 1U), + routeReference("itm", "messages", std::nullopt, 1U), + routeReference("dwt", "timestamps", std::nullopt, std::nullopt), + }; + for (const auto& reference : rejected) { + EXPECT_TRUE( + metaRejects(formattedConfig({reference}), "requires an ITM route anchor or supported feature fallback")); + } + + EXPECT_TRUE(metaRejects(formattedConfig({routeReference("itm", "nested/core/itm", "core", 1U)}), + "anchor path must use '[pname/]itm'")); + auto nestedData = routeReference("dwt", "nested/core/data#0", "core", 1U); + nestedData.dataSetupIndex = 0U; + EXPECT_TRUE(metaRejects(formattedConfig({nestedData}), "requires an ITM route anchor or supported feature fallback")); + EXPECT_TRUE(metaRejects(formattedConfig({routeReference("event", "timestamps", std::nullopt, 1U)}), + "timestamps reference must use type 'itm' or transitional type 'dwt'")); +} + +TEST(CtraceUnitTests, testCtraceRunMetaValidatesFormattedRouteBindingsAndIds) +{ + for (const auto id : {0U, 112U, 127U}) { + auto invalid = routeReference("itm", "itm", std::nullopt, id); + invalid.error = {"producer diagnostic must not hide the invalid ID"}; + EXPECT_TRUE(metaRejects(formattedConfig({invalid}), "between 1 and 111")); + } + + const auto sameIdDifferentProcessors = formattedConfig({ + routeReference("itm", "first/itm", "first", 1U), + routeReference("itm", "second/itm", "second", 1U), + }); + EXPECT_TRUE(metaRejects(sameIdDifferentProcessors, "conflicting ITM processor bindings")); + + const auto conflictingContentProcessor = formattedConfig({ + routeReference("itm", "core/itm", "core", 1U), + routeReference("itm", "other/messages", "other", 1U, {1U}), + }); + EXPECT_TRUE(metaRejects(conflictingContentProcessor, "conflicting ITM processor bindings")); + + const auto sameProcessorDifferentIds = formattedConfig({ + routeReference("itm", "core/itm", "core", 1U), + routeReference("itm", "core/itm", "core", 2U), + }); + EXPECT_TRUE(metaRejects(sameProcessorDifferentIds, "bound to multiple CoreSight Trace Bus IDs")); + + auto fallback = routeReference("dwt", "core/data#0", "core", 1U); + fallback.dataSetupIndex = 0U; + const auto compatible = + CtraceRunMeta::fromConfig(formattedConfig({routeReference("itm", "core/itm", "core", 1U), fallback})); + EXPECT_EQ(compatible.routes().size(), 1U); + + const auto unbound = CtraceRunMeta::fromConfig(formattedConfig({ + routeReference("itm", "itm", std::nullopt, 1U), + routeReference("itm", "itm", std::nullopt, 111U), + })); + ASSERT_EQ(unbound.routes().size(), 2U); + EXPECT_FALSE(unbound.routes()[0].processorName.has_value()); + EXPECT_FALSE(unbound.routes()[1].processorName.has_value()); + + EXPECT_TRUE(metaRejects(formattedConfig({routeReference("itm", "itm", std::nullopt, std::nullopt)}), + "anchor requires a CoreSight Trace Bus ID")); + EXPECT_TRUE(metaRejects(formattedConfig({routeReference("dwt", "core/itm", "core", 1U)}), + "anchor must use reference type 'itm'")); + + EXPECT_TRUE(metaRejects(formattedConfig({routeReference("itm", "core/itm", "core", 1U, {32U})}), + "ITM source must be between 0 and 31")); + + const auto adoptedProcessor = CtraceRunMeta::fromConfig(formattedConfig({ + routeReference("itm", "itm", std::nullopt, 1U), + routeReference("itm", "core/itm", "core", 1U), + })); + EXPECT_EQ(adoptedProcessor.routes().front().processorName, std::optional("core")); + + EXPECT_TRUE(metaRejects(formattedConfig({routeReference("itm", "core/itm", "core", 1U), + routeReference("itm", "other/itm", "core", 1U, {1U})}), + "path processor conflicts with pname")); +} + +TEST(CtraceUnitTests, testCtraceRunMetaValidatesFormattedSetupInference) +{ + const std::vector processors{ + makeTimestampSetup("first"), + makeTimestampSetup("second"), + }; + EXPECT_TRUE(metaRejects(formattedConfig({routeReference("itm", "itm", std::nullopt, 1U)}, processors), + "pname is required for a formatted ctrace-ref")); + EXPECT_TRUE(metaRejects(formattedConfig({routeReference("itm", "ghost/itm", "ghost", 1U)}, processors), + "has no matching active ctrace-setup processor")); + + auto unnamedFragment = makeTimestampSetup(std::nullopt); + EXPECT_TRUE(metaRejects(formattedConfig({routeReference("itm", "first/itm", "first", 1U)}, + {processors[0], processors[1], unnamedFragment}), + "pname is required for active ctrace-setup fragments")); + + const auto inferred = CtraceRunMeta::fromConfig( + formattedConfig({routeReference("itm", "itm", std::nullopt, 1U)}, {makeTimestampSetup("core")})); + ASSERT_EQ(inferred.routes().size(), 1U); + EXPECT_EQ(inferred.routes().front().processorName, std::optional("core")); + + EXPECT_TRUE( + metaRejects(formattedConfig({routeReference("itm", "other/itm", "core", 1U)}, {makeTimestampSetup("core")}), + "path processor conflicts with pname")); + EXPECT_TRUE( + metaRejects(formattedConfig({routeReference("itm", "other/itm", std::nullopt, 1U)}, {makeTimestampSetup("core")}), + "has no matching active ctrace-setup processor")); + + const auto unnamedSetup = makeTimestampSetup(std::nullopt); + EXPECT_TRUE(metaRejects(formattedConfig({routeReference("itm", "first/itm", "first", 1U), + routeReference("itm", "second/itm", "second", 2U)}, + {unnamedSetup}), + "one unnamed ctrace-setup processor cannot bind multiple formatted pnames")); + EXPECT_TRUE(metaRejects( + formattedConfig({routeReference("itm", "itm", std::nullopt, 1U), routeReference("itm", "itm", std::nullopt, 2U)}, + {unnamedSetup}), + "one unnamed ctrace-setup processor cannot bind multiple formatted ITM routes")); + + const auto unnamedWithContent = CtraceRunMeta::fromConfig(formattedConfig( + {routeReference("itm", "core/itm", "core", 1U), routeReference("itm", "core/messages", "core", 1U, {1U})}, + {unnamedSetup})); + EXPECT_EQ(unnamedWithContent.routes().front().processorName, std::optional("core")); +} + +TEST(CtraceUnitTests, testCtraceRunMetaMergesRepeatedFormattedSetupFragments) +{ + auto timestamps = makeTimestampSetup("core", 100U, 4U); + timestamps.featurePaths = {"core/timestamps"}; + TraceRunSetup itm; + itm.itm = TraceRunItmSetup{3U}; + itm.featurePaths = {"itm"}; + auto config = formattedConfig({routeReference("itm", "core/itm", "core", 1U)}, {timestamps, itm}); + + const auto meta = CtraceRunMeta::fromConfig(config); + ASSERT_EQ(meta.routes().size(), 1U); + EXPECT_EQ(meta.routes().front().timestampClockHz, std::optional(100U)); + EXPECT_EQ(meta.routes().front().timestampPrescaler, 4U); + EXPECT_EQ(meta.routes().front().itmEnableMask, std::optional(3U)); + + auto conflictingPrescaler = timestamps; + conflictingPrescaler.timestamps->timestampPrescaler = 16U; + EXPECT_TRUE(metaRejects(formattedConfig(config.references, {timestamps, conflictingPrescaler}), + "conflicting timestamps.itm-prescaler")); + + auto invalidPrescaler = timestamps; + invalidPrescaler.timestamps->timestampPrescaler = 2U; + invalidPrescaler.timestamps->line = 12U; + EXPECT_TRUE(metaRejects(formattedConfig(config.references, {invalidPrescaler}), "trace.yml(12)")); + + auto conflictingClock = timestamps; + conflictingClock.timestamps->clockHz = 200U; + const auto deferredClock = + CtraceRunMeta::fromConfig(formattedConfig(config.references, {timestamps, conflictingClock})); + EXPECT_TRUE(deferredClock.routes().front().timestampClockError.has_value()); + + auto missingClock = timestamps; + missingClock.timestamps->clockHz.reset(); + for (const auto reverse : {false, true}) { + const auto complemented = CtraceRunMeta::fromConfig( + formattedConfig(config.references, reverse ? std::vector{timestamps, missingClock} + : std::vector{missingClock, timestamps})); + EXPECT_EQ(complemented.routes().front().timestampClockHz, std::optional(100U)); + EXPECT_FALSE(complemented.routes().front().timestampClockError.has_value()); + } + + auto conflictingItm = itm; + conflictingItm.itm->enableMask = 5U; + const auto nonFatalItm = CtraceRunMeta::fromConfig(formattedConfig(config.references, {itm, conflictingItm})); + EXPECT_FALSE(nonFatalItm.routes().front().itmEnableMask.has_value()); + EXPECT_TRUE(nonFatalItm.routes().front().itmEnableError.has_value()); + ASSERT_EQ(nonFatalItm.warnings().size(), 1U); + EXPECT_NE(nonFatalItm.warnings().front().message.find("itm.enable"), std::string::npos); + + TraceRunSetup firstData; + firstData.processorName = "core"; + firstData.data.resize(1U); + firstData.data.front().size = 2U; + firstData.featurePaths = {"core/data#0"}; + auto secondData = firstData; + auto dataReference = routeReference("dwt", "core/data#0", "core", 1U, {0U}); + dataReference.dataSetupIndex = 0U; + const auto compatibleData = + CtraceRunMeta::fromConfig(formattedConfig({config.references.front(), dataReference}, {firstData, secondData})); + ASSERT_EQ(compatibleData.routes().front().sources.size(), 1U); + EXPECT_EQ(compatibleData.routes().front().sources.front().dataSize, 2U); + EXPECT_FALSE(compatibleData.routes().front().sources.front().dataSizeError.has_value()); + + secondData.data.front().size = 4U; + const auto conflictingData = + CtraceRunMeta::fromConfig(formattedConfig({config.references.front(), dataReference}, {firstData, secondData})); + ASSERT_EQ(conflictingData.routes().front().sources.size(), 1U); + EXPECT_EQ(conflictingData.routes().front().sources.front().dataSize, TraceRunSchema::kDefaultDwtDataSize); + EXPECT_EQ(conflictingData.routes().front().sources.front().dataSizeError, + std::optional("conflicting active ctrace-setup data.size values")); + + auto defaultData = firstData; + defaultData.data.front().size.reset(); + secondData.data.front().size = 2U; + const auto defaultConflict = + CtraceRunMeta::fromConfig(formattedConfig({config.references.front(), dataReference}, {defaultData, secondData})); + EXPECT_EQ(defaultConflict.routes().front().sources.front().dataSizeError, + std::optional("conflicting active ctrace-setup data.size values")); + + secondData = firstData; + secondData.data.front().sizeError = "invalid setup size"; + const auto adoptedDataError = + CtraceRunMeta::fromConfig(formattedConfig({config.references.front(), dataReference}, {firstData, secondData})); + EXPECT_EQ(adoptedDataError.routes().front().sources.front().dataSizeError, + std::optional("invalid setup size")); + + firstData.data.front().sizeError = "first setup error"; + secondData.data.front().sizeError = "second setup error"; + const auto conflictingDataErrors = + CtraceRunMeta::fromConfig(formattedConfig({config.references.front(), dataReference}, {firstData, secondData})); + EXPECT_EQ(conflictingDataErrors.routes().front().sources.front().dataSizeError, + std::optional("conflicting active ctrace-setup data.size values")); +} + +TEST(CtraceUnitTests, testCtraceRunMetaRetainsItmSetupErrorsIndependentlyOfFragmentOrder) +{ + TraceRunSetup valid; + valid.processorName = "core"; + valid.itm = TraceRunItmSetup{3U}; + TraceRunSetup malformed = valid; + malformed.itm->enableMask.reset(); + malformed.itm->enableError = "invalid itm.enable"; + const auto anchor = routeReference("itm", "core/itm", "core", 1U); + + for (const auto format : {TraceRunFormat::Unformatted, TraceRunFormat::Formatted}) { + for (const auto reverse : {false, true}) { + auto setups = + reverse ? std::vector{malformed, valid} : std::vector{valid, malformed}; + auto config = formattedConfig({anchor}, std::move(setups)); + config.traceFormat = format; + const auto meta = CtraceRunMeta::fromConfig(config); + ASSERT_EQ(meta.routes().size(), 1U); + EXPECT_FALSE(meta.routes().front().itmEnableMask.has_value()); + EXPECT_EQ(meta.routes().front().itmEnableError, std::optional("invalid itm.enable")); + } + } + + auto secondMalformed = malformed; + secondMalformed.itm->enableError = "another itm.enable error"; + for (const auto format : {TraceRunFormat::Unformatted, TraceRunFormat::Formatted}) { + auto config = formattedConfig({anchor}, {malformed, secondMalformed}); + config.traceFormat = format; + const auto meta = CtraceRunMeta::fromConfig(config); + EXPECT_EQ(meta.routes().front().itmEnableError, + std::optional("conflicting active ctrace-setup itm.enable values")); + } + + auto secondValid = valid; + secondValid.itm->enableMask = 5U; + auto unformattedConflict = formattedConfig({anchor}, {valid, secondValid}); + unformattedConflict.traceFormat = TraceRunFormat::Unformatted; + const auto conflictMeta = CtraceRunMeta::fromConfig(unformattedConflict); + EXPECT_FALSE(conflictMeta.routes().front().itmEnableMask.has_value()); + EXPECT_EQ(conflictMeta.routes().front().itmEnableError, + std::optional("conflicting active ctrace-setup itm.enable values")); + ASSERT_EQ(conflictMeta.warnings().size(), 1U); + + TraceRunSetup emptyItm; + emptyItm.processorName = "core"; + emptyItm.itm = TraceRunItmSetup{}; + const auto emptyMeta = CtraceRunMeta::fromConfig(formattedConfig({anchor}, {emptyItm})); + EXPECT_FALSE(emptyMeta.routes().front().itmEnableMask.has_value()); + EXPECT_FALSE(emptyMeta.routes().front().itmEnableError.has_value()); +} + +TEST(CtraceUnitTests, testCtraceRunMetaResolvesDisabledFragmentsLocally) +{ + TraceRunSetup disabled; + disabled.processorName = "core"; + disabled.disabled = true; + disabled.ordinal = 4U; + disabled.featurePaths = {"core/itm"}; + const auto anchor = routeReference("itm", "core/itm", "core", 1U); + EXPECT_TRUE(metaRejects(formattedConfig({anchor}, {disabled}), "disabled ctrace-setup fragment 4")); + + TraceRunSetup active; + active.processorName = "core"; + active.itm = TraceRunItmSetup{1U}; + active.featurePaths = {"core/itm"}; + EXPECT_NO_THROW((void)CtraceRunMeta::fromConfig(formattedConfig({anchor}, {disabled, active}))); + + const auto shortAnchor = routeReference("itm", "itm", "core", 1U); + EXPECT_TRUE(metaRejects(formattedConfig({shortAnchor}, {disabled}), "disabled ctrace-setup fragment 4")); + + auto disabledData = disabled; + disabledData.featurePaths = {"core/data#0"}; + EXPECT_NO_THROW((void)CtraceRunMeta::fromConfig(formattedConfig({anchor}, {disabledData, active}))); + + auto disabledControl = disabled; + disabledControl.featurePaths = {"core/instructions", "core/synchronization"}; + EXPECT_TRUE( + metaRejects(formattedConfig({routeReference("dwt", "core/instructions/start", "core", 1U)}, {disabledControl}), + "resolves only to disabled ctrace-setup fragment")); + EXPECT_TRUE( + metaRejects(formattedConfig({routeReference("dwt", "core/synchronization#0", "core", 1U)}, {disabledControl}), + "resolves only to disabled ctrace-setup fragment")); + disabledControl.processorName.reset(); + disabledControl.featurePaths = {"instructions"}; + EXPECT_TRUE( + metaRejects(formattedConfig({routeReference("dwt", "core/instructions/start", "core", 1U)}, {disabledControl}), + "resolves only to disabled ctrace-setup fragment")); + EXPECT_NO_THROW((void)CtraceRunMeta::fromConfig(formattedConfig( + {anchor, routeReference("dwt", "other/instructions/start", "core", 1U)}, {disabledControl, active}))); + + TraceRunSetup foreignDisabled; + foreignDisabled.processorName = "other"; + foreignDisabled.disabled = true; + foreignDisabled.featurePaths = {"other/data#0"}; + auto data = routeReference("dwt", "core/data#0", std::nullopt, 1U); + data.dataSetupIndex = 0U; + EXPECT_NO_THROW((void)CtraceRunMeta::fromConfig(formattedConfig({data}, {foreignDisabled}))); + + TraceRunConfig unformatted; + unformatted.references = {anchor}; + unformatted.setups = {disabled}; + EXPECT_TRUE(metaRejects(unformatted, "resolves only to disabled ctrace-setup fragment")); +} + +TEST(CtraceUnitTests, testCtraceRunMetaRetainsDiagnosticsWithoutWeakeningRouting) +{ + auto diagnosedAnchor = routeReference("itm", "core/itm", "core", 1U, {32U}); + diagnosedAnchor.error = {"source setup failed"}; + const auto diagnosed = CtraceRunMeta::fromConfig(formattedConfig({diagnosedAnchor})); + ASSERT_EQ(diagnosed.routes().size(), 1U); + EXPECT_TRUE(diagnosed.routes().front().sources.empty()); + ASSERT_EQ(diagnosed.routes().front().referenceDiagnostics.size(), 1U); + EXPECT_EQ(diagnosed.routes().front().referenceDiagnostics.front().message, "source setup failed"); + + TraceRunConfig diagnosedSingle; + diagnosedSingle.references = {diagnosedAnchor}; + const auto single = CtraceRunMeta::fromConfig(diagnosedSingle); + EXPECT_EQ(single.routes().front().processorName, std::optional("core")); + EXPECT_TRUE(single.routes().front().sources.empty()); + + auto duplicateSource = routeReference("itm", "core/itm", "core", 1U, {1U, 1U}); + duplicateSource.error = {"duplicate producer source"}; + const auto duplicate = CtraceRunMeta::fromConfig(formattedConfig({duplicateSource})); + EXPECT_TRUE(duplicate.routes().front().sources.empty()); + + auto streamlessDiagnostic = routeReference("global_ts", "core/timesync", "core", std::nullopt); + streamlessDiagnostic.warning = {"time sync unavailable"}; + const auto routeDiagnostic = + CtraceRunMeta::fromConfig(formattedConfig({routeReference("itm", "core/itm", "core", 1U), streamlessDiagnostic})); + ASSERT_EQ(routeDiagnostic.routes().front().referenceDiagnostics.size(), 1U); + EXPECT_EQ(routeDiagnostic.routes().front().referenceDiagnostics.front().message, "time sync unavailable"); + + auto secondAnchor = routeReference("itm", "other/itm", "other", 2U); + const auto routedDiagnostic = CtraceRunMeta::fromConfig( + formattedConfig({routeReference("itm", "core/itm", "core", 1U), secondAnchor, streamlessDiagnostic})); + ASSERT_EQ(routedDiagnostic.routes().size(), 2U); + ASSERT_EQ(routedDiagnostic.routes()[0].referenceDiagnostics.size(), 1U); + EXPECT_EQ(routedDiagnostic.routes()[0].referenceDiagnostics.front().message, "time sync unavailable"); + EXPECT_TRUE(routedDiagnostic.routes()[1].referenceDiagnostics.empty()); + + const auto conflictingStreamless = formattedConfig({ + routeReference("itm", "itm", std::nullopt, 1U), + routeReference("global_ts", "first/timesync", "first", std::nullopt), + routeReference("overflow", "second/overflow", "second", std::nullopt), + }); + EXPECT_TRUE(metaRejects(conflictingStreamless, "streamless ctrace-ref cannot be associated")); + + EXPECT_TRUE(metaRejects(formattedConfig({routeReference("itm", "core/itm", "core", 1U), + routeReference("overflow", "core/overflow", "core", 2U)}), + "without an ITM route anchor or supported feature fallback")); +} + +TEST(CtraceUnitTests, testCtraceRunMetaDefersFormattedOutputMetadataErrors) +{ + auto setup = makeTimestampSetup("core", std::nullopt, std::nullopt); + setup.timestamps->clockError = "invalid timestamps.clock"; + auto data = routeReference("dwt", "core/data#0", "core", 1U, {0U}); + data.dataSetupIndex = 0U; + data.dataTypeError = "invalid data-type"; + data.dataSizeError = "invalid size"; + const auto meta = + CtraceRunMeta::fromConfig(formattedConfig({routeReference("itm", "core/itm", "core", 1U), data}, {setup})); + + ASSERT_EQ(meta.routes().size(), 1U); + const auto& route = meta.routes().front(); + EXPECT_TRUE(route.timestampsConfigured); + EXPECT_FALSE(route.timestampClockHz.has_value()); + EXPECT_EQ(route.timestampClockError, std::optional("invalid timestamps.clock")); + EXPECT_EQ(route.timestampPrescaler, TraceRunSchema::kDefaultTimestampPrescaler); + ASSERT_EQ(route.sources.size(), 1U); + EXPECT_EQ(route.sources.front().dataTypeError, std::optional("invalid data-type")); + EXPECT_EQ(route.sources.front().dataSizeError, std::optional("invalid size")); +} diff --git a/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp b/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp index f5d3a54cd..9a668b1c4 100644 --- a/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp +++ b/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -77,6 +78,7 @@ TEST(CtraceUnitTests, TraceRunReaderParsesConsumedFields) { TraceRunFixture file("ctrace-run-reader-consumed-fields-test"); const auto config = file.read(R"yml(ctrace-run: + trace-format: formatted ctrace-setup: - pname: core0 timestamps: @@ -152,6 +154,57 @@ TEST(CtraceUnitTests, TraceRunReaderAcceptsScalarAndArraySourceNotation) EXPECT_TRUE(config.references[2].sources == std::vector({4U, 5U})); } +TEST(CtraceUnitTests, TraceRunReaderParsesTraceFormatDeclaration) +{ + TraceRunFixture file("ctrace-run-reader-trace-format-test"); + const auto absent = file.read("ctrace-run:\n ctrace-refs: []\n"); + EXPECT_FALSE(absent.traceFormat.has_value()); + EXPECT_EQ(TraceRunSchema::effectiveTraceFormat(absent.traceFormat), TraceRunFormat::Unformatted); + + const auto nullValue = file.read("ctrace-run:\n trace-format: null\n ctrace-refs: []\n"); + EXPECT_FALSE(nullValue.traceFormat.has_value()); + EXPECT_EQ(TraceRunSchema::effectiveTraceFormat(nullValue.traceFormat), TraceRunFormat::Unformatted); + + const auto unformatted = file.read("ctrace-run:\n trace-format: unformatted\n ctrace-refs: []\n"); + EXPECT_EQ(unformatted.traceFormat, std::optional(TraceRunFormat::Unformatted)); + + const auto formatted = file.read("ctrace-run:\n trace-format: formatted\n ctrace-refs: []\n"); + EXPECT_EQ(formatted.traceFormat, std::optional(TraceRunFormat::Formatted)); +} + +TEST(CtraceUnitTests, TraceRunReaderRejectsInvalidTraceFormatDeclaration) +{ + TraceRunFixture file("ctrace-run-reader-trace-format-errors-test"); + expectReadError(file, "ctrace-run:\n trace-format: unknown\n ctrace-refs: []\n", + "'trace-format' must be 'unformatted' or 'formatted'"); + expectReadError(file, "ctrace-run:\n trace-format: ''\n ctrace-refs: []\n", + "'trace-format' must be 'unformatted' or 'formatted'"); + expectReadError(file, "ctrace-run:\n trace-format: []\n ctrace-refs: []\n", + "'trace-format' must be a scalar value"); + expectReadError(file, "ctrace-run:\n trace-format: {}\n ctrace-refs: []\n", + "'trace-format' must be a scalar value"); +} + +TEST(CtraceUnitTests, TraceRunReaderIgnoresCopiedItmAtbidForRouting) +{ + TraceRunFixture file("ctrace-run-reader-itm-atbid-test"); + const auto config = file.read(R"yml(ctrace-run: + trace-format: formatted + ctrace-setup: + - pname: core + itm: { enable: 3, atbid: [127] } + ctrace-refs: + - { type: itm, ctrace-ref: core/itm, pname: core, stream: 1 } +)yml"); + + ASSERT_EQ(config.setups.size(), 1U); + ASSERT_TRUE(config.setups.front().itm.has_value()); + EXPECT_EQ(config.setups.front().itm->enableMask, 3U); + const auto meta = CtraceRunMeta::fromConfig(config); + ASSERT_EQ(meta.routes().size(), 1U); + EXPECT_EQ(meta.routes().front().traceBusId, std::optional(1U)); +} + TEST(CtraceUnitTests, TraceRunReaderUsesReferencedSetupSizeAsFallback) { TraceRunFixture file("ctrace-run-reader-setup-size-test"); @@ -218,6 +271,7 @@ TEST(CtraceUnitTests, TraceRunReaderAcceptsProcessorItmReferenceWithoutEnabledCh { TraceRunFixture file("ctrace-run-reader-empty-itm-reference-test"); const auto config = file.read(R"yml(ctrace-run: + trace-format: formatted ctrace-setup: - pname: core0 itm: @@ -308,6 +362,11 @@ TEST(CtraceUnitTests, TraceRunReaderRejectsMalformedReferenceRoutes) testCase.fields + "\n"; expectReadError(file, yaml, testCase.error); } + + expectReadError(file, + "ctrace-run:\n ctrace-refs:\n - { type: exception, ctrace-ref: core/exceptions, stream: [], " + "error: producer-error }\n", + "'stream' must be a scalar unsigned integer"); } TEST(CtraceUnitTests, TraceRunReaderIgnoresNullAndNonScalarOptionalReferenceValues) @@ -367,34 +426,75 @@ TEST(CtraceUnitTests, TraceRunReaderPreservesDiagnosticReferences) TraceRunFixture file("ctrace-run-reader-diagnostic-references-test"); const auto config = file.read(R"yml(ctrace-run: ctrace-refs: - - { type: event, ctrace-ref: core/event, pname: core0, info: [note, detail], warning: [] } - - { type: pmu, ctrace-ref: core/pmu, pname: core1, warning: [warning] } - - { type: pcsample, ctrace-ref: core/pc, pname: null, error: unavailable } + - { type: event, ctrace-ref: core/events#0, pname: core0, stream: 1, info: [note, detail], warning: [] } + - { type: pmu, ctrace-ref: core/events#1, pname: core1, stream: 2, warning: [warning] } + - { type: pcsample, ctrace-ref: core/pcsampling, pname: null, stream: 3, error: unavailable } - { type: dwt, ctrace-ref: core/data#, source: 0, address: invalid, error: [diagnostic, detail] } - - { type: dwt, ctrace-ref: core/data#x, stream: [], source: 0, error: malformed } - { type: dwt, ctrace-ref: core/notdata#2, error: null } - { type: itm, ctrace-ref: core/itm0, source: 0, error: disabled } - { type: itm, ctrace-ref: core/itm1, source: 1, error: usable, label: null } - - { type: exception, ctrace-ref: core/exceptions, error: ignored } - - { type: global_ts, ctrace-ref: core/timesync, warning: ignored } - - { type: overflow, ctrace-ref: core/overflow, info: ignored } + - { type: exception, ctrace-ref: core/exceptions, pname: core0, stream: 4, error: exception-error } + - { type: global_ts, ctrace-ref: core/timesync, pname: core0, stream: 5, warning: global-warning } + - { type: overflow, ctrace-ref: core/overflow, pname: core0, stream: 6, info: overflow-info } )yml"); - ASSERT_EQ(config.references.size(), 8U) << "reader must ignore diagnostic annotations on unconsumed reference types"; + ASSERT_EQ(config.references.size(), 10U); EXPECT_EQ(config.references[0].processorName, std::optional("core0")); + EXPECT_EQ(config.references[0].stream, std::optional(1U)); EXPECT_EQ(config.references[0].info, (std::vector{"note", "detail"})); EXPECT_TRUE(config.references[0].warning.empty()); EXPECT_EQ(config.references[1].processorName, std::optional("core1")); + EXPECT_EQ(config.references[1].stream, std::optional(2U)); EXPECT_EQ(config.references[1].warning, (std::vector{"warning"})); EXPECT_FALSE(config.references[2].processorName.has_value()); + EXPECT_EQ(config.references[2].stream, std::optional(3U)); EXPECT_EQ(config.references[2].error, (std::vector{"unavailable"})); EXPECT_EQ(config.references[3].error, (std::vector{"diagnostic", "detail"})); EXPECT_FALSE(config.references[3].address.has_value()); + EXPECT_TRUE(config.references[3].addressError.has_value()); EXPECT_FALSE(config.references[3].dataSetupIndex.has_value()); - EXPECT_FALSE(config.references[4].stream.has_value()); - EXPECT_FALSE(config.references[4].dataSetupIndex.has_value()); - EXPECT_TRUE(config.references[5].error.empty()); - EXPECT_TRUE(config.references[6].sources == std::vector{0U}); - EXPECT_FALSE(config.references[7].label.has_value()); + EXPECT_TRUE(config.references[4].error.empty()); + EXPECT_TRUE(config.references[5].sources == std::vector{0U}); + EXPECT_FALSE(config.references[6].label.has_value()); + EXPECT_EQ(config.references[7].error, (std::vector{"exception-error"})); + EXPECT_EQ(config.references[7].stream, std::optional(4U)); + EXPECT_EQ(config.references[8].warning, (std::vector{"global-warning"})); + EXPECT_EQ(config.references[8].stream, std::optional(5U)); + EXPECT_EQ(config.references[9].info, (std::vector{"overflow-info"})); + EXPECT_EQ(config.references[9].stream, std::optional(6U)); +} + +TEST(CtraceUnitTests, TraceRunReaderRetainsSourceLessBindingsWithProducerErrors) +{ + TraceRunFixture file("ctrace-run-reader-source-less-bindings-test"); + const auto config = file.read(R"yml(ctrace-run: + trace-format: formatted + ctrace-refs: + - { type: itm, ctrace-ref: core/itm, pname: core, stream: 7, error: itm-error } + - { type: itm, ctrace-ref: core/timestamps, pname: core, stream: 7, error: normative-error } + - { type: dwt, ctrace-ref: core/timestamps, pname: core, stream: 7, error: transitional-error } + - { type: itm, ctrace-ref: core/itm, pname: core, stream: 7, source: [invalid], error: source-error } +)yml"); + + ASSERT_EQ(config.references.size(), 4U); + for (const auto& reference : config.references) { + EXPECT_EQ(reference.processorName, std::optional("core")); + EXPECT_EQ(reference.stream, std::optional(7U)); + EXPECT_TRUE(reference.sources.empty()); + ASSERT_EQ(reference.error.size(), 1U); + } + const auto meta = CtraceRunMeta::fromConfig(config); + ASSERT_EQ(meta.routes().size(), 1U); + EXPECT_TRUE(meta.routes().front().sources.empty()); + + auto unformatted = config; + unformatted.traceFormat.reset(); + unformatted.references = {config.references.back()}; + unformatted.references.front().type = "dwt"; + unformatted.references.front().ctraceRef = "core/data#0"; + unformatted.references.front().dataSetupIndex = 0U; + const auto singleMeta = CtraceRunMeta::fromConfig(unformatted); + EXPECT_EQ(singleMeta.routes().front().processorName, std::optional("core")); + EXPECT_TRUE(singleMeta.routes().front().sources.empty()); } TEST(CtraceUnitTests, TraceRunReaderParsesTimestampSetupVariants) @@ -450,12 +550,13 @@ TEST(CtraceUnitTests, TraceRunReaderTreatsNullOptionalSetupValuesAsAbsent) const auto meta = CtraceRunMeta::fromConfig(config); EXPECT_FALSE(meta.timestampClockHz().has_value()); - EXPECT_EQ(meta.timestampPrescaler(), - std::optional(TraceRunSchema::kDefaultTimestampPrescaler)); + EXPECT_EQ(meta.timestampPrescaler(), std::optional(TraceRunSchema::kDefaultTimestampPrescaler)); const auto generatedSetup = file.read(R"yml(ctrace-run: ctrace-setup: - null + - pname: itm-null-only + itm: null - pname: null-enable timestamps: null itm: @@ -484,18 +585,13 @@ TEST(CtraceUnitTests, TraceRunReaderRejectsMalformedConsumedSetups) constexpr Case cases[] = { {"timestamps: { itm-prescaler: [] }", "'timestamps.itm-prescaler' must be a scalar unsigned integer"}, {"timestamps: { itm-prescaler: invalid }", "'itm-prescaler' must be an unsigned integer in range"}, - {"itm: []", "'itm' must be a map containing 'enable'"}, - {"itm: { enable: [] }", "'itm.enable' must be a scalar unsigned integer"}, - {"itm: { enable: '' }", "'itm.enable' must be a scalar unsigned integer"}, - {"itm: { enable: invalid }", "'itm.enable' must be an unsigned integer in range"}, {"itm: { enable: 1, enable: 2 }", "map keys must be unique"}, }; for (const auto& testCase : cases) { expectReadError(file, std::string(prefix) + testCase.setup + "\n", testCase.error); } - expectReadError(file, "ctrace-run:\n ctrace-refs: []\n ctrace-setup: {}\n", - "'ctrace-setup' must be an array"); + expectReadError(file, "ctrace-run:\n ctrace-refs: []\n ctrace-setup: {}\n", "'ctrace-setup' must be an array"); expectReadError(file, "ctrace-run:\n ctrace-refs: []\n ctrace-setup: [invalid]\n", "each 'ctrace-setup' entry must be a map"); @@ -509,6 +605,44 @@ TEST(CtraceUnitTests, TraceRunReaderRejectsMalformedConsumedSetups) "'pname' must be a scalar string"); } +TEST(CtraceUnitTests, TraceRunReaderDefersMalformedItmSetupMetadata) +{ + TraceRunFixture file("ctrace-run-reader-deferred-itm-errors-test"); + constexpr std::string_view cases[] = { + "itm: []", + "itm: { enable: [] }", + "itm: { enable: '' }", + "itm: { enable: invalid }", + }; + for (const auto setup : cases) { + const auto yaml = std::string(R"yml(ctrace-run: + trace-format: formatted + ctrace-refs: + - { type: itm, ctrace-ref: core/itm, pname: core, stream: 1 } + ctrace-setup: + - pname: core + )yml") + std::string(setup) + + "\n"; + const auto config = file.read(yaml); + ASSERT_EQ(config.setups.size(), 1U) << setup; + ASSERT_TRUE(config.setups.front().itm.has_value()) << setup; + EXPECT_FALSE(config.setups.front().itm->enableMask.has_value()) << setup; + ASSERT_TRUE(config.setups.front().itm->enableError.has_value()) << setup; + + const auto formatted = CtraceRunMeta::fromConfig(config); + ASSERT_EQ(formatted.routes().size(), 1U) << setup; + EXPECT_FALSE(formatted.routes().front().itmEnableMask.has_value()) << setup; + EXPECT_TRUE(formatted.routes().front().itmEnableError.has_value()) << setup; + + auto unformattedConfig = config; + unformattedConfig.traceFormat.reset(); + const auto unformatted = CtraceRunMeta::fromConfig(unformattedConfig); + ASSERT_EQ(unformatted.routes().size(), 1U) << setup; + EXPECT_FALSE(unformatted.routes().front().itmEnableMask.has_value()) << setup; + EXPECT_TRUE(unformatted.routes().front().itmEnableError.has_value()) << setup; + } +} + TEST(CtraceUnitTests, TraceRunReaderParsesReferencedDataVariants) { TraceRunFixture file("ctrace-run-reader-data-variants-test"); @@ -541,15 +675,27 @@ TEST(CtraceUnitTests, TraceRunReaderParsesReferencedDataVariants) - { size: null } - data: [{}] )yml"); - ASSERT_EQ(config.setups.size(), 4U); + ASSERT_EQ(config.setups.size(), 5U); ASSERT_EQ(config.setups[0].data.size(), 7U); + EXPECT_TRUE(config.setups[0].data[0].present); + EXPECT_TRUE(config.setups[0].data[1].present); EXPECT_FALSE(config.setups[0].data[1].size.has_value()); + EXPECT_EQ(config.setups[0].data[1].sizeError, std::optional("each 'data' entry must be a map")); EXPECT_EQ(config.setups[0].data[2].sizeError, std::optional("'data.size' must be a scalar unsigned integer")); EXPECT_FALSE(config.setups[0].data[3].size.has_value()); EXPECT_TRUE(config.setups[0].data[4].sizeError.has_value()); EXPECT_EQ(config.setups[0].data[5].size, std::optional(2U)); - EXPECT_TRUE(config.setups[2].data[0].size == std::nullopt); + EXPECT_TRUE(config.setups[1].data.empty()); + EXPECT_EQ(config.setups[1].dataError, std::optional("'data' must be an array")); + EXPECT_EQ(config.setups[1].featurePaths, (std::vector{"core1/data"})); + ASSERT_EQ(config.setups[2].data.size(), 1U); + EXPECT_TRUE(config.setups[2].data[0].present); + EXPECT_FALSE(config.setups[2].dataError.has_value()); + ASSERT_EQ(config.setups[3].data.size(), 1U); + EXPECT_TRUE(config.setups[3].data[0].present); + ASSERT_EQ(config.setups[4].data.size(), 1U); + EXPECT_TRUE(config.setups[4].data[0].present); expectReadError(file, R"yml(ctrace-run: ctrace-refs: [{ type: dwt, ctrace-ref: data#0, source: 0 }] @@ -559,15 +705,205 @@ TEST(CtraceUnitTests, TraceRunReaderParsesReferencedDataVariants) "map keys must be unique"); } -TEST(CtraceUnitTests, TraceRunReaderSkipsDisabledSetup) +TEST(CtraceUnitTests, TraceRunReaderDefersMalformedDataContainerWithoutIndexSizedAllocation) +{ + TraceRunFixture file("ctrace-run-reader-deferred-data-container-error-test"); + const auto index = std::to_string(std::numeric_limits::max()); + const auto config = file.read(std::string(R"yml(ctrace-run: + ctrace-setup: + - pname: core + data: invalid + ctrace-refs: + - { type: dwt, ctrace-ref: core/data#)yml") + + index + ", pname: core, source: 0 }\n"); + + ASSERT_EQ(config.setups.size(), 1U); + EXPECT_TRUE(config.setups.front().data.empty()); + EXPECT_EQ(config.setups.front().dataError, std::optional("'data' must be an array")); + const auto meta = CtraceRunMeta::fromConfig(config); + ASSERT_EQ(meta.sources().size(), 1U); + EXPECT_EQ(meta.sources().front().dataSize, TraceRunSchema::kDefaultDwtDataSize); + EXPECT_EQ(meta.sources().front().dataSizeError, std::optional("'data' must be an array")); +} + +TEST(CtraceUnitTests, TraceRunReaderPropagatesMalformedDataEntryToSourceMetadata) +{ + TraceRunFixture file("ctrace-run-reader-deferred-data-entry-error-test"); + const auto config = file.read(R"yml(ctrace-run: + trace-format: formatted + ctrace-setup: + - pname: core + data: [invalid] + ctrace-refs: + - { type: itm, ctrace-ref: core/itm, pname: core, stream: 1 } + - { type: dwt, ctrace-ref: core/data#0, pname: core, stream: 1, source: 0 } +)yml"); + + ASSERT_EQ(config.setups.size(), 1U); + ASSERT_EQ(config.setups.front().data.size(), 1U); + EXPECT_EQ(config.setups.front().data.front().sizeError, + std::optional("each 'data' entry must be a map")); + const auto meta = CtraceRunMeta::fromConfig(config); + ASSERT_EQ(meta.sources().size(), 1U); + EXPECT_EQ(meta.sources().front().dataSize, TraceRunSchema::kDefaultDwtDataSize); + EXPECT_EQ(meta.sources().front().dataSizeError, std::optional("each 'data' entry must be a map")); +} + +TEST(CtraceUnitTests, TraceRunReaderDoesNotCreateSetupMetadataFromNullDataEntries) +{ + TraceRunFixture file("ctrace-run-reader-null-data-entry-test"); + const auto config = file.read(R"yml(ctrace-run: + trace-format: formatted + ctrace-setup: + - pname: core + data: [null] + ctrace-refs: + - { type: dwt, ctrace-ref: core/data#0, pname: core, stream: 1, source: 0 } +)yml"); + + EXPECT_TRUE(config.setups.empty()); + const auto meta = CtraceRunMeta::fromConfig(config); + ASSERT_EQ(meta.sources().size(), 1U); + EXPECT_EQ(meta.sources().front().dataSize, TraceRunSchema::kDefaultDwtDataSize); + EXPECT_FALSE(meta.sources().front().dataSizeError.has_value()); + + const auto otherProcessor = file.read(R"yml(ctrace-run: + ctrace-setup: + - pname: core + data: [null] + - pname: other + timestamps: null + ctrace-refs: + - { type: dwt, ctrace-ref: core/data#0, pname: core, source: 0 } +)yml"); + ASSERT_EQ(otherProcessor.setups.size(), 1U); + EXPECT_EQ(otherProcessor.setups.front().processorName, std::optional("other")); + const auto otherMeta = CtraceRunMeta::fromConfig(otherProcessor); + EXPECT_TRUE(otherMeta.sources().empty()); + EXPECT_EQ(otherMeta.routes().front().processorName, std::optional("other")); + + const auto malformedIndex = file.read(R"yml(ctrace-run: + ctrace-setup: + - data: [{ size: 2 }] + ctrace-refs: + - { type: dwt, ctrace-ref: data#invalid, source: 0 } +)yml"); + ASSERT_EQ(malformedIndex.references.size(), 1U); + EXPECT_FALSE(malformedIndex.references.front().dataSetupIndex.has_value()); + EXPECT_TRUE(malformedIndex.setups.empty()); + + const auto foreignProcessor = file.read(R"yml(ctrace-run: + ctrace-setup: + - pname: core + data: invalid-but-irrelevant + ctrace-refs: + - { type: dwt, ctrace-ref: other/data#0, source: 0 } +)yml"); + ASSERT_EQ(foreignProcessor.setups.size(), 1U); + EXPECT_FALSE(foreignProcessor.setups.front().dataError.has_value()); + const auto foreignMeta = CtraceRunMeta::fromConfig(foreignProcessor); + ASSERT_EQ(foreignMeta.sources().size(), 1U); + EXPECT_EQ(foreignMeta.sources().front().processorName, std::optional("other")); + EXPECT_EQ(foreignMeta.sources().front().dataSize, TraceRunSchema::kDefaultDwtDataSize); +} + +TEST(CtraceUnitTests, TraceRunReaderSkipsEmptyActiveSetupsAndNullDataEntries) +{ + TraceRunFixture file("ctrace-run-reader-empty-active-setup-test"); + const auto config = file.read(R"yml(ctrace-run: + ctrace-setup: + - {} + - pname: unused + - pname: [] + ignored-node: [] + - pname: unused-event + events: + - pname: core + data: [null, {}, null, { size: 2 }] + ctrace-refs: + - { type: dwt, ctrace-ref: core/data#0, pname: core, source: 2 } + - { type: dwt, ctrace-ref: core/data#1, pname: core, source: 0 } + - { type: dwt, ctrace-ref: core/data#3, pname: core, source: 1 } +)yml"); + + ASSERT_EQ(config.setups.size(), 1U); + const auto& setup = config.setups.front(); + EXPECT_EQ(setup.ordinal, 4U); + EXPECT_EQ(setup.processorName, std::optional("core")); + EXPECT_EQ(setup.featurePaths, (std::vector{"core/data#1", "core/data#3"})); + ASSERT_EQ(setup.data.size(), 4U); + EXPECT_FALSE(setup.data[0].present); + EXPECT_TRUE(setup.data[1].present); + EXPECT_FALSE(setup.data[2].present); + EXPECT_TRUE(setup.data[3].present); + EXPECT_FALSE(setup.data[1].size.has_value()); + EXPECT_EQ(setup.data[3].size, std::optional(2U)); +} + +TEST(CtraceUnitTests, TraceRunReaderPreservesDisabledSetupFragments) { TraceRunFixture file("ctrace-run-reader-disabled-setup-test"); - EXPECT_TRUE(file.read(R"yml(ctrace-run: + const auto config = file.read(R"yml(ctrace-run: ctrace-setup: - - disable: + - null + - pname: core + disable: false + timestamps: + clock: [] + timesync: + data: [{ size: [] }, null] + exceptions: + events: [{}, null, {}] + itm: invalid + pcsampling: + synchronization: + instructions: + tracehalt: + - pname: core timestamps: clock: 400000000 + - pname: [] + disable: [] + events: [] + - pname: null-disabled + disable: ctrace-refs: [] -)yml") - .setups.empty()); +)yml"); + + ASSERT_EQ(config.setups.size(), 4U); + const auto& disabled = config.setups[0]; + EXPECT_TRUE(disabled.disabled); + EXPECT_EQ(disabled.ordinal, 1U); + EXPECT_EQ(disabled.processorName, std::optional("core")); + EXPECT_FALSE(disabled.timestamps.has_value()); + EXPECT_FALSE(disabled.itm.has_value()); + EXPECT_TRUE(disabled.data.empty()); + EXPECT_EQ(disabled.featurePaths, (std::vector{ + "core/timestamps", + "core/timesync", + "core/data#0", + "core/exceptions", + "core/events#0", + "core/events#2", + "core/itm", + "core/pcsampling", + "core/synchronization", + "core/instructions", + "core/tracehalt", + })); + + EXPECT_FALSE(config.setups[1].disabled); + EXPECT_EQ(config.setups[1].ordinal, 2U); + EXPECT_EQ(config.setups[1].featurePaths, (std::vector{"core/timestamps"})); + ASSERT_TRUE(config.setups[1].timestamps.has_value()); + EXPECT_EQ(config.setups[1].timestamps->clockHz, std::optional(400000000U)); + + EXPECT_TRUE(config.setups[2].disabled); + EXPECT_EQ(config.setups[2].ordinal, 3U); + EXPECT_FALSE(config.setups[2].processorName.has_value()); + EXPECT_TRUE(config.setups[2].featurePaths.empty()); + + EXPECT_TRUE(config.setups[3].disabled); + EXPECT_EQ(config.setups[3].processorName, std::optional("null-disabled")); + EXPECT_TRUE(config.setups[3].featurePaths.empty()); } From 3d7fe51b2cd3783e5c2a1c3c5a5219b7760d1d6a Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Wed, 9 Sep 2026 20:35:39 +0200 Subject: [PATCH 03/31] feat(ctrace): resolve and preflight raw inputs --- .../ctrace/docs/multicore-multisource-plan.md | 4 +- tools/ctrace/src/control/FileDecodeJob.cpp | 78 ++-- tools/ctrace/src/control/FileDecodeJob.h | 20 +- .../ctrace/src/control/TraceDirectoryJob.cpp | 38 +- tools/ctrace/src/tracerun/CtraceRunMeta.cpp | 6 + tools/ctrace/src/tracerun/CtraceRunMeta.h | 16 + .../ctrace/src/tracerun/TraceRunDiscovery.cpp | 139 ++++++- tools/ctrace/src/tracerun/TraceRunDiscovery.h | 84 +++- .../test/integration/src/CtraceIntegTests.cpp | 47 +++ .../src/control/TraceDirectoryJobTests.cpp | 385 ++++++++++++++++-- .../src/tracerun/TraceRunDiscoveryTests.cpp | 212 +++++++++- 11 files changed, 886 insertions(+), 143 deletions(-) diff --git a/tools/ctrace/docs/multicore-multisource-plan.md b/tools/ctrace/docs/multicore-multisource-plan.md index 9061c3876..d98d0a94f 100644 --- a/tools/ctrace/docs/multicore-multisource-plan.md +++ b/tools/ctrace/docs/multicore-multisource-plan.md @@ -537,8 +537,8 @@ Phase 0 -> Phase 1 -> Phase 2 -> Phase 3 -> Phase 4 | :--- | :--- | :--- | | 0 | Baseline, fixtures, goldens, coverage gate | Complete | | 1 | Trace-run declaration and route normalization | Complete | -| 2 | Raw-input discovery and preflight | Next | -| 3 | Route-aware semantic state, diagnostics, and CSV | Pending | +| 2 | Raw-input discovery and preflight | Complete | +| 3 | Route-aware semantic state, diagnostics, and CSV | Next | | 4 | CTF descriptors and metadata model | Pending | | 5 | Multi-stream CTF bundle and Trace Compass policy | Pending | | 6 | DecodeTree `SINGLE` migration | Pending | diff --git a/tools/ctrace/src/control/FileDecodeJob.cpp b/tools/ctrace/src/control/FileDecodeJob.cpp index e897eda37..88df2d855 100644 --- a/tools/ctrace/src/control/FileDecodeJob.cpp +++ b/tools/ctrace/src/control/FileDecodeJob.cpp @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include @@ -44,21 +43,18 @@ class RawFileReader final { bool eof = false; }; - /** @brief Opens a raw trace input for binary reading. */ - explicit RawFileReader(std::filesystem::path path) + /** @brief Reads one already opened and preflighted raw trace input. */ + RawFileReader(std::filesystem::path path, std::istream& stream) : m_path(std::move(path)), - m_stream(m_path, std::ios::binary), + m_stream(stream), m_buffer(64U * 1024U) { - if (!m_stream) { - throw std::runtime_error("failed to open input file: " + m_path.string()); - } } /** @brief Returns the next raw byte chunk. */ ReadResult read() { - if (m_eof || !m_stream.is_open()) { + if (m_eof) { return {{}, true}; } @@ -67,22 +63,19 @@ class RawFileReader final { if (readBytes > 0) { if (m_stream.eof()) { m_eof = true; - m_stream.close(); } return {{m_buffer.data(), static_cast(readBytes)}, false}; } if (m_stream.bad()) { throw std::runtime_error("failed to read input file: " + m_path.string()); } - m_eof = true; - m_stream.close(); return {{}, true}; } private: std::filesystem::path m_path; - std::ifstream m_stream; + std::istream& m_stream; std::vector m_buffer; bool m_eof = false; }; @@ -103,11 +96,8 @@ static std::string decodeSummary(const DecodeResult& decode, std::chrono::steady /** @brief Extracts fallback and per-stream timestamp prescalers from metadata. */ static ItmTimestampPrescalers timestampPrescalers(const CtraceRunMeta& ctraceRunMeta) { - auto fallback = ctraceRunMeta.timestampPrescaler(); - if (!fallback.has_value() && !ctraceRunMeta.hasDistinctProcessorPrescalers()) { - fallback = TraceRunSchema::kDefaultTimestampPrescaler; - } - return {fallback, ctraceRunMeta.timestampPrescalersByTraceBusId()}; + return {ctraceRunMeta.timestampPrescaler().value_or(TraceRunSchema::kDefaultTimestampPrescaler), + ctraceRunMeta.timestampPrescalersByTraceBusId()}; } /** @brief Converts command-line output selection into an output request. */ @@ -139,29 +129,31 @@ static std::vector> createConfiguredOutputs(const T return outputs; } -FileDecodeJob::FileDecodeJob(CliOptions options, std::filesystem::path rawInputPath, DiagnosticSink& diagnostics, - CtraceRunMeta ctraceRunMeta) +FileDecodeJob::FileDecodeJob(CliOptions options, TraceRunInputDescriptor input, DiagnosticSink& diagnostics) : m_options(std::move(options)), - m_rawInputPath(std::move(rawInputPath)), - m_diagnostics(diagnostics), - m_ctraceRunMeta(std::move(ctraceRunMeta)) + m_input(std::move(input)), + m_diagnostics(diagnostics) { } -FileDecodeJob::FileDecodeJob(CliOptions options, std::filesystem::path rawInputPath, DiagnosticSink& diagnostics, - CtraceRunMeta ctraceRunMeta, OpenCsdItmSessionFactory sessionFactory) +FileDecodeJob::FileDecodeJob(CliOptions options, TraceRunInputDescriptor input, DiagnosticSink& diagnostics, + OpenCsdItmSessionFactory sessionFactory) : m_options(std::move(options)), - m_rawInputPath(std::move(rawInputPath)), + m_input(std::move(input)), m_diagnostics(diagnostics), - m_ctraceRunMeta(std::move(ctraceRunMeta)), m_sessionFactory(std::move(sessionFactory)) { } void FileDecodeJob::run() { - const auto prescalers = timestampPrescalers(m_ctraceRunMeta); - auto outputPlan = planTraceOutputs(outputRequest(m_options), m_rawInputPath, m_ctraceRunMeta, m_diagnostics); + if (m_input.format() == TraceRunFormat::Formatted) { + throw std::runtime_error("formatted trace input is not enabled yet"); + } + + const auto& ctraceRunMeta = m_input.metadata(); + const auto prescalers = timestampPrescalers(ctraceRunMeta); + auto outputPlan = planTraceOutputs(outputRequest(m_options), m_input.path(), ctraceRunMeta, m_diagnostics); if (outputPlan.hasRequestedOutputs() && !outputPlan.hasEnabledOutputs()) { return; } @@ -169,33 +161,25 @@ void FileDecodeJob::run() DiagnosticSink::Severity::Info, "applied ctrace-run meta", { - {"path", m_ctraceRunMeta.configPath()}, - {"processors", std::to_string(m_ctraceRunMeta.processorCount())}, - {"sources", std::to_string(m_ctraceRunMeta.sources().size())}, + {"path", ctraceRunMeta.configPath()}, + {"processors", std::to_string(ctraceRunMeta.processorCount())}, + {"sources", std::to_string(ctraceRunMeta.sources().size())}, }, }); auto outputs = createConfiguredOutputs(outputPlan, m_diagnostics); - DecodeConsumers consumers(std::move(outputs), m_diagnostics, m_ctraceRunMeta.itmEnableMask(), - m_ctraceRunMeta.itmEnableMasksByTraceBusId()); + DecodeConsumers consumers(std::move(outputs), m_diagnostics, ctraceRunMeta.itmEnableMask(), + ctraceRunMeta.itmEnableMasksByTraceBusId()); - if (prescalers.fallback.has_value()) { - m_diagnostics.report({ - DiagnosticSink::Severity::Info, - "using timestamp prescaler", - {{"value", std::to_string(*prescalers.fallback)}}, - }); - } else { - m_diagnostics.report({ - DiagnosticSink::Severity::Info, - "using Trace-Bus-ID-specific timestamp prescalers", - {{"traceBusIds", std::to_string(prescalers.byTraceBusId.size())}}, - }); - } + m_diagnostics.report({ + DiagnosticSink::Severity::Info, + "using timestamp prescaler", + {{"value", std::to_string(*prescalers.fallback)}}, + }); const auto decodeStart = std::chrono::steady_clock::now(); DecodeResult decode; bool decoderFatal = false; try { - RawFileReader input(m_rawInputPath); + RawFileReader input(m_input.path(), m_input.stream()); std::unique_ptr pipeline; if (m_sessionFactory) { pipeline = std::make_unique(prescalers, consumers, m_sessionFactory); diff --git a/tools/ctrace/src/control/FileDecodeJob.h b/tools/ctrace/src/control/FileDecodeJob.h index 64ef81227..81a1dee3a 100644 --- a/tools/ctrace/src/control/FileDecodeJob.h +++ b/tools/ctrace/src/control/FileDecodeJob.h @@ -11,9 +11,7 @@ #include "CliOptions.h" #include "DiagnosticSink.h" #include "OpenCsdItmDecoder.h" -#include "CtraceRunMeta.h" - -#include +#include "TraceRunDiscovery.h" /** @brief Decodes one raw trace file and owns its configured output lifecycle. */ class FileDecodeJob { @@ -21,22 +19,19 @@ class FileDecodeJob { /** * @brief Creates a file decode job using the production OpenCSD session. * @param options Validated command-line options. - * @param rawInputPath Raw SWO input to decode. + * @param input Selected and preflighted raw input with normalized metadata. * @param diagnostics Sink receiving operational diagnostics. - * @param ctraceRunMeta Normalized metadata for decoding and output. */ - FileDecodeJob(CliOptions options, std::filesystem::path rawInputPath, DiagnosticSink& diagnostics, - CtraceRunMeta ctraceRunMeta); + FileDecodeJob(CliOptions options, TraceRunInputDescriptor input, DiagnosticSink& diagnostics); /** * @brief Creates a file decode job with an injected OpenCSD session factory. * @param options Validated command-line options. - * @param rawInputPath Raw SWO input to decode. + * @param input Selected and preflighted raw input with normalized metadata. * @param diagnostics Sink receiving operational diagnostics. - * @param ctraceRunMeta Normalized metadata for decoding and output. * @param sessionFactory Factory used to create the decoder session. */ - FileDecodeJob(CliOptions options, std::filesystem::path rawInputPath, DiagnosticSink& diagnostics, - CtraceRunMeta ctraceRunMeta, OpenCsdItmSessionFactory sessionFactory); + FileDecodeJob(CliOptions options, TraceRunInputDescriptor input, DiagnosticSink& diagnostics, + OpenCsdItmSessionFactory sessionFactory); /** * @brief Runs decoding, reporting, and output completion for the input file. @@ -49,9 +44,8 @@ class FileDecodeJob { private: CliOptions m_options; - std::filesystem::path m_rawInputPath; + TraceRunInputDescriptor m_input; DiagnosticSink& m_diagnostics; - CtraceRunMeta m_ctraceRunMeta; OpenCsdItmSessionFactory m_sessionFactory; }; diff --git a/tools/ctrace/src/control/TraceDirectoryJob.cpp b/tools/ctrace/src/control/TraceDirectoryJob.cpp index 7fc2b3ad1..240ef12a1 100644 --- a/tools/ctrace/src/control/TraceDirectoryJob.cpp +++ b/tools/ctrace/src/control/TraceDirectoryJob.cpp @@ -116,41 +116,21 @@ void TraceDirectoryJob::run() }, }); reportConsumedReferenceDiagnostics(config, m_diagnostics); - const auto ctraceRunMeta = CtraceRunMeta::fromConfig(config); + auto ctraceRunMeta = CtraceRunMeta::fromConfig(config); reportTraceRunWarnings(ctraceRunMeta, m_diagnostics); - if (config.traceFormat == TraceRunFormat::Formatted) { - throw std::runtime_error("formatted trace input is not enabled yet"); - } - const auto rawInputs = TraceRunDiscovery::rawInputs(configFile); - bool processedSolutionSet = false; - for (const auto& rawInput : rawInputs) { - if (rawInput.channel != "SWO") { - m_diagnostics.report({ - DiagnosticSink::Severity::Warning, - "skipping raw trace channel that is not implemented yet", - { - {"solutionSet", solutionSet}, - {"channel", rawInput.channel}, - {"path", rawInput.path.string()}, - }, - }); - continue; - } - - FileDecodeJob fileJob(m_options, rawInput.path, m_diagnostics, ctraceRunMeta); - fileJob.run(); - processedSolutionSet = true; - } - if (!processedSolutionSet) { + auto input = TraceRunDiscovery::resolveInput(std::move(ctraceRunMeta), [&](const auto& rawInput) { m_diagnostics.report({ - DiagnosticSink::Severity::Error, - "no supported .SWO.raw input found", + DiagnosticSink::Severity::Warning, + "skipping raw trace channel that is not implemented yet", { {"solutionSet", solutionSet}, - {"traceDir", configFile.parent_path().string()}, + {"channel", rawInput.channel}, + {"path", rawInput.path.string()}, }, }); - } + }); + FileDecodeJob fileJob(m_options, std::move(input), m_diagnostics); + fileJob.run(); } catch (const std::exception& error) { m_diagnostics.report({ DiagnosticSink::Severity::Error, diff --git a/tools/ctrace/src/tracerun/CtraceRunMeta.cpp b/tools/ctrace/src/tracerun/CtraceRunMeta.cpp index aebfade87..38909537c 100644 --- a/tools/ctrace/src/tracerun/CtraceRunMeta.cpp +++ b/tools/ctrace/src/tracerun/CtraceRunMeta.cpp @@ -1105,6 +1105,7 @@ CtraceRunMeta CtraceRunMeta::fromConfig(const TraceRunConfig& config) { CtraceRunMeta ctraceRunMeta; ctraceRunMeta.m_configPath = config.path; + ctraceRunMeta.m_traceFormat = config.traceFormat; ctraceRunMeta.m_referenceDiagnostics = collectReferenceDiagnostics(config); validateDisabledReferences(config); @@ -1308,6 +1309,11 @@ const std::string& CtraceRunMeta::configPath() const return m_configPath; } +const std::optional& CtraceRunMeta::traceFormat() const +{ + return m_traceFormat; +} + const std::optional& CtraceRunMeta::timestampClockHz() const { return m_timestampClockHz; diff --git a/tools/ctrace/src/tracerun/CtraceRunMeta.h b/tools/ctrace/src/tracerun/CtraceRunMeta.h index 5e6bba0df..584efdbc7 100644 --- a/tools/ctrace/src/tracerun/CtraceRunMeta.h +++ b/tools/ctrace/src/tracerun/CtraceRunMeta.h @@ -17,6 +17,7 @@ #include struct TraceRunConfig; +enum class TraceRunFormat; /** @brief Stores normalized metadata for one trace source route. */ struct CtraceRunSourceMeta { @@ -85,6 +86,15 @@ struct CtraceRunRoute { /** @brief Provides validated trace-run metadata consumed by decoding and output. */ class CtraceRunMeta { public: + /** @brief Copies normalized trace-run metadata. */ + CtraceRunMeta(const CtraceRunMeta&) = default; + /** @brief Moves normalized trace-run metadata. */ + CtraceRunMeta(CtraceRunMeta&&) = default; + /** @brief Copies normalized trace-run metadata. */ + CtraceRunMeta& operator=(const CtraceRunMeta&) = default; + /** @brief Moves normalized trace-run metadata. */ + CtraceRunMeta& operator=(CtraceRunMeta&&) = default; + /** * @brief Normalizes a parsed trace-run configuration. * @param config Parsed trace-run configuration. @@ -97,6 +107,8 @@ class CtraceRunMeta { /** @brief Returns the source trace-run configuration path. */ const std::string& configPath() const; + /** @brief Returns the optional global byte-format declaration used during normalization. */ + const std::optional& traceFormat() const; /** @brief Returns the unambiguous timestamp clock, if available. */ const std::optional& timestampClockHz() const; /** @brief Returns timestamp metadata indexed by Trace Bus ID. */ @@ -125,7 +137,11 @@ class CtraceRunMeta { const std::vector& warnings() const; private: + /** @brief Restricts construction to normalized instances returned by fromConfig(). */ + CtraceRunMeta() = default; + std::string m_configPath; + std::optional m_traceFormat; std::optional m_timestampClockHz; std::map m_timestampsByTraceBusId; std::optional m_timestampPrescaler; diff --git a/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp b/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp index 30e903637..ba505de35 100644 --- a/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp +++ b/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -20,16 +21,91 @@ constexpr std::string_view ConfigSuffix = ".ctrace-run.yml"; +TraceRunInputDescriptor::TraceRunInputDescriptor(std::filesystem::path path, std::string channel, TraceRunFormat format, + bool formatDeclared, TraceRunInputFraming framing, + CtraceRunMeta metadata, std::ifstream stream) + : m_path(std::move(path)), + m_channel(std::move(channel)), + m_format(format), + m_formatDeclared(formatDeclared), + m_framing(framing), + m_metadata(std::move(metadata)), + m_stream(std::move(stream)) +{ +} + +const std::filesystem::path& TraceRunInputDescriptor::path() const noexcept +{ + return m_path; +} + +const std::string& TraceRunInputDescriptor::channel() const noexcept +{ + return m_channel; +} + +TraceRunFormat TraceRunInputDescriptor::format() const noexcept +{ + return m_format; +} + +bool TraceRunInputDescriptor::formatDeclared() const noexcept +{ + return m_formatDeclared; +} + +TraceRunInputFraming TraceRunInputDescriptor::framing() const noexcept +{ + return m_framing; +} + +const CtraceRunMeta& TraceRunInputDescriptor::metadata() const noexcept +{ + return m_metadata; +} + +std::istream& TraceRunInputDescriptor::stream() noexcept +{ + return m_stream; +} + /** @brief Tests whether a string ends in the supplied suffix. */ static bool endsWith(const std::string_view& value, const std::string_view& suffix) { return value.size() >= suffix.size() && value.substr(value.size() - suffix.size()) == suffix; } -/** @brief Tests whether a file extension names a supported trace channel. */ +/** @brief Tests whether a byte belongs to the CMSIS RestrictedString character set. */ +static bool isRestrictedStringCharacter(const char character) +{ + return (character >= 'A' && character <= 'Z') || (character >= 'a' && character <= 'z') || + (character >= '0' && character <= '9') || character == '_' || character == '-'; +} + +/** @brief Tests whether a channel names one specification-defined Trace Buffer. */ +static bool isTraceBufferChannel(const std::string_view& value) +{ + constexpr std::string_view namedPrefix = "TB_"; + if (value == "TB") { + return true; + } + if (value.size() <= namedPrefix.size() || value.substr(0U, namedPrefix.size()) != namedPrefix) { + return false; + } + const auto name = value.substr(namedPrefix.size()); + return std::all_of(name.begin(), name.end(), isRestrictedStringCharacter); +} + +/** @brief Tests whether a file extension names a recognized trace channel. */ static bool isTraceChannel(const std::string_view& value) { - return value == "SWO" || value == "TB" || value == "ER"; + return value == "SWO" || value == "ER" || isTraceBufferChannel(value); +} + +/** @brief Resolves input eligibility from declaration state without guessing from the channel. */ +static bool isEligibleTraceChannel(const std::string_view& value, bool formatDeclared) +{ + return value == "SWO" || (formatDeclared && isTraceBufferChannel(value)); } /** @brief Tests whether a solution-set name is reserved by Windows. */ @@ -126,9 +202,6 @@ std::vector TraceRunDiscovery::rawInputs(const std::filesystem std::vector inputs; for (const auto& entry : std::filesystem::directory_iterator(directory)) { - if (!entry.is_regular_file()) { - continue; - } const auto filename = entry.path().filename().string(); if (filename.rfind(prefix, 0) != 0 || !endsWith(filename, suffix)) { continue; @@ -147,3 +220,59 @@ std::vector TraceRunDiscovery::rawInputs(const std::filesystem [](const TraceRunRawInput& left, const TraceRunRawInput& right) { return left.path < right.path; }); return inputs; } + +TraceRunInputDescriptor TraceRunDiscovery::resolveInput(CtraceRunMeta metadata, + const SkippedTraceRunInputSink& skippedInputSink) +{ + if (metadata.configPath().empty()) { + throw std::runtime_error("normalized trace-run metadata has no configuration path"); + } + const std::filesystem::path configFile(metadata.configPath()); + const auto rawInputs = TraceRunDiscovery::rawInputs(configFile); + const auto& traceFormat = metadata.traceFormat(); + std::vector eligible; + for (const auto& rawInput : rawInputs) { + if (isEligibleTraceChannel(rawInput.channel, traceFormat.has_value())) { + eligible.push_back(&rawInput); + } else if (skippedInputSink) { + skippedInputSink(rawInput); + } + } + + const auto solutionSet = solutionSetName(configFile); + if (eligible.empty()) { + throw std::runtime_error("no eligible raw trace input found for solution-set " + solutionSet); + } + if (eligible.size() > 1U) { + std::string message = "multiple eligible raw trace inputs found for solution-set " + solutionSet + ":"; + for (const auto* rawInput : eligible) { + message += " " + rawInput->path.string(); + } + throw std::runtime_error(message); + } + + const auto& selected = *eligible.front(); + if (!std::filesystem::is_regular_file(selected.path)) { + throw std::runtime_error("raw trace input is not a regular file: " + selected.path.string()); + } + std::ifstream readable(selected.path, std::ios::binary | std::ios::ate); + if (!readable.is_open()) { + throw std::runtime_error("raw trace input is not readable: " + selected.path.string()); + } + + const auto format = TraceRunSchema::effectiveTraceFormat(traceFormat); + readable.exceptions(std::ios::badbit | std::ios::failbit); + const auto endPosition = readable.tellg(); + const auto fileSize = static_cast(static_cast(endPosition)); + if (format == TraceRunFormat::Formatted && fileSize % TraceRunInputContract::kMemoryAlignedFrameSize != 0U) { + throw std::runtime_error("formatted raw trace input size must be a multiple of " + + std::to_string(TraceRunInputContract::kMemoryAlignedFrameSize) + + " bytes: " + selected.path.string() + " (size=" + std::to_string(fileSize) + ")"); + } + + readable.seekg(0U, std::ios::beg); + readable.exceptions(std::ios::goodbit); + + return TraceRunInputDescriptor(selected.path, selected.channel, format, traceFormat.has_value(), + TraceRunInputFraming::MemoryAligned, std::move(metadata), std::move(readable)); +} diff --git a/tools/ctrace/src/tracerun/TraceRunDiscovery.h b/tools/ctrace/src/tracerun/TraceRunDiscovery.h index 25fe46c48..e22047786 100644 --- a/tools/ctrace/src/tracerun/TraceRunDiscovery.h +++ b/tools/ctrace/src/tracerun/TraceRunDiscovery.h @@ -8,7 +8,14 @@ #ifndef CTRACE_SRC_TRACERUN_TRACERUNDISCOVERY_H #define CTRACE_SRC_TRACERUN_TRACERUNDISCOVERY_H +#include "CtraceRunMeta.h" +#include "TraceRunConfig.h" + +#include #include +#include +#include +#include #include #include #include @@ -19,6 +26,70 @@ struct TraceRunRawInput { std::string channel; }; +/** @brief Identifies the internal framing contract for formatted raw input. */ +enum class TraceRunInputFraming { + MemoryAligned, +}; + +namespace TraceRunInputContract { + +/** @brief Size of one memory-aligned CoreSight formatter frame. */ +inline constexpr std::uintmax_t kMemoryAlignedFrameSize = 16U; + +} // namespace TraceRunInputContract + +class FileDecodeJob; +class TraceRunInputDescriptorTestAccess; + +/** @brief Owns one selected, preflighted raw input handle and its normalized metadata. */ +class TraceRunInputDescriptor { +public: + /** @brief Moves exclusive ownership of one preflighted input handle. */ + TraceRunInputDescriptor(TraceRunInputDescriptor&&) = default; + /** @brief Moves exclusive ownership of one preflighted input handle. */ + TraceRunInputDescriptor& operator=(TraceRunInputDescriptor&&) = default; + /** @brief Disables copying because the descriptor owns an open input handle. */ + TraceRunInputDescriptor(const TraceRunInputDescriptor&) = delete; + /** @brief Disables copy assignment because the descriptor owns an open input handle. */ + TraceRunInputDescriptor& operator=(const TraceRunInputDescriptor&) = delete; + + /** @brief Returns the selected raw-input path used for diagnostics and output naming. */ + const std::filesystem::path& path() const noexcept; + /** @brief Returns the selected trace channel. */ + const std::string& channel() const noexcept; + /** @brief Returns the effective global byte format. */ + TraceRunFormat format() const noexcept; + /** @brief Reports whether a non-null byte-format declaration was supplied. */ + bool formatDeclared() const noexcept; + /** @brief Returns the internal formatted-input framing contract. */ + TraceRunInputFraming framing() const noexcept; + /** @brief Returns the normalized trace-run metadata and routes. */ + const CtraceRunMeta& metadata() const noexcept; + +private: + friend class FileDecodeJob; + friend class TraceRunDiscovery; + friend class TraceRunInputDescriptorTestAccess; + + /** @brief Returns the preflighted stream positioned at the first input byte. */ + std::istream& stream() noexcept; + + /** @brief Creates one descriptor after successful selection and preflight. */ + TraceRunInputDescriptor(std::filesystem::path path, std::string channel, TraceRunFormat format, bool formatDeclared, + TraceRunInputFraming framing, CtraceRunMeta metadata, std::ifstream stream); + + std::filesystem::path m_path; + std::string m_channel; + TraceRunFormat m_format = TraceRunFormat::Unformatted; + bool m_formatDeclared = false; + TraceRunInputFraming m_framing = TraceRunInputFraming::MemoryAligned; + CtraceRunMeta m_metadata; + std::ifstream m_stream; +}; + +/** @brief Receives recognized raw inputs excluded from the active selection contract. */ +using SkippedTraceRunInputSink = std::function; + /** @brief Discovers trace-run configurations and their raw input files. */ class TraceRunDiscovery final { public: @@ -38,13 +109,18 @@ class TraceRunDiscovery final { */ static std::string solutionSetName(const std::filesystem::path& configFile); /** - * @brief Discovers the supported raw inputs associated with a trace-run file. - * @param configFile Trace-run configuration path. - * @return Deterministically ordered existing raw inputs and their channel names. + * @brief Discovers, selects, and preflights exactly one eligible raw input. + * @param metadata Normalized trace-run metadata, routes, format declaration, and source path. + * @param skippedInputSink Optional observer for recognized inputs excluded from selection. + * @return Fully normalized input descriptor safe to pass to a decode job. + * @throws std::runtime_error If the source path, selection, file access, or formatted alignment is invalid. */ - static std::vector rawInputs(const std::filesystem::path& configFile); + static TraceRunInputDescriptor resolveInput(CtraceRunMeta metadata, + const SkippedTraceRunInputSink& skippedInputSink = {}); private: + /** @brief Discovers recognized raw inputs associated with one trace-run file. */ + static std::vector rawInputs(const std::filesystem::path& configFile); /** @brief Prevents construction of this stateless discovery utility. */ TraceRunDiscovery() = delete; }; diff --git a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp index b388daaba..4eeab91c5 100644 --- a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp +++ b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp @@ -271,6 +271,53 @@ TEST_F(CtraceIntegTests, GeneratesAllOutputs) expectNonEmptyFile(workDirectory() / "Minimal.SWO.traceanalysis.xml"); } +TEST_F(CtraceIntegTests, DecodesExplicitUnformattedNamedTraceBuffer) +{ + writeFile(workDirectory() / "Named.ctrace-run.yml", R"yml(ctrace-run: + trace-format: unformatted + ctrace-setup: + - timestamps: + clock: 400000000 + ctrace-refs: [] +)yml"); + + const std::string raw{"\0\0\0\0\0\x80\x17\x34\x12\x00\x08\x09\x41", 13U}; + writeFile(workDirectory() / "Named.TB_MTB.raw", raw); + + const auto result = run({"ctrace", workDirectory().string(), "--target", "Named", "--all"}); + EXPECT_EQ(0, result.exitCode) << result.stderrText; + EXPECT_EQ("cycles,stream,type,source,value,pc,address,note\n" + "0,,pcsample,,,0x08001234,,\n" + "0,,itm,1,0x41,,,\n", + readTextFile(workDirectory() / "Named.TB_MTB.csv")); + expectNonEmptyFile(workDirectory() / "Named.ctf" / "metadata"); + expectNonEmptyFile(workDirectory() / "Named.ctf" / "stream_0"); + expectNonEmptyFile(workDirectory() / "Named.TB_MTB.traceanalysis.xml"); +} + +TEST_F(CtraceIntegTests, RejectsPartialFormattedFrameBeforeCreatingArtifacts) +{ + writeFile(workDirectory() / "Partial.ctrace-run.yml", R"yml(ctrace-run: + trace-format: formatted + ctrace-setup: + - pname: core + ctrace-refs: + - ctrace-ref: core/itm + type: itm + pname: core + stream: 1 +)yml"); + writeFile(workDirectory() / "Partial.TB.raw", std::string(15U, 'f')); + + const auto result = run({"ctrace", workDirectory().string(), "--target", "Partial", "--all"}); + EXPECT_EQ(1, result.exitCode); + expectContains(result.stderrText, "formatted raw trace input size must be a multiple of 16 bytes"); + expectNotContains(result.stderrText, "formatted trace input is not enabled yet"); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Partial.TB.csv")); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Partial.ctf")); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Partial.TB.traceanalysis.xml")); +} + TEST_F(CtraceIntegTests, ExpandsDwtEventCountersAcrossCsvAndCtf) { writeFile(workDirectory() / "Events.ctrace-run.yml", R"yml(ctrace-run: diff --git a/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp b/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp index f47c50b4b..347412819 100644 --- a/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp +++ b/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,16 @@ #include #include +/** @brief Provides controlled fault injection without exposing descriptor stream mutation in production. */ +class TraceRunInputDescriptorTestAccess final { +public: + /** @brief Marks the retained stream bad so the next read exercises stable error normalization. */ + static void setBad(TraceRunInputDescriptor& input) + { + input.stream().setstate(std::ios::badbit); + } +}; + /** @brief Creates the default trace-run configuration used by directory tests. */ static TraceRunConfig defaultTraceRunConfig() { @@ -38,6 +49,25 @@ static TraceRunConfig defaultTraceRunConfig() return config; } +/** @brief Creates one direct decode-job input for focused control tests. */ +static TraceRunInputDescriptor testInput(const std::filesystem::path& path, TraceRunConfig config = {}) +{ + const auto filename = path.filename().string(); + constexpr std::string_view rawSuffix = ".raw"; + if (filename.size() <= rawSuffix.size() || + filename.compare(filename.size() - rawSuffix.size(), rawSuffix.size(), rawSuffix) != 0) { + throw std::runtime_error("test input must be named ..raw"); + } + const auto channelSeparator = filename.rfind('.', filename.size() - rawSuffix.size() - 1U); + if (channelSeparator == std::string::npos) { + throw std::runtime_error("test input must be named ..raw"); + } + const auto solutionSet = filename.substr(0U, channelSeparator); + const auto configFile = path.parent_path() / (solutionSet + ".ctrace-run.yml"); + config.path = configFile.string(); + return TraceRunDiscovery::resolveInput(CtraceRunMeta::fromConfig(config)); +} + /** @brief Supplies deterministic trace-run configurations to directory-job tests. */ class TestTraceRunConfigReader final : public TraceRunConfigReader { public: @@ -98,6 +128,8 @@ TEST(CtraceUnitTests, testTraceDirectoryTargetAndOutputNames) const auto& root = temporaryPath.path(); const auto traceDir = root / ".trace"; writeTraceInputs(traceDir, {"Alpha", "Beta"}); + writeTestFile(traceDir / "Alpha.TB_MTB.raw", "unsupported"); + writeTestFile(traceDir / "Alpha.ER.raw", "unsupported"); CliOptions options; options.traceDir = traceDir.string(); @@ -122,6 +154,10 @@ TEST(CtraceUnitTests, testTraceDirectoryTargetAndOutputNames) << "TraceDirectoryJob XML output name mismatch"; ASSERT_TRUE(!std::filesystem::exists(traceDir / "Beta.SWO.csv")) << "TraceDirectoryJob should not process unselected target"; + EXPECT_TRUE(diagnostics.containsContext("channel", "TB_MTB")); + EXPECT_TRUE(diagnostics.containsContext("channel", "ER")); + EXPECT_FALSE(std::filesystem::exists(traceDir / "Alpha.TB_MTB.csv")); + EXPECT_FALSE(std::filesystem::exists(traceDir / "Alpha.ER.csv")); } TEST(CtraceUnitTests, testTraceDirectoryBatchCheckAndExplicitConfig) @@ -164,8 +200,8 @@ TEST(CtraceUnitTests, testTraceDirectoryRejectsFormattedInputBeforeRawFrontendAn { const TemporaryTestPath temporaryPath("ctrace-trace-directory-formatted-guard-test"); const auto traceDir = temporaryPath.path() / ".trace"; - writeTraceInputs(traceDir, {"Formatted"}); - writeTestFile(traceDir / "Formatted.TB.raw"); + writeTestFile(traceDir / "Formatted.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Formatted.SWO.raw", std::string(TraceRunInputContract::kMemoryAlignedFrameSize, 'f')); TraceRunConfig config; config.traceFormat = TraceRunFormat::Formatted; @@ -183,17 +219,199 @@ TEST(CtraceUnitTests, testTraceDirectoryRejectsFormattedInputBeforeRawFrontendAn EXPECT_TRUE(diagnostics.containsMessage("formatted trace input is not enabled yet")); EXPECT_FALSE(diagnostics.containsMessage("CTF output requires timestamps.clock")); EXPECT_FALSE(diagnostics.containsMessage("skipping raw trace channel")); + EXPECT_FALSE(diagnostics.containsMessage("formatted raw trace input size")); EXPECT_FALSE(std::filesystem::exists(traceDir / "Formatted.SWO.csv")); EXPECT_FALSE(std::filesystem::exists(traceDir / "Formatted.ctf")); EXPECT_FALSE(std::filesystem::exists(traceDir / "Formatted.SWO.traceanalysis.xml")); } +TEST(CtraceUnitTests, testTraceDirectoryPreflightsFormattedAlignmentBeforeGuardAndArtifacts) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-directory-formatted-preflight-test"); + const auto traceDir = temporaryPath.path() / ".trace"; + writeTestFile(traceDir / "Partial.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Partial.TB.raw", std::string(15U, 'f')); + writeTestFile(traceDir / "Partial.TB.csv", "csv sentinel"); + writeTestFile(traceDir / "Partial.ctf" / "sentinel", "ctf sentinel"); + writeTestFile(traceDir / "Partial.TB.traceanalysis.xml", "xml sentinel"); + + TraceRunConfig config; + config.traceFormat = TraceRunFormat::Formatted; + config.references.push_back(TraceRunTestSupport::makeReference("itm", "core", 1U, {}, "core/itm")); + + CliOptions options; + options.traceDir = traceDir.string(); + options.targetName = "Partial"; + options.outputFormat = OutputFormat::All; + + CollectingDiagnosticSink diagnostics; + TestTraceRunConfigReader reader(config); + TraceDirectoryJob(options, diagnostics, reader).run(); + + EXPECT_TRUE(diagnostics.containsMessage("formatted raw trace input size must be a multiple of 16 bytes")); + EXPECT_FALSE(diagnostics.containsMessage("formatted trace input is not enabled yet")); + EXPECT_FALSE(diagnostics.containsMessage("CTF output requires timestamps.clock")); + EXPECT_FALSE(diagnostics.containsMessage("applied ctrace-run meta")); + EXPECT_EQ(readTestTextFile(traceDir / "Partial.TB.csv"), "csv sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "Partial.ctf" / "sentinel"), "ctf sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "Partial.TB.traceanalysis.xml"), "xml sentinel"); +} + +TEST(CtraceUnitTests, testTraceDirectoryRejectsDirectoryInputBeforeArtifacts) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-directory-nonregular-input-test"); + const auto traceDir = temporaryPath.path() / ".trace"; + writeTestFile(traceDir / "Directory.ctrace-run.yml", "ctrace-run:\n"); + std::filesystem::create_directory(traceDir / "Directory.SWO.raw"); + writeTestFile(traceDir / "Directory.SWO.csv", "csv sentinel"); + writeTestFile(traceDir / "Directory.ctf" / "sentinel", "ctf sentinel"); + writeTestFile(traceDir / "Directory.SWO.traceanalysis.xml", "xml sentinel"); + + CliOptions options; + options.traceDir = traceDir.string(); + options.outputFormat = OutputFormat::All; + CollectingDiagnosticSink diagnostics; + TestTraceRunConfigReader reader; + TraceDirectoryJob(options, diagnostics, reader).run(); + + EXPECT_TRUE(diagnostics.containsMessage("raw trace input is not a regular file")); + EXPECT_FALSE(diagnostics.containsMessage("applied ctrace-run meta")); + EXPECT_EQ(readTestTextFile(traceDir / "Directory.SWO.csv"), "csv sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "Directory.ctf" / "sentinel"), "ctf sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "Directory.SWO.traceanalysis.xml"), "xml sentinel"); +} + +TEST(CtraceUnitTests, testTraceDirectoryRejectsDanglingSymlinkBeforeArtifacts) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-directory-dangling-input-test"); + const auto traceDir = temporaryPath.path() / ".trace"; + writeTestFile(traceDir / "Dangling.ctrace-run.yml", "ctrace-run:\n"); + std::error_code error; + std::filesystem::create_symlink("missing.raw", traceDir / "Dangling.SWO.raw", error); + if (error) { + GTEST_SKIP() << error.message(); + } + writeTestFile(traceDir / "Dangling.SWO.csv", "csv sentinel"); + writeTestFile(traceDir / "Dangling.ctf" / "sentinel", "ctf sentinel"); + writeTestFile(traceDir / "Dangling.SWO.traceanalysis.xml", "xml sentinel"); + + CliOptions options; + options.traceDir = traceDir.string(); + options.outputFormat = OutputFormat::All; + CollectingDiagnosticSink diagnostics; + TestTraceRunConfigReader reader; + TraceDirectoryJob(options, diagnostics, reader).run(); + + EXPECT_TRUE(diagnostics.containsMessage("raw trace input is not a regular file")); + EXPECT_FALSE(diagnostics.containsMessage("applied ctrace-run meta")); + EXPECT_EQ(readTestTextFile(traceDir / "Dangling.SWO.csv"), "csv sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "Dangling.ctf" / "sentinel"), "ctf sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "Dangling.SWO.traceanalysis.xml"), "xml sentinel"); +} + +TEST(CtraceUnitTests, testTraceDirectoryDecodesExplicitUnformattedTraceBuffersAndSkipsEventRecorder) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-directory-explicit-input-test"); + const auto traceDir = temporaryPath.path() / ".trace"; + writeTestFile(traceDir / "Plain.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Plain.TB.raw"); + writeTestFile(traceDir / "Plain.ER.raw", "unsupported"); + writeTestFile(traceDir / "Named.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Named.TB_MTB.raw"); + writeTestFile(traceDir / "Swo.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Swo.SWO.raw"); + + auto config = defaultTraceRunConfig(); + config.traceFormat = TraceRunFormat::Unformatted; + CliOptions options; + options.traceDir = traceDir.string(); + options.outputFormat = OutputFormat::Csv; + + CollectingDiagnosticSink diagnostics; + TestTraceRunConfigReader reader(config); + TraceDirectoryJob(options, diagnostics, reader).run(); + + EXPECT_EQ(diagnostics.failureCount(), 0U); + EXPECT_TRUE(diagnostics.containsMessage("skipping raw trace channel that is not implemented yet")); + EXPECT_TRUE(diagnostics.containsContext("channel", "ER")); + EXPECT_TRUE(std::filesystem::is_regular_file(traceDir / "Plain.TB.csv")); + EXPECT_TRUE(std::filesystem::is_regular_file(traceDir / "Named.TB_MTB.csv")); + EXPECT_TRUE(std::filesystem::is_regular_file(traceDir / "Swo.SWO.csv")); + EXPECT_FALSE(std::filesystem::exists(traceDir / "Plain.ER.csv")); +} + +TEST(CtraceUnitTests, testTraceDirectoryRejectsAmbiguousExplicitInputsWithoutTouchingArtifacts) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-directory-ambiguous-input-test"); + const auto traceDir = temporaryPath.path() / ".trace"; + writeTestFile(traceDir / "Ambiguous.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Ambiguous.SWO.raw"); + writeTestFile(traceDir / "Ambiguous.TB.raw"); + writeTestFile(traceDir / "Ambiguous.SWO.csv", "swo sentinel"); + writeTestFile(traceDir / "Ambiguous.TB.csv", "tb sentinel"); + writeTestFile(traceDir / "Ambiguous.ctf" / "sentinel", "ctf sentinel"); + writeTestFile(traceDir / "Ambiguous.SWO.traceanalysis.xml", "swo xml sentinel"); + writeTestFile(traceDir / "Ambiguous.TB.traceanalysis.xml", "tb xml sentinel"); + writeTestFile(traceDir / "TbNamed.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "TbNamed.TB.raw"); + writeTestFile(traceDir / "TbNamed.TB_MTB.raw"); + writeTestFile(traceDir / "TbNamed.TB.csv", "tb sentinel"); + writeTestFile(traceDir / "TbNamed.TB_MTB.csv", "named sentinel"); + writeTestFile(traceDir / "TbNamed.ctf" / "sentinel", "ctf sentinel"); + writeTestFile(traceDir / "TbNamed.TB.traceanalysis.xml", "tb xml sentinel"); + writeTestFile(traceDir / "TbNamed.TB_MTB.traceanalysis.xml", "named xml sentinel"); + writeTestFile(traceDir / "NamedPair.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "NamedPair.TB_MTB.raw"); + writeTestFile(traceDir / "NamedPair.TB_ETB.raw"); + writeTestFile(traceDir / "NamedPair.TB_MTB.csv", "mtb sentinel"); + writeTestFile(traceDir / "NamedPair.TB_ETB.csv", "etb sentinel"); + writeTestFile(traceDir / "NamedPair.ctf" / "sentinel", "ctf sentinel"); + writeTestFile(traceDir / "NamedPair.TB_MTB.traceanalysis.xml", "mtb xml sentinel"); + writeTestFile(traceDir / "NamedPair.TB_ETB.traceanalysis.xml", "etb xml sentinel"); + + auto config = defaultTraceRunConfig(); + config.traceFormat = TraceRunFormat::Unformatted; + CliOptions options; + options.traceDir = traceDir.string(); + options.outputFormat = OutputFormat::All; + + CollectingDiagnosticSink diagnostics; + TestTraceRunConfigReader reader(config); + TraceDirectoryJob(options, diagnostics, reader).run(); + + EXPECT_TRUE(diagnostics.containsMessage("multiple eligible raw trace inputs found")); + EXPECT_FALSE(diagnostics.containsMessage("applied ctrace-run meta")); + EXPECT_EQ(readTestTextFile(traceDir / "Ambiguous.SWO.csv"), "swo sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "Ambiguous.TB.csv"), "tb sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "Ambiguous.ctf" / "sentinel"), "ctf sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "Ambiguous.SWO.traceanalysis.xml"), "swo xml sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "Ambiguous.TB.traceanalysis.xml"), "tb xml sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "TbNamed.TB.csv"), "tb sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "TbNamed.TB_MTB.csv"), "named sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "TbNamed.ctf" / "sentinel"), "ctf sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "TbNamed.TB.traceanalysis.xml"), "tb xml sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "TbNamed.TB_MTB.traceanalysis.xml"), "named xml sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "NamedPair.TB_MTB.csv"), "mtb sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "NamedPair.TB_ETB.csv"), "etb sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "NamedPair.ctf" / "sentinel"), "ctf sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "NamedPair.TB_MTB.traceanalysis.xml"), "mtb xml sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "NamedPair.TB_ETB.traceanalysis.xml"), "etb xml sentinel"); + EXPECT_EQ(std::count_if(diagnostics.events().begin(), diagnostics.events().end(), + [](const auto& event) { + return event.message.find("multiple eligible raw trace inputs found") != std::string::npos; + }), + 3U); +} + TEST(CtraceUnitTests, testTraceDirectoryReportsGenerationDiagnosticsAndMissingSwo) { const TemporaryTestPath temporaryPath("ctrace-trace-directory-diagnostics-test"); const auto traceDir = temporaryPath.path() / ".trace"; writeTestFile(traceDir / "Alpha.ctrace-run.yml", "ctrace-run:\n"); - writeTestFile(traceDir / "Alpha.TB.raw", "unsupported"); + writeTestFile(traceDir / "Alpha.ER.raw", "unsupported"); + writeTestFile(traceDir / "Alpha.ER.csv", "csv sentinel"); + writeTestFile(traceDir / "Alpha.ctf" / "sentinel", "ctf sentinel"); + writeTestFile(traceDir / "Alpha.ER.traceanalysis.xml", "xml sentinel"); TraceRunConfig config; auto reported = TraceRunTestSupport::makeReference("event", "core", 3U, {}, "core/event"); @@ -219,6 +437,7 @@ TEST(CtraceUnitTests, testTraceDirectoryReportsGenerationDiagnosticsAndMissingSw CliOptions options; options.traceDir = traceDir.string(); + options.outputFormat = OutputFormat::All; CollectingDiagnosticSink diagnostics; TestTraceRunConfigReader reader(config); TraceDirectoryJob(options, diagnostics, reader).run(); @@ -233,7 +452,21 @@ TEST(CtraceUnitTests, testTraceDirectoryReportsGenerationDiagnosticsAndMissingSw EXPECT_TRUE(diagnostics.containsMessage("trace generation setup failed without a diagnostic message")); EXPECT_TRUE(diagnostics.containsMessage("does not match ctrace-setup pname")); EXPECT_TRUE(diagnostics.containsMessage("skipping raw trace channel")); - EXPECT_TRUE(diagnostics.containsMessage("no supported .SWO.raw input found")); + EXPECT_TRUE(diagnostics.containsContext("channel", "ER")); + EXPECT_TRUE(diagnostics.containsMessage("no eligible raw trace input found")); + + const auto skipped = std::find_if(diagnostics.events().begin(), diagnostics.events().end(), [](const auto& event) { + return event.message == "skipping raw trace channel that is not implemented yet"; + }); + const auto missing = std::find_if(diagnostics.events().begin(), diagnostics.events().end(), [](const auto& event) { + return event.message.find("no eligible raw trace input found") != std::string::npos; + }); + ASSERT_NE(skipped, diagnostics.events().end()); + ASSERT_NE(missing, diagnostics.events().end()); + EXPECT_LT(skipped, missing); + EXPECT_EQ(readTestTextFile(traceDir / "Alpha.ER.csv"), "csv sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "Alpha.ctf" / "sentinel"), "ctf sentinel"); + EXPECT_EQ(readTestTextFile(traceDir / "Alpha.ER.traceanalysis.xml"), "xml sentinel"); const auto* info = findDiagnostic(diagnostics, "producer note"); const auto* warning = findDiagnostic(diagnostics, "producer warning"); @@ -296,49 +529,98 @@ TEST(CtraceUnitTests, testTraceDirectoryReportsConfigFailureAndRequiresDirectory EXPECT_TRUE(diagnostics.containsMessage("synthetic config failure")); } -TEST(CtraceUnitTests, testFileDecodeJobHandlesMissingInputAndDisabledCtf) +TEST(CtraceUnitTests, testFileDecodeJobHandlesDisabledCtf) { const TemporaryTestPath temporaryPath("ctrace-file-decode-control-test"); CollectingDiagnosticSink diagnostics; - CliOptions checkOnly; - FileDecodeJob missing(checkOnly, temporaryPath.path() / "missing.raw", diagnostics, CtraceRunMeta::fromConfig({})); - EXPECT_THROW(missing.run(), std::runtime_error); - const auto rawPath = temporaryPath.path() / "empty.SWO.raw"; writeTestFile(rawPath); CliOptions ctf; ctf.outputFormat = OutputFormat::Ctf; - FileDecodeJob disabled(ctf, rawPath, diagnostics, CtraceRunMeta::fromConfig({})); + bool sessionCreated = false; + const auto script = std::make_shared(); + const auto delegate = OpenCsdSessionTestSupport::scriptedFactory(script); + OpenCsdItmSessionFactory factory = [&](OpenCsdPacketCollector& collector, OpenCsdErrorController& errors) { + sessionCreated = true; + return delegate(collector, errors); + }; + FileDecodeJob disabled(ctf, testInput(rawPath), diagnostics, std::move(factory)); EXPECT_NO_THROW(disabled.run()); EXPECT_TRUE(diagnostics.containsMessage("CTF output requires timestamps.clock")); + EXPECT_FALSE(sessionCreated); + EXPECT_FALSE(std::filesystem::exists(temporaryPath.path() / "empty.ctf")); } -TEST(CtraceUnitTests, testFileDecodeJobReportsPerStreamPrescalers) +TEST(CtraceUnitTests, testFileDecodeJobNeverConstructsDirectDecoderForFormattedInput) { - const TemporaryTestPath temporaryPath("ctrace-file-decode-prescalers-test"); - const auto rawPath = temporaryPath.path() / "empty.SWO.raw"; - writeTestFile(rawPath); + const TemporaryTestPath temporaryPath("ctrace-file-decode-formatted-guard-test"); + const auto rawPath = temporaryPath.path() / "formatted.TB.raw"; + writeTestFile(rawPath, std::string(TraceRunInputContract::kMemoryAlignedFrameSize, 'f')); + writeTestFile(temporaryPath.path() / "formatted.TB.csv", "csv sentinel"); + writeTestFile(temporaryPath.path() / "formatted.ctf" / "sentinel", "ctf sentinel"); + writeTestFile(temporaryPath.path() / "formatted.TB.traceanalysis.xml", "xml sentinel"); + + bool sessionCreated = false; + const auto script = std::make_shared(); + const auto delegate = OpenCsdSessionTestSupport::scriptedFactory(script); + OpenCsdItmSessionFactory factory = [&](OpenCsdPacketCollector& collector, OpenCsdErrorController& errors) { + sessionCreated = true; + return delegate(collector, errors); + }; + CliOptions options; + options.outputFormat = OutputFormat::All; TraceRunConfig config; config.traceFormat = TraceRunFormat::Formatted; - config.setups = { - TraceRunTestSupport::makeTimestampSetup("first", 100U, 4U), - TraceRunTestSupport::makeTimestampSetup("second", 100U, 16U), + config.references.push_back(TraceRunTestSupport::makeReference("itm", "core", 1U, {}, "core/itm")); + CollectingDiagnosticSink diagnostics; + FileDecodeJob job(options, testInput(rawPath, config), diagnostics, std::move(factory)); + + EXPECT_TRUE(throwsWithMessage([&] { job.run(); }, "formatted trace input is not enabled yet")); + EXPECT_FALSE(sessionCreated); + EXPECT_FALSE(diagnostics.containsMessage("CTF output requires timestamps.clock")); + EXPECT_EQ(readTestTextFile(temporaryPath.path() / "formatted.TB.csv"), "csv sentinel"); + EXPECT_EQ(readTestTextFile(temporaryPath.path() / "formatted.ctf" / "sentinel"), "ctf sentinel"); + EXPECT_EQ(readTestTextFile(temporaryPath.path() / "formatted.TB.traceanalysis.xml"), "xml sentinel"); +} + +TEST(CtraceUnitTests, testInputSelectionAndPreflightNeverConstructDecoder) +{ + const TemporaryTestPath temporaryPath("ctrace-input-before-decoder-test"); + const auto& root = temporaryPath.createDirectory(); + writeTestFile(root / "Ambiguous.SWO.raw"); + writeTestFile(root / "Ambiguous.TB.raw"); + writeTestFile(root / "Partial.TB.raw", std::string(15U, 'f')); + + bool sessionCreated = false; + const auto script = std::make_shared(); + const auto delegate = OpenCsdSessionTestSupport::scriptedFactory(script); + OpenCsdItmSessionFactory factory = [&](OpenCsdPacketCollector& collector, OpenCsdErrorController& errors) { + sessionCreated = true; + return delegate(collector, errors); }; - config.references = { - TraceRunTestSupport::makeReference("itm", "first", 1U, {1U}, "first/itm"), - TraceRunTestSupport::makeReference("itm", "second", 2U, {1U}, "second/itm"), + CollectingDiagnosticSink diagnostics; + CliOptions options; + options.outputFormat = OutputFormat::All; + const auto run = [&](const std::filesystem::path& configFile, TraceRunConfig config) { + config.path = configFile.string(); + auto input = TraceRunDiscovery::resolveInput(CtraceRunMeta::fromConfig(config)); + FileDecodeJob(options, std::move(input), diagnostics, factory).run(); }; - CollectingDiagnosticSink diagnostics; - FileDecodeJob job(CliOptions{}, rawPath, diagnostics, CtraceRunMeta::fromConfig(config)); - EXPECT_NO_THROW(job.run()); - const auto diagnostic = std::find_if(diagnostics.events().begin(), diagnostics.events().end(), [](const auto& event) { - return event.message == "using Trace-Bus-ID-specific timestamp prescalers"; - }); - ASSERT_NE(diagnostic, diagnostics.events().end()); - EXPECT_EQ(diagnostic->severity, DiagnosticSink::Severity::Info); - EXPECT_EQ(diagnostic->context, (std::vector>{{"traceBusIds", "2"}})); + EXPECT_TRUE(throwsWithMessage([&] { run(root / "Missing.ctrace-run.yml", {}); }, "no eligible raw trace input")); + TraceRunConfig unformatted; + unformatted.traceFormat = TraceRunFormat::Unformatted; + EXPECT_TRUE(throwsWithMessage([&] { run(root / "Ambiguous.ctrace-run.yml", unformatted); }, + "multiple eligible raw trace inputs")); + TraceRunConfig formatted; + formatted.traceFormat = TraceRunFormat::Formatted; + formatted.references.push_back(TraceRunTestSupport::makeReference("itm", "core", 1U, {}, "core/itm")); + EXPECT_TRUE(throwsWithMessage([&] { run(root / "Partial.ctrace-run.yml", formatted); }, "multiple of 16 bytes")); + EXPECT_FALSE(sessionCreated); + EXPECT_FALSE(std::filesystem::exists(root / "Missing.ctf")); + EXPECT_FALSE(std::filesystem::exists(root / "Ambiguous.ctf")); + EXPECT_FALSE(std::filesystem::exists(root / "Partial.ctf")); } TEST(CtraceUnitTests, testFileDecodeJobAbortsOutputsAfterFatalDecoderError) @@ -352,21 +634,50 @@ TEST(CtraceUnitTests, testFileDecodeJobAbortsOutputsAfterFatalDecoderError) const auto script = std::make_shared(); script->pushes = {{OCSD_RESP_FATAL_SYS_ERR, 1U}}; - FileDecodeJob job(options, rawPath, diagnostics, CtraceRunMeta::fromConfig({}), - OpenCsdSessionTestSupport::scriptedFactory(script)); + FileDecodeJob job(options, testInput(rawPath), diagnostics, OpenCsdSessionTestSupport::scriptedFactory(script)); EXPECT_NO_THROW(job.run()); EXPECT_GT(diagnostics.failureCount(), 0U); EXPECT_FALSE(std::filesystem::exists(temporaryPath.path() / "fatal.SWO.csv")); } -TEST(CtraceUnitTests, testFileDecodeJobReportsRawInputReadFailures) +TEST(CtraceUnitTests, testFileDecodeJobReportsRawInputReadFailuresWithPath) +{ + const TemporaryTestPath temporaryPath("ctrace-file-decode-read-failure-test"); + const auto rawPath = temporaryPath.path() / "failure.SWO.raw"; + writeTestFile(rawPath, "trace"); + auto input = testInput(rawPath); + TraceRunInputDescriptorTestAccess::setBad(input); + + CollectingDiagnosticSink diagnostics; + FileDecodeJob job(CliOptions{}, std::move(input), diagnostics); + const auto message = captureExceptionMessage([&] { job.run(); }); + ASSERT_TRUE(message.has_value()); + EXPECT_EQ(*message, "failed to read input file: " + rawPath.string()); +} + +TEST(CtraceUnitTests, testFileDecodeJobConsumesPreflightedHandleAfterPathReplacement) { - if (!TestPlatform::supports(TestPlatformCapability::DirectoryReadFailure)) { + if (!TestPlatform::supports(TestPlatformCapability::PosixPermissions)) { GTEST_SKIP(); } - const TemporaryTestPath temporaryPath("ctrace-file-decode-read-failure-test"); - temporaryPath.createDirectory(); + + const TemporaryTestPath temporaryPath("ctrace-file-decode-open-handle-test"); + const auto rawPath = temporaryPath.path() / "retained.SWO.raw"; + const std::string raw{"\0\0\0\0\0\x80\x17\x34\x12\x00\x08\x09\x41", 13U}; + writeTestFile(rawPath, raw); + auto input = testInput(rawPath); + + std::filesystem::rename(rawPath, temporaryPath.path() / "moved.raw"); + std::filesystem::create_directory(rawPath); + + CliOptions options; + options.outputFormat = OutputFormat::Csv; CollectingDiagnosticSink diagnostics; - FileDecodeJob job(CliOptions{}, temporaryPath.path(), diagnostics, CtraceRunMeta::fromConfig({})); - EXPECT_THROW(job.run(), std::runtime_error); + FileDecodeJob job(options, std::move(input), diagnostics); + EXPECT_NO_THROW(job.run()); + EXPECT_EQ(diagnostics.failureCount(), 0U); + EXPECT_EQ(readTestTextFile(temporaryPath.path() / "retained.SWO.csv"), + "cycles,stream,type,source,value,pc,address,note\n" + "0,,pcsample,,,0x08001234,,\n" + "0,,itm,1,0x41,,,\n"); } diff --git a/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp b/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp index c32329d25..3c7d281af 100644 --- a/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp +++ b/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp @@ -6,15 +6,43 @@ */ #include "TestPath.h" +#include "TestPlatform.h" #include "TestSupport.h" #include +#include "CtraceRunMeta.h" #include "TraceRunDiscovery.h" #include #include #include #include +#include #include +static_assert(!std::is_aggregate_v); +static_assert(!std::is_default_constructible_v); +static_assert(!std::is_copy_constructible_v); +static_assert(!std::is_default_constructible_v); +using ResolveInputSignature = TraceRunInputDescriptor (*)(CtraceRunMeta, const SkippedTraceRunInputSink&); +static_assert(std::is_same_v); + +/** @brief Creates normalized metadata carrying the requested declaration state. */ +static CtraceRunMeta inputMetadata(const std::filesystem::path& configFile, + const std::optional& traceFormat = std::nullopt) +{ + TraceRunConfig config; + config.path = configFile.string(); + config.traceFormat = traceFormat; + if (traceFormat == TraceRunFormat::Formatted) { + TraceRunReference route; + route.ctraceRef = "core/itm"; + route.type = "itm"; + route.processorName = "core"; + route.stream = 1U; + config.references.push_back(std::move(route)); + } + return CtraceRunMeta::fromConfig(config); +} + TEST(CtraceUnitTests, testTraceRunDiscovery) { const TemporaryTestPath temporaryPath("ctrace-trace-run-discovery-test"); @@ -23,13 +51,21 @@ TEST(CtraceUnitTests, testTraceRunDiscovery) writeTestFile(traceDir / "Alpha.ctrace-run.yml", "ctrace-run:\n"); writeTestFile(traceDir / "Alpha.SWO.raw"); writeTestFile(traceDir / "Alpha.TB.raw"); + writeTestFile(traceDir / "Alpha.TB_ETB-0.raw"); + writeTestFile(traceDir / "Alpha.TB_MTB.raw"); writeTestFile(traceDir / "Alpha.ER.raw"); writeTestFile(traceDir / "Alpha.swo.raw"); writeTestFile(traceDir / "Alpha.custom.raw"); writeTestFile(traceDir / "unrelated.raw"); writeTestFile(traceDir / "Alpha..raw"); + writeTestFile(traceDir / "Alpha.TB_.raw"); + writeTestFile(traceDir / "Alpha.TBish.raw"); + writeTestFile(traceDir / "Alpha.TB_bad.name.raw"); + writeTestFile(traceDir / "Alpha.TB_bad name.raw"); + writeTestFile(traceDir / "Alpha.TB_\xC3\x84.raw"); std::filesystem::create_directories(traceDir / "ignored.ctrace-run.yml"); std::filesystem::create_directories(traceDir / "Alpha.SWO.raw.dir"); + std::filesystem::create_directories(traceDir / "Alpha.TB_ETB_0.raw"); const auto batch = TraceRunDiscovery::selectConfigFiles(traceDir, std::nullopt); ASSERT_TRUE(batch.size() == 2U) << "TraceRunDiscovery batch configuration count mismatch"; @@ -41,11 +77,23 @@ TEST(CtraceUnitTests, testTraceRunDiscovery) ASSERT_TRUE(selected[0].filename() == "Alpha.ctrace-run.yml") << "TraceRunDiscovery target path mismatch"; ASSERT_TRUE(TraceRunDiscovery::solutionSetName(selected[0]) == "Alpha") << "TraceRunDiscovery solution-set mismatch"; - const auto rawInputs = TraceRunDiscovery::rawInputs(selected[0]); - ASSERT_TRUE(rawInputs.size() == 3U) << "TraceRunDiscovery must accept only the specified trace channels"; - ASSERT_TRUE(rawInputs[0].channel == "ER") << "TraceRunDiscovery ER channel mismatch"; - ASSERT_TRUE(rawInputs[1].channel == "SWO") << "TraceRunDiscovery SWO channel mismatch"; - ASSERT_TRUE(rawInputs[2].channel == "TB") << "TraceRunDiscovery TB channel mismatch"; + std::vector skippedChannels; + { + const auto legacy = TraceRunDiscovery::resolveInput( + inputMetadata(selected[0]), [&](const auto& input) { skippedChannels.push_back(input.channel); }); + EXPECT_EQ(legacy.path().filename(), "Alpha.SWO.raw"); + EXPECT_EQ(legacy.channel(), "SWO"); + } + EXPECT_EQ(skippedChannels, (std::vector{"ER", "TB", "TB_ETB-0", "TB_ETB_0", "TB_MTB"})); + + skippedChannels.clear(); + EXPECT_TRUE(throwsWithMessage( + [&] { + (void)TraceRunDiscovery::resolveInput(inputMetadata(selected[0], TraceRunFormat::Unformatted), + [&](const auto& input) { skippedChannels.push_back(input.channel); }); + }, + "multiple eligible raw trace inputs")); + EXPECT_EQ(skippedChannels, (std::vector{"ER"})); const std::vector unsafeTargets{ "", @@ -89,5 +137,157 @@ TEST(CtraceUnitTests, testTraceRunDiscovery) emptyPath.createDirectory(); writeTestFile(emptyPath.path() / ".ctrace-run.yml"); EXPECT_THROW((void)TraceRunDiscovery::selectConfigFiles(emptyPath.path(), std::nullopt), std::runtime_error); - EXPECT_TRUE(TraceRunDiscovery::rawInputs("parentless.ctrace-run.yml").empty()); +} + +TEST(CtraceUnitTests, testTraceRunDiscoveryResolvesOnePreflightedInput) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-run-input-resolution-test"); + const auto& root = temporaryPath.createDirectory(); + const auto legacyConfig = root / "Legacy.ctrace-run.yml"; + const auto swo = root / "Legacy.SWO.raw"; + writeTestFile(swo); + + { + auto legacy = TraceRunDiscovery::resolveInput(inputMetadata(legacyConfig)); + EXPECT_EQ(legacy.path(), swo); + EXPECT_EQ(legacy.channel(), "SWO"); + EXPECT_EQ(legacy.format(), TraceRunFormat::Unformatted); + EXPECT_FALSE(legacy.formatDeclared()); + EXPECT_EQ(legacy.framing(), TraceRunInputFraming::MemoryAligned); + EXPECT_FALSE(legacy.metadata().traceFormat().has_value()); + ASSERT_EQ(legacy.metadata().routes().size(), 1U); + EXPECT_FALSE(legacy.metadata().routes().front().traceBusId.has_value()); + } + + const auto explicitConfig = root / "Explicit.ctrace-run.yml"; + const auto tb = root / "Explicit.TB_MTB.raw"; + for (const auto size : {0U, 13U}) { + writeTestFile(tb, std::string(size, 'u')); + auto explicitUnformatted = + TraceRunDiscovery::resolveInput(inputMetadata(explicitConfig, TraceRunFormat::Unformatted)); + EXPECT_EQ(explicitUnformatted.path(), tb); + EXPECT_EQ(explicitUnformatted.channel(), "TB_MTB"); + EXPECT_EQ(explicitUnformatted.format(), TraceRunFormat::Unformatted); + EXPECT_TRUE(explicitUnformatted.formatDeclared()); + EXPECT_EQ(explicitUnformatted.metadata().traceFormat(), TraceRunFormat::Unformatted); + } + + const auto formattedConfig = root / "Formatted.ctrace-run.yml"; + const auto formatted = root / "Formatted.TB.raw"; + for (const auto size : {0U, 16U, 32U}) { + writeTestFile(formatted, std::string(size, 'f')); + auto descriptor = TraceRunDiscovery::resolveInput(inputMetadata(formattedConfig, TraceRunFormat::Formatted)); + EXPECT_EQ(descriptor.path(), formatted); + EXPECT_EQ(descriptor.channel(), "TB"); + EXPECT_EQ(descriptor.format(), TraceRunFormat::Formatted); + EXPECT_TRUE(descriptor.formatDeclared()); + EXPECT_EQ(descriptor.metadata().traceFormat(), TraceRunFormat::Formatted); + ASSERT_EQ(descriptor.metadata().routes().size(), 1U); + EXPECT_EQ(descriptor.metadata().routes().front().traceBusId, 1U); + } + for (const auto size : {1U, 15U, 17U, 31U}) { + writeTestFile(formatted, std::string(size, 'f')); + EXPECT_TRUE(throwsWithMessage( + [&] { (void)TraceRunDiscovery::resolveInput(inputMetadata(formattedConfig, TraceRunFormat::Formatted)); }, + "multiple of 16 bytes")); + } +} + +TEST(CtraceUnitTests, testTraceRunDiscoveryRejectsInvalidInputSelectionBeforePreflight) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-run-input-selection-test"); + const auto& root = temporaryPath.createDirectory(); + EXPECT_TRUE(throwsWithMessage([&] { (void)TraceRunDiscovery::resolveInput(CtraceRunMeta::fromConfig({})); }, + "metadata has no configuration path")); + + const auto missingConfig = root / "Missing.ctrace-run.yml"; + EXPECT_TRUE(throwsWithMessage([&] { (void)TraceRunDiscovery::resolveInput(inputMetadata(missingConfig)); }, + "no eligible raw trace input")); + + const auto eventRecorderConfig = root / "ErOnly.ctrace-run.yml"; + const auto eventRecorder = root / "ErOnly.ER.raw"; + writeTestFile(eventRecorder); + std::vector skippedChannels; + EXPECT_TRUE(throwsWithMessage( + [&] { + (void)TraceRunDiscovery::resolveInput(inputMetadata(eventRecorderConfig, TraceRunFormat::Formatted), + [&](const auto& input) { skippedChannels.push_back(input.channel); }); + }, + "no eligible raw trace input")); + EXPECT_EQ(skippedChannels, (std::vector{"ER"})); + + const auto swoTbConfig = root / "SwoTb.ctrace-run.yml"; + const auto directoryInput = root / "SwoTb.SWO.raw"; + std::filesystem::create_directory(directoryInput); + const auto regularInput = root / "SwoTb.TB.raw"; + writeTestFile(regularInput); + EXPECT_TRUE(throwsWithMessage( + [&] { (void)TraceRunDiscovery::resolveInput(inputMetadata(swoTbConfig, TraceRunFormat::Unformatted)); }, + "multiple eligible raw trace inputs")); + + const auto tbNamedConfig = root / "TbNamed.ctrace-run.yml"; + writeTestFile(root / "TbNamed.TB.raw"); + writeTestFile(root / "TbNamed.TB_MTB.raw"); + EXPECT_TRUE(throwsWithMessage( + [&] { (void)TraceRunDiscovery::resolveInput(inputMetadata(tbNamedConfig, TraceRunFormat::Unformatted)); }, + "multiple eligible raw trace inputs")); + + const auto namedPairConfig = root / "NamedPair.ctrace-run.yml"; + const auto namedInput = root / "NamedPair.TB_MTB.raw"; + const auto otherNamedInput = root / "NamedPair.TB_ETB.raw"; + writeTestFile(namedInput); + writeTestFile(otherNamedInput); + EXPECT_TRUE(throwsWithMessage( + [&] { (void)TraceRunDiscovery::resolveInput(inputMetadata(namedPairConfig, TraceRunFormat::Formatted)); }, + "multiple eligible raw trace inputs")); + + const auto nonRegularConfig = root / "NonRegular.ctrace-run.yml"; + const auto nonRegular = root / "NonRegular.SWO.raw"; + std::filesystem::create_directory(nonRegular); + EXPECT_TRUE(throwsWithMessage([&] { (void)TraceRunDiscovery::resolveInput(inputMetadata(nonRegularConfig)); }, + "not a regular file")); +} + +TEST(CtraceUnitTests, testTraceRunDiscoveryAcceptsRegularSymlinkAndRejectsDanglingSymlink) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-run-symlink-input-test"); + const auto& root = temporaryPath.createDirectory(); + writeTestFile(root / "target.raw", "trace"); + + std::error_code error; + const auto regularLink = root / "Linked.SWO.raw"; + std::filesystem::create_symlink("target.raw", regularLink, error); + if (error) { + GTEST_SKIP() << error.message(); + } + { + auto descriptor = TraceRunDiscovery::resolveInput(inputMetadata(root / "Linked.ctrace-run.yml")); + EXPECT_EQ(descriptor.path(), regularLink); + } + + const auto danglingLink = root / "Dangling.SWO.raw"; + std::filesystem::create_symlink("missing.raw", danglingLink); + EXPECT_TRUE( + throwsWithMessage([&] { (void)TraceRunDiscovery::resolveInput(inputMetadata(root / "Dangling.ctrace-run.yml")); }, + "not a regular file")); +} + +TEST(CtraceUnitTests, testTraceRunDiscoveryRejectsUnreadableInput) +{ + if (!TestPlatform::supports(TestPlatformCapability::PosixPermissions)) { + GTEST_SKIP(); + } + + const TemporaryTestPath temporaryPath("ctrace-trace-run-unreadable-input-test"); + const auto& root = temporaryPath.createDirectory(); + const auto configFile = root / "Unreadable.ctrace-run.yml"; + const auto rawInput = root / "Unreadable.SWO.raw"; + writeTestFile(rawInput, "trace"); + std::filesystem::permissions(rawInput, std::filesystem::perms::owner_write, std::filesystem::perm_options::replace); + const auto message = + captureExceptionMessage([&] { (void)TraceRunDiscovery::resolveInput(inputMetadata(configFile)); }); + std::filesystem::permissions(rawInput, std::filesystem::perms::owner_all, std::filesystem::perm_options::replace); + + ASSERT_TRUE(message.has_value()); + EXPECT_NE(message->find("not readable"), std::string::npos); } From b3f87f78b2d63696b73d96d933527e81bf5704a2 Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Thu, 10 Sep 2026 09:11:31 +0200 Subject: [PATCH 04/31] feat(ctrace): make semantic routes explicit --- .../ctrace/docs/multicore-multisource-plan.md | 4 +- tools/ctrace/src/CMakeLists.txt | 1 + tools/ctrace/src/control/DecodeConsumers.cpp | 23 +- tools/ctrace/src/control/DecodeConsumers.h | 7 +- tools/ctrace/src/control/FileDecodeJob.cpp | 31 ++- .../ctrace/src/decode/CortexMPostDecoder.cpp | 36 +-- tools/ctrace/src/decode/CortexMPostDecoder.h | 8 +- .../src/decode/CortexMStreamDecoder.cpp | 82 ++++--- .../ctrace/src/decode/CortexMStreamDecoder.h | 29 +-- tools/ctrace/src/decode/DecodePipeline.cpp | 12 +- tools/ctrace/src/decode/DecodePipeline.h | 11 +- tools/ctrace/src/decode/DwtPacketDecoder.cpp | 26 +- tools/ctrace/src/decode/DwtPacketDecoder.h | 5 +- tools/ctrace/src/decode/OpenCsdItmDecoder.cpp | 15 +- tools/ctrace/src/decode/OpenCsdItmDecoder.h | 6 +- .../src/decode/OpenCsdPacketCollector.cpp | 38 ++- .../src/decode/OpenCsdPacketCollector.h | 13 +- tools/ctrace/src/decode/OpenCsdTraceElement.h | 6 +- .../src/diagnostics/TraceIssueReporter.cpp | 48 ++-- .../src/diagnostics/TraceIssueReporter.h | 17 +- tools/ctrace/src/model/TraceEvent.h | 6 +- tools/ctrace/src/model/TraceRoute.h | 72 ++++++ tools/ctrace/src/model/TraceSelection.cpp | 9 +- tools/ctrace/src/model/TraceSelection.h | 3 + .../ctrace/src/output/OutputRequirements.cpp | 31 ++- tools/ctrace/src/output/TraceOutputConfig.h | 13 +- tools/ctrace/src/output/csv/CsvRowMapper.cpp | 4 +- .../ctrace/src/output/ctf/CtfBundleOutput.cpp | 2 + tools/ctrace/src/output/ctf/CtfEncoder.cpp | 228 ++++++++++++------ tools/ctrace/src/output/ctf/CtfEncoder.h | 29 ++- tools/ctrace/src/tracerun/CtraceRunMeta.cpp | 23 +- tools/ctrace/src/tracerun/CtraceRunMeta.h | 6 +- .../unit/src/control/DecodeConsumersTests.cpp | 31 ++- .../src/decode/CortexMPostDecoderTests.cpp | 15 +- .../unit/src/decode/DecodePipelineTests.cpp | 217 +++++++++++++++-- .../unit/src/decode/DwtPacketDecoderTests.cpp | 26 +- .../src/decode/OpenCsdItmDecoderTests.cpp | 10 +- .../decode/OpenCsdPacketCollectorTests.cpp | 45 +++- .../unit/src/diagnostics/DiagnosticsTests.cpp | 34 +++ .../unit/src/model/TraceSelectionTests.cpp | 27 ++- .../src/output/OutputRequirementsTests.cpp | 10 + .../src/output/csv/CsvFileOutputTests.cpp | 29 ++- .../unit/src/output/csv/CsvRowMapperTests.cpp | 21 ++ .../src/output/ctf/CtfBundleOutputTests.cpp | 38 ++- .../unit/src/output/ctf/CtfEncoderTests.cpp | 158 +++++++++++- .../src/output/ctf/CtfMetadataWriterTests.cpp | 17 +- .../unit/src/tracerun/CtraceRunMetaTests.cpp | 41 +++- .../tracerun/TraceRunConfigReaderTests.cpp | 6 +- .../src/tracerun/TraceRunDiscoveryTests.cpp | 4 +- .../unit/support/OpenCsdSessionTestSupport.h | 2 +- .../test/unit/support/OpenCsdTestSupport.h | 4 +- tools/ctrace/test/unit/support/TestSupport.h | 12 +- 52 files changed, 1195 insertions(+), 396 deletions(-) create mode 100644 tools/ctrace/src/model/TraceRoute.h diff --git a/tools/ctrace/docs/multicore-multisource-plan.md b/tools/ctrace/docs/multicore-multisource-plan.md index d98d0a94f..4f376cd5b 100644 --- a/tools/ctrace/docs/multicore-multisource-plan.md +++ b/tools/ctrace/docs/multicore-multisource-plan.md @@ -538,8 +538,8 @@ Phase 0 -> Phase 1 -> Phase 2 -> Phase 3 -> Phase 4 | 0 | Baseline, fixtures, goldens, coverage gate | Complete | | 1 | Trace-run declaration and route normalization | Complete | | 2 | Raw-input discovery and preflight | Complete | -| 3 | Route-aware semantic state, diagnostics, and CSV | Next | -| 4 | CTF descriptors and metadata model | Pending | +| 3 | Route-aware semantic state, diagnostics, and CSV | Complete | +| 4 | CTF descriptors and metadata model | Next | | 5 | Multi-stream CTF bundle and Trace Compass policy | Pending | | 6 | DecodeTree `SINGLE` migration | Pending | | 7 | Clean formatted decoding and TB integration | Pending | diff --git a/tools/ctrace/src/CMakeLists.txt b/tools/ctrace/src/CMakeLists.txt index 2c51aa3af..ffd42deea 100644 --- a/tools/ctrace/src/CMakeLists.txt +++ b/tools/ctrace/src/CMakeLists.txt @@ -4,6 +4,7 @@ set(CTRACE_MODEL_HEADER_FILES model/TraceEvent.h + model/TraceRoute.h model/TraceSelection.h model/TraceStreamId.h ) diff --git a/tools/ctrace/src/control/DecodeConsumers.cpp b/tools/ctrace/src/control/DecodeConsumers.cpp index 7fce924a3..aa7801f33 100644 --- a/tools/ctrace/src/control/DecodeConsumers.cpp +++ b/tools/ctrace/src/control/DecodeConsumers.cpp @@ -11,6 +11,7 @@ #include "TraceEvent.h" #include "TraceOutput.h" #include "TraceOutputLifecycle.h" +#include "TraceRoute.h" #include "TraceStreamId.h" #include @@ -34,10 +35,10 @@ static std::string hexMask(std::uint32_t value) DecodeConsumers::DecodeConsumers(std::vector> outputs, DiagnosticSink& diagnostics, std::optional itmEnableMask, - std::map itmEnableMasksByTraceBusId) + std::map itmEnableMasksByRoute) : m_diagnostics(diagnostics), m_itmEnableMask(itmEnableMask), - m_itmEnableMasksByTraceBusId(std::move(itmEnableMasksByTraceBusId)), + m_itmEnableMasksByRoute(std::move(itmEnableMasksByRoute)), m_issueReporter(diagnostics), m_outputLifecycle(std::move(outputs), diagnostics) { @@ -60,23 +61,25 @@ void DecodeConsumers::reportItmConfigurationMismatch(const TraceEvent& event) } auto enableMask = m_itmEnableMask; - const auto streamMask = m_itmEnableMasksByTraceBusId.find(event.traceBusId); - if (streamMask != m_itmEnableMasksByTraceBusId.end()) { + const auto streamMask = m_itmEnableMasksByRoute.find(event.route.id); + if (streamMask != m_itmEnableMasksByRoute.end()) { enableMask = streamMask->second; } if (!enableMask.has_value() || ((*enableMask & (1U << software->channel)) != 0U) || - !m_reportedDisabledItmChannels.emplace(event.traceBusId, software->channel).second) { + !m_reportedDisabledItmChannels.emplace(event.route.id, software->channel).second) { return; } + std::vector> context; + if (event.route.traceBusId.has_value()) { + context.emplace_back("stream", std::to_string(*event.route.traceBusId)); + } + context.emplace_back("channel", std::to_string(software->channel)); + context.emplace_back("enable", hexMask(*enableMask)); m_diagnostics.report({ DiagnosticSink::Severity::Warning, "ITM data was received on a channel not enabled by ctrace-setup.itm.enable", - { - {"stream", std::to_string(event.traceBusId)}, - {"channel", std::to_string(software->channel)}, - {"enable", hexMask(*enableMask)}, - }, + std::move(context), }); } diff --git a/tools/ctrace/src/control/DecodeConsumers.h b/tools/ctrace/src/control/DecodeConsumers.h index 23090828b..f0f2dd82a 100644 --- a/tools/ctrace/src/control/DecodeConsumers.h +++ b/tools/ctrace/src/control/DecodeConsumers.h @@ -13,6 +13,7 @@ #include "TraceIssueReporter.h" #include "TraceOutput.h" #include "TraceOutputLifecycle.h" +#include "TraceRoute.h" #include #include @@ -28,7 +29,7 @@ class DecodeConsumers final : public TraceEventSink { /** @brief Creates the consumers for one raw trace input. */ DecodeConsumers(std::vector> outputs, DiagnosticSink& diagnostics, std::optional itmEnableMask = std::nullopt, - std::map itmEnableMasksByTraceBusId = {}); + std::map itmEnableMasksByRoute = {}); /** @brief Forwards one decoded event to all configured consumers. */ void append(const TraceEvent& event) override; @@ -47,8 +48,8 @@ class DecodeConsumers final : public TraceEventSink { DiagnosticSink& m_diagnostics; std::optional m_itmEnableMask; - std::map m_itmEnableMasksByTraceBusId; - std::set> m_reportedDisabledItmChannels; + std::map m_itmEnableMasksByRoute; + std::set> m_reportedDisabledItmChannels; TraceIssueReporter m_issueReporter; TraceOutputLifecycle m_outputLifecycle; std::uint64_t m_eventCount = 0; diff --git a/tools/ctrace/src/control/FileDecodeJob.cpp b/tools/ctrace/src/control/FileDecodeJob.cpp index 88df2d855..e5772e092 100644 --- a/tools/ctrace/src/control/FileDecodeJob.cpp +++ b/tools/ctrace/src/control/FileDecodeJob.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -93,11 +94,23 @@ static std::string decodeSummary(const DecodeResult& decode, std::chrono::steady return out.str(); } -/** @brief Extracts fallback and per-stream timestamp prescalers from metadata. */ -static ItmTimestampPrescalers timestampPrescalers(const CtraceRunMeta& ctraceRunMeta) +/** @brief Resolves the one semantic route used by the current SINGLE frontend. */ +static CortexMDecodeRoute decodeRoute(const CtraceRunMeta& ctraceRunMeta) { - return {ctraceRunMeta.timestampPrescaler().value_or(TraceRunSchema::kDefaultTimestampPrescaler), - ctraceRunMeta.timestampPrescalersByTraceBusId()}; + const auto& route = ctraceRunMeta.routes().front(); + return {route.identity, route.timestampPrescaler}; +} + +/** @brief Indexes route-local ITM enable masks without using a transport sentinel. */ +static std::map itmEnableMasks(const CtraceRunMeta& ctraceRunMeta) +{ + std::map result; + for (const auto& route : ctraceRunMeta.routes()) { + if (route.itmEnableMask.has_value()) { + result.emplace(route.identity.id, *route.itmEnableMask); + } + } + return result; } /** @brief Converts command-line output selection into an output request. */ @@ -152,7 +165,7 @@ void FileDecodeJob::run() } const auto& ctraceRunMeta = m_input.metadata(); - const auto prescalers = timestampPrescalers(ctraceRunMeta); + const auto route = decodeRoute(ctraceRunMeta); auto outputPlan = planTraceOutputs(outputRequest(m_options), m_input.path(), ctraceRunMeta, m_diagnostics); if (outputPlan.hasRequestedOutputs() && !outputPlan.hasEnabledOutputs()) { return; @@ -168,12 +181,12 @@ void FileDecodeJob::run() }); auto outputs = createConfiguredOutputs(outputPlan, m_diagnostics); DecodeConsumers consumers(std::move(outputs), m_diagnostics, ctraceRunMeta.itmEnableMask(), - ctraceRunMeta.itmEnableMasksByTraceBusId()); + itmEnableMasks(ctraceRunMeta)); m_diagnostics.report({ DiagnosticSink::Severity::Info, "using timestamp prescaler", - {{"value", std::to_string(*prescalers.fallback)}}, + {{"value", std::to_string(route.timestampPrescaler)}}, }); const auto decodeStart = std::chrono::steady_clock::now(); DecodeResult decode; @@ -182,9 +195,9 @@ void FileDecodeJob::run() RawFileReader input(m_input.path(), m_input.stream()); std::unique_ptr pipeline; if (m_sessionFactory) { - pipeline = std::make_unique(prescalers, consumers, m_sessionFactory); + pipeline = std::make_unique(route, consumers, m_sessionFactory); } else { - pipeline = std::make_unique(prescalers, consumers); + pipeline = std::make_unique(route, consumers); } while (true) { const auto read = input.read(); diff --git a/tools/ctrace/src/decode/CortexMPostDecoder.cpp b/tools/ctrace/src/decode/CortexMPostDecoder.cpp index 5902fa9d2..f1e2f93f6 100644 --- a/tools/ctrace/src/decode/CortexMPostDecoder.cpp +++ b/tools/ctrace/src/decode/CortexMPostDecoder.cpp @@ -19,8 +19,9 @@ #include #include -CortexMPostDecoder::CortexMPostDecoder(TraceEventSink& eventSink) - : m_eventSink(eventSink) +CortexMPostDecoder::CortexMPostDecoder(TraceRouteIdentity route, TraceEventSink& eventSink) + : m_route(std::move(route)), + m_eventSink(eventSink) { } @@ -108,7 +109,7 @@ void CortexMPostDecoder::appendSync(const OpenCsdTraceElement& element) { TraceEvent event{SyncTraceEvent{}}; event.index = element.sourceIndex; - event.traceBusId = element.traceBusId; + event.route = m_route; queueOrEmitWhileAwaitingTimestamp(std::move(event)); } @@ -125,7 +126,7 @@ void CortexMPostDecoder::appendOverflow(const OpenCsdTraceElement& element) "overflow: new timestamp segment; time across boundary may be unreliable", }}; event.index = element.sourceIndex; - event.traceBusId = element.traceBusId; + event.route = m_route; event.tcyc = m_timelineKnown ? std::optional(m_currentTcyc) : std::nullopt; event.quality = TraceQuality{true, false, m_overflowCount}; emitEvent(event); @@ -139,7 +140,7 @@ void CortexMPostDecoder::appendGlobalTimestamp(const OpenCsdTraceElement& elemen element.clockChange, }}; event.index = element.sourceIndex; - event.traceBusId = element.traceBusId; + event.route = m_route; m_pendingEvents.push_back(std::move(event)); } @@ -147,11 +148,11 @@ void CortexMPostDecoder::appendDiscontinuity(const OpenCsdTraceElement& element) { const auto status = markDiscontinuity(); - queueDiscontinuityIssue( - element.sourceIndex, element.traceBusId, status, element.issueCode.value_or(TraceIssueCode::DataLoss), - element.errorMessage.empty() ? "data loss/resync boundary; timestamps across this point may not match" - : element.errorMessage, - element.rawBytesConsumed); + queueDiscontinuityIssue(element.sourceIndex, status, element.issueCode.value_or(TraceIssueCode::DataLoss), + element.errorMessage.empty() + ? "data loss/resync boundary; timestamps across this point may not match" + : element.errorMessage, + element.rawBytesConsumed); } void CortexMPostDecoder::appendError(const OpenCsdTraceElement& element) @@ -166,7 +167,7 @@ void CortexMPostDecoder::appendError(const OpenCsdTraceElement& element) std::nullopt, }}; event.index = element.sourceIndex; - event.traceBusId = element.traceBusId; + event.route = m_route; event.tcyc = m_currentTcyc; event.quality = status; if (element.awaitingResumeTimestamp) { @@ -186,7 +187,7 @@ void CortexMPostDecoder::appendSoftware(const OpenCsdTraceElement& element) element.value, }}; event.index = element.sourceIndex; - event.traceBusId = element.traceBusId; + event.route = m_route; event.tcyc = m_currentTcyc; event.quality = currentTraceStatus(element.overflow); m_pendingEvents.push_back(std::move(event)); @@ -196,7 +197,7 @@ void CortexMPostDecoder::appendDwt(const OpenCsdTraceElement& element) { auto events = m_dwtDecoder.decode({ element.sourceIndex, - element.traceBusId, + m_route, static_cast(element.discriminator), element.size, element.value, @@ -218,7 +219,7 @@ void CortexMPostDecoder::appendTimestamp(const OpenCsdTraceElement& element) TraceEvent event{LocalTimestampTraceEvent{}}; event.index = element.sourceIndex; - event.traceBusId = element.traceBusId; + event.route = m_route; event.tcyc = m_currentTcyc; emitEvent(event); @@ -254,9 +255,8 @@ void CortexMPostDecoder::appendPendingEvents(std::vector events) std::make_move_iterator(events.end())); } -void CortexMPostDecoder::queueDiscontinuityIssue(std::uint64_t sourceIndex, std::uint8_t traceBusId, - const TraceQuality& quality, TraceIssueCode issueCode, - const std::string& message, +void CortexMPostDecoder::queueDiscontinuityIssue(std::uint64_t sourceIndex, const TraceQuality& quality, + TraceIssueCode issueCode, const std::string& message, std::optional rawBytesConsumed) { TraceEvent event{TraceIssueEvent{ @@ -267,7 +267,7 @@ void CortexMPostDecoder::queueDiscontinuityIssue(std::uint64_t sourceIndex, std: m_currentTcyc, }}; event.index = sourceIndex; - event.traceBusId = traceBusId; + event.route = m_route; event.tcyc = m_currentTcyc; event.quality = quality; m_pendingEvents.push_back(std::move(event)); diff --git a/tools/ctrace/src/decode/CortexMPostDecoder.h b/tools/ctrace/src/decode/CortexMPostDecoder.h index 56cdc01e4..7a019a7a6 100644 --- a/tools/ctrace/src/decode/CortexMPostDecoder.h +++ b/tools/ctrace/src/decode/CortexMPostDecoder.h @@ -11,6 +11,7 @@ #include "OpenCsdTraceElement.h" #include "DwtPacketDecoder.h" #include "TraceEvent.h" +#include "TraceRoute.h" #include #include @@ -21,7 +22,7 @@ class CortexMPostDecoder final : public OpenCsdTraceElementSink { public: /** @brief Creates a post-decoder that emits to the supplied event sink. */ - explicit CortexMPostDecoder(TraceEventSink& eventSink); + CortexMPostDecoder(TraceRouteIdentity route, TraceEventSink& eventSink); /** @brief Appends one OpenCSD trace element. */ void append(OpenCsdTraceElement element) override; @@ -50,8 +51,8 @@ class CortexMPostDecoder final : public OpenCsdTraceElementSink { void appendTimestamp(const OpenCsdTraceElement& element); /** @brief Queues an issue whose final interval ends at the next reliable timestamp. */ - void queueDiscontinuityIssue(std::uint64_t sourceIndex, std::uint8_t traceBusId, const TraceQuality& quality, - TraceIssueCode issueCode, const std::string& message, + void queueDiscontinuityIssue(std::uint64_t sourceIndex, const TraceQuality& quality, TraceIssueCode issueCode, + const std::string& message, std::optional rawBytesConsumed = std::nullopt); /** @brief Finalizes queued discontinuity intervals at the first resumed timestamp. */ void finalizePendingDiscontinuityIssues(std::optional firstResumedTcyc); @@ -79,6 +80,7 @@ class CortexMPostDecoder final : public OpenCsdTraceElementSink { /** @brief Increments the saturated overflow counter. */ void noteOverflow(); + TraceRouteIdentity m_route; TraceEventSink& m_eventSink; std::uint64_t m_eventCount = 0; std::vector m_pendingEvents; diff --git a/tools/ctrace/src/decode/CortexMStreamDecoder.cpp b/tools/ctrace/src/decode/CortexMStreamDecoder.cpp index 07db9204e..9d7518574 100644 --- a/tools/ctrace/src/decode/CortexMStreamDecoder.cpp +++ b/tools/ctrace/src/decode/CortexMStreamDecoder.cpp @@ -11,69 +11,77 @@ #include "OpenCsdTraceElement.h" #include "SaturatingArithmetic.h" #include "TraceEvent.h" +#include "TraceRoute.h" +#include "TraceStreamId.h" #include #include +#include #include #include #include +#include -CortexMStreamDecoder::CortexMStreamDecoder(ItmTimestampPrescalers prescalers, TraceEventSink& eventSink) - : m_prescalers(std::move(prescalers)), - m_eventSink(eventSink) +CortexMStreamDecoder::CortexMStreamDecoder(std::vector routes, TraceEventSink& eventSink) { + if (routes.empty()) { + throw std::invalid_argument("Cortex-M stream decoding requires at least one normalized route"); + } + + std::set traceBusIds; + for (const auto& route : routes) { + if (route.timestampPrescaler == 0U) { + throw std::invalid_argument("ITM timestamp prescaler must be greater than zero"); + } + if (route.identity.traceBusId.has_value() && !CoreSight::isAtbTraceId(*route.identity.traceBusId)) { + throw std::invalid_argument("formatted Cortex-M route requires a CoreSight ATB trace ID between 1 and 111"); + } + if (route.identity.traceBusId.has_value() && !traceBusIds.insert(*route.identity.traceBusId).second) { + throw std::invalid_argument("duplicate CoreSight ATB trace ID in Cortex-M route configuration"); + } + RouteDecoder state; + state.identity = route.identity; + state.timestampPrescaler = route.timestampPrescaler; + state.decoder = std::make_unique(route.identity, eventSink); + if (!m_decoders.emplace(route.identity.id, std::move(state)).second) { + throw std::invalid_argument("duplicate normalized route ID in Cortex-M route configuration"); + } + } } CortexMStreamDecoder::~CortexMStreamDecoder() = default; void CortexMStreamDecoder::append(OpenCsdTraceElement element) { + const auto found = m_decoders.find(element.route.id); + if (found == m_decoders.end()) { + throw std::runtime_error("OpenCSD element references unknown normalized route " + + std::to_string(element.route.id.value())); + } + auto& route = found->second; + if (element.route != route.identity) { + throw std::runtime_error("OpenCSD element route identity does not match normalized route catalogue"); + } if (element.kind == OpenCsdTraceElement::Kind::LocalTimestamp && element.tcyc.has_value()) { - element.tcyc = SaturatingArithmetic::multiply(*element.tcyc, prescaler(element.traceBusId)); + element.tcyc = SaturatingArithmetic::multiply(*element.tcyc, route.timestampPrescaler); } - decoder(element.traceBusId).append(std::move(element)); + route.decoder->append(std::move(element)); } void CortexMStreamDecoder::finish() { - for (auto& [stream, decoder] : m_decoders) { - (void)stream; - decoder->finish(); + for (auto& [routeId, route] : m_decoders) { + (void)routeId; + route.decoder->finish(); } } std::uint64_t CortexMStreamDecoder::eventCount() const { std::uint64_t count = 0U; - for (const auto& [stream, decoder] : m_decoders) { - (void)stream; - count += decoder->eventCount(); + for (const auto& [routeId, route] : m_decoders) { + (void)routeId; + count += route.decoder->eventCount(); } return count; } - -std::uint32_t CortexMStreamDecoder::prescaler(std::uint8_t traceBusId) const -{ - const auto found = m_prescalers.byTraceBusId.find(traceBusId); - if (found != m_prescalers.byTraceBusId.end()) { - return found->second; - } - if (m_prescalers.fallback.has_value()) { - return *m_prescalers.fallback; - } - if (traceBusId == 0U) { - throw std::runtime_error("ITM timestamps with Trace Bus ID 0 cannot be assigned to " - "different processor prescalers; a formatted source ID is required"); - } - throw std::runtime_error("CoreSight Trace Bus ID " + std::to_string(traceBusId) + - " has no unambiguous processor timestamp prescaler"); -} - -CortexMPostDecoder& CortexMStreamDecoder::decoder(std::uint8_t traceBusId) -{ - auto& result = m_decoders[traceBusId]; - if (!result) { - result = std::make_unique(m_eventSink); - } - return *result; -} diff --git a/tools/ctrace/src/decode/CortexMStreamDecoder.h b/tools/ctrace/src/decode/CortexMStreamDecoder.h index 2a61ec5c7..c80bc0a32 100644 --- a/tools/ctrace/src/decode/CortexMStreamDecoder.h +++ b/tools/ctrace/src/decode/CortexMStreamDecoder.h @@ -10,25 +10,26 @@ #include "OpenCsdTraceElement.h" #include "TraceEvent.h" +#include "TraceRoute.h" #include #include #include -#include +#include class CortexMPostDecoder; -/** @brief Stores fallback and stream-specific ITM timestamp prescalers. */ -struct ItmTimestampPrescalers { - std::optional fallback; - std::map byTraceBusId; +/** @brief Configures semantic state and timestamp scaling for one normalized route. */ +struct CortexMDecodeRoute { + TraceRouteIdentity identity; + std::uint32_t timestampPrescaler = 1U; }; /** @brief Routes OpenCSD elements to per-stream Cortex-M post-decoders. */ class CortexMStreamDecoder final : public OpenCsdTraceElementSink { public: /** @brief Creates a stream router with timestamp scaling configuration. */ - CortexMStreamDecoder(ItmTimestampPrescalers prescalers, TraceEventSink& eventSink); + CortexMStreamDecoder(std::vector routes, TraceEventSink& eventSink); /** @brief Destroys all per-stream post-decoders. */ ~CortexMStreamDecoder(); @@ -45,14 +46,14 @@ class CortexMStreamDecoder final : public OpenCsdTraceElementSink { std::uint64_t eventCount() const; private: - /** @brief Resolves the configured timestamp prescaler for one Trace Bus ID. */ - std::uint32_t prescaler(std::uint8_t traceBusId) const; - /** @brief Returns or lazily creates the post-decoder for one Trace Bus ID. */ - CortexMPostDecoder& decoder(std::uint8_t traceBusId); - - ItmTimestampPrescalers m_prescalers; - TraceEventSink& m_eventSink; - std::map> m_decoders; + /** @brief Owns semantic state and timestamp scaling for one normalized route. */ + struct RouteDecoder { + TraceRouteIdentity identity; + std::uint32_t timestampPrescaler = 1U; + std::unique_ptr decoder; + }; + + std::map m_decoders; }; #endif // CTRACE_SRC_DECODE_CORTEXMSTREAMDECODER_H diff --git a/tools/ctrace/src/decode/DecodePipeline.cpp b/tools/ctrace/src/decode/DecodePipeline.cpp index 5ddb80f1e..076ce34f4 100644 --- a/tools/ctrace/src/decode/DecodePipeline.cpp +++ b/tools/ctrace/src/decode/DecodePipeline.cpp @@ -16,16 +16,16 @@ #include #include -DecodePipeline::DecodePipeline(ItmTimestampPrescalers timestampPrescalers, TraceEventSink& eventSink) - : m_streamDecoder(std::move(timestampPrescalers), eventSink), - m_decoder(m_streamDecoder) +DecodePipeline::DecodePipeline(CortexMDecodeRoute route, TraceEventSink& eventSink) + : m_streamDecoder({route}, eventSink), + m_decoder(std::move(route.identity), m_streamDecoder) { } -DecodePipeline::DecodePipeline(ItmTimestampPrescalers timestampPrescalers, TraceEventSink& eventSink, +DecodePipeline::DecodePipeline(CortexMDecodeRoute route, TraceEventSink& eventSink, const OpenCsdItmSessionFactory& sessionFactory) - : m_streamDecoder(std::move(timestampPrescalers), eventSink), - m_decoder(m_streamDecoder, sessionFactory) + : m_streamDecoder({route}, eventSink), + m_decoder(std::move(route.identity), m_streamDecoder, sessionFactory) { } diff --git a/tools/ctrace/src/decode/DecodePipeline.h b/tools/ctrace/src/decode/DecodePipeline.h index d0ee3680d..abb6575f3 100644 --- a/tools/ctrace/src/decode/DecodePipeline.h +++ b/tools/ctrace/src/decode/DecodePipeline.h @@ -37,19 +37,18 @@ struct DecodeResult { class DecodePipeline final { public: /** - * @brief Creates a pipeline with stream-specific timestamp prescalers. - * @param timestampPrescalers Default and per-stream timestamp prescalers. + * @brief Creates a pipeline bound to one normalized semantic route. + * @param route Route identity and timestamp prescaler used by the SINGLE decoder. * @param eventSink Sink receiving decoded events synchronously. */ - DecodePipeline(ItmTimestampPrescalers timestampPrescalers, TraceEventSink& eventSink); + DecodePipeline(CortexMDecodeRoute route, TraceEventSink& eventSink); /** * @brief Creates a pipeline with an injected OpenCSD session factory. - * @param timestampPrescalers Default and per-stream timestamp prescalers. + * @param route Route identity and timestamp prescaler used by the SINGLE decoder. * @param eventSink Sink receiving decoded events synchronously. * @param sessionFactory Factory used to create the OpenCSD session. */ - DecodePipeline(ItmTimestampPrescalers timestampPrescalers, TraceEventSink& eventSink, - const OpenCsdItmSessionFactory& sessionFactory); + DecodePipeline(CortexMDecodeRoute route, TraceEventSink& eventSink, const OpenCsdItmSessionFactory& sessionFactory); /** * @brief Pushes the next contiguous chunk of raw trace bytes. diff --git a/tools/ctrace/src/decode/DwtPacketDecoder.cpp b/tools/ctrace/src/decode/DwtPacketDecoder.cpp index 48cbe4928..8c8d9ea38 100644 --- a/tools/ctrace/src/decode/DwtPacketDecoder.cpp +++ b/tools/ctrace/src/decode/DwtPacketDecoder.cpp @@ -99,7 +99,7 @@ std::vector DwtPacketDecoder::decode(const DwtPayloadPacket& payload std::nullopt, }}; error.index = payload.index; - error.traceBusId = payload.traceBusId; + error.route = payload.route; error.tcyc = payload.tcyc; error.quality = payload.quality; output.push_back(std::move(error)); @@ -107,7 +107,7 @@ std::vector DwtPacketDecoder::decode(const DwtPayloadPacket& payload } TraceEvent packet{DwtEventTraceEvent{static_cast(payload.value)}}; packet.index = payload.index; - packet.traceBusId = payload.traceBusId; + packet.route = payload.route; packet.tcyc = payload.tcyc; packet.quality = payload.quality; output.push_back(std::move(packet)); @@ -126,7 +126,7 @@ std::vector DwtPacketDecoder::decode(const DwtPayloadPacket& payload std::nullopt, }}; error.index = payload.index; - error.traceBusId = payload.traceBusId; + error.route = payload.route; error.tcyc = payload.tcyc; error.quality = payload.quality; output.push_back(std::move(error)); @@ -134,7 +134,7 @@ std::vector DwtPacketDecoder::decode(const DwtPayloadPacket& payload } TraceEvent packet{PmuTraceEvent{static_cast(payload.value)}}; packet.index = payload.index; - packet.traceBusId = payload.traceBusId; + packet.route = payload.route; packet.tcyc = payload.tcyc; packet.quality = payload.quality; output.push_back(std::move(packet)); @@ -154,7 +154,7 @@ std::vector DwtPacketDecoder::decode(const DwtPayloadPacket& payload std::nullopt, }}; error.index = payload.index; - error.traceBusId = payload.traceBusId; + error.route = payload.route; error.tcyc = payload.tcyc; error.quality = payload.quality; output.push_back(std::move(error)); @@ -162,7 +162,7 @@ std::vector DwtPacketDecoder::decode(const DwtPayloadPacket& payload } TraceEvent packet{ExceptionTraceEvent{exceptionNumber, action}}; packet.index = payload.index; - packet.traceBusId = payload.traceBusId; + packet.route = payload.route; packet.tcyc = payload.tcyc; packet.quality = payload.quality; output.push_back(std::move(packet)); @@ -184,7 +184,7 @@ std::vector DwtPacketDecoder::decode(const DwtPayloadPacket& payload std::nullopt, }}; error.index = payload.index; - error.traceBusId = payload.traceBusId; + error.route = payload.route; error.tcyc = payload.tcyc; error.quality = payload.quality; output.push_back(std::move(error)); @@ -192,7 +192,7 @@ std::vector DwtPacketDecoder::decode(const DwtPayloadPacket& payload } TraceEvent packet{PcSampleTraceEvent{payload.value, isSleeping}}; packet.index = payload.index; - packet.traceBusId = payload.traceBusId; + packet.route = payload.route; packet.tcyc = payload.tcyc; packet.quality = payload.quality; output.push_back(std::move(packet)); @@ -246,7 +246,7 @@ void DwtPacketDecoder::decodeDataTrace(const DwtPayloadPacket& payload, std::vec PendingDataTrace event; event.index = payload.index; - event.traceBusId = payload.traceBusId; + event.route = payload.route; event.quality = payload.quality; if (packetType == DwtDataPacketType::Address) { @@ -258,7 +258,7 @@ void DwtPacketDecoder::decodeDataTrace(const DwtPayloadPacket& payload, std::vec } TraceEvent match{DwtMatchTraceEvent{comparator}}; match.index = payload.index; - match.traceBusId = payload.traceBusId; + match.route = payload.route; match.tcyc = payload.tcyc; match.quality = payload.quality; output.push_back(std::move(match)); @@ -278,7 +278,7 @@ void DwtPacketDecoder::decodeDataTrace(const DwtPayloadPacket& payload, std::vec std::nullopt, }}; error.index = payload.index; - error.traceBusId = payload.traceBusId; + error.route = payload.route; error.tcyc = payload.tcyc; error.quality = payload.quality; output.push_back(std::move(error)); @@ -321,7 +321,7 @@ void DwtPacketDecoder::sendDataTraceEvent(std::uint32_t comparator, const Pendin (pending->hasValue && event.hasValue); if (!repeatsFragmentKind) { pending->index = event.index; - pending->traceBusId = event.traceBusId; + pending->route = event.route; pending->pc = event.hasPc ? event.pc : pending->pc; pending->address = event.hasAddress ? event.address : pending->address; pending->value = event.hasValue ? event.value : pending->value; @@ -371,7 +371,7 @@ void DwtPacketDecoder::flushPending(std::uint32_t comparator, const TraceQuality TraceEvent packet = makePacket(); packet.index = pending->index; - packet.traceBusId = pending->traceBusId; + packet.route = pending->route; packet.tcyc = tcyc; packet.quality = quality; output.push_back(std::move(packet)); diff --git a/tools/ctrace/src/decode/DwtPacketDecoder.h b/tools/ctrace/src/decode/DwtPacketDecoder.h index 3ddf5bb5b..eb02bc608 100644 --- a/tools/ctrace/src/decode/DwtPacketDecoder.h +++ b/tools/ctrace/src/decode/DwtPacketDecoder.h @@ -9,6 +9,7 @@ #define CTRACE_SRC_DECODE_DWTPACKETDECODER_H #include "TraceEvent.h" +#include "TraceRoute.h" #include #include @@ -19,7 +20,7 @@ /** @brief Stores a decoded DWT hardware payload and its trace metadata. */ struct DwtPayloadPacket { std::uint64_t index = 0; - std::uint8_t traceBusId = 0U; + TraceRouteIdentity route; std::uint8_t discriminator = 0; std::uint8_t size = 0; std::uint32_t value = 0; @@ -43,7 +44,7 @@ class DwtPacketDecoder { /** @brief Accumulates the fragments of one pending DWT data-trace event. */ struct PendingDataTrace { std::uint64_t index = 0; - std::uint8_t traceBusId = 0U; + TraceRouteIdentity route; DwtAddressFragment pc; DwtAddressFragment address; std::uint32_t value = 0; diff --git a/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp b/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp index a1783db6f..8bdb01679 100644 --- a/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp +++ b/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp @@ -12,6 +12,7 @@ #include "OpenCsdPacketCollector.h" #include "OpenCsdItmSession.h" #include "OpenCsdTraceElement.h" +#include "TraceRoute.h" #include "opencsd/ocsd_if_types.h" #include @@ -20,6 +21,7 @@ #include #include #include +#include static_assert(sizeof(ocsd_trc_index_t) == sizeof(std::uint64_t), "ctrace requires 64-bit OpenCSD trace indices"); @@ -34,8 +36,9 @@ createDefaultOpenCsdItmSession(OpenCsdPacketCollector& collector, OpenCsdErrorCo class OpenCsdItmDecoderImpl { public: /** @brief Creates a decoder implementation around one session factory. */ - OpenCsdItmDecoderImpl(OpenCsdTraceElementSink& elementSink, const OpenCsdItmSessionFactory& sessionFactory) - : m_collector(elementSink) + OpenCsdItmDecoderImpl(TraceRouteIdentity route, OpenCsdTraceElementSink& elementSink, + const OpenCsdItmSessionFactory& sessionFactory) + : m_collector(std::move(route), elementSink) { try { m_session = sessionFactory(m_collector, m_errorController); @@ -324,14 +327,14 @@ class OpenCsdItmDecoderImpl { bool m_finished = false; }; -OpenCsdItmDecoder::OpenCsdItmDecoder(OpenCsdTraceElementSink& elementSink) - : m_impl(std::make_unique(elementSink, createDefaultOpenCsdItmSession)) +OpenCsdItmDecoder::OpenCsdItmDecoder(TraceRouteIdentity route, OpenCsdTraceElementSink& elementSink) + : m_impl(std::make_unique(std::move(route), elementSink, createDefaultOpenCsdItmSession)) { } -OpenCsdItmDecoder::OpenCsdItmDecoder(OpenCsdTraceElementSink& elementSink, +OpenCsdItmDecoder::OpenCsdItmDecoder(TraceRouteIdentity route, OpenCsdTraceElementSink& elementSink, const OpenCsdItmSessionFactory& sessionFactory) - : m_impl(std::make_unique(elementSink, sessionFactory)) + : m_impl(std::make_unique(std::move(route), elementSink, sessionFactory)) { } diff --git a/tools/ctrace/src/decode/OpenCsdItmDecoder.h b/tools/ctrace/src/decode/OpenCsdItmDecoder.h index 4ae654251..c0c91a668 100644 --- a/tools/ctrace/src/decode/OpenCsdItmDecoder.h +++ b/tools/ctrace/src/decode/OpenCsdItmDecoder.h @@ -9,6 +9,7 @@ #define CTRACE_SRC_DECODE_OPENCSDITMDECODER_H #include "OpenCsdTraceElement.h" +#include "TraceRoute.h" #include #include @@ -65,13 +66,14 @@ class OpenCsdItmDecoder { * @brief Creates a decoder using the production OpenCSD session. * @param elementSink Sink receiving decoded and recovery elements. */ - OpenCsdItmDecoder(OpenCsdTraceElementSink& elementSink); + OpenCsdItmDecoder(TraceRouteIdentity route, OpenCsdTraceElementSink& elementSink); /** * @brief Creates a decoder with an injected OpenCSD session factory. * @param elementSink Sink receiving decoded and recovery elements. * @param sessionFactory Factory used to construct the external session. */ - OpenCsdItmDecoder(OpenCsdTraceElementSink& elementSink, const OpenCsdItmSessionFactory& sessionFactory); + OpenCsdItmDecoder(TraceRouteIdentity route, OpenCsdTraceElementSink& elementSink, + const OpenCsdItmSessionFactory& sessionFactory); /** @brief Destroys the decoder implementation and external session. */ ~OpenCsdItmDecoder(); diff --git a/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp b/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp index d17f3a052..201dc7e39 100644 --- a/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp +++ b/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp @@ -8,8 +8,8 @@ #include "OpenCsdPacketCollector.h" #include "TraceEvent.h" -#include "TraceStreamId.h" #include "OpenCsdTraceElement.h" +#include "TraceRoute.h" #include "common/trc_gen_elem.h" #include "opencsd/itm/trc_pkt_elem_itm.h" #include "opencsd/itm/trc_pkt_types_itm.h" @@ -24,8 +24,9 @@ #include #include -OpenCsdPacketCollector::OpenCsdPacketCollector(OpenCsdTraceElementSink& elementSink) - : m_elementSink(elementSink) +OpenCsdPacketCollector::OpenCsdPacketCollector(TraceRouteIdentity route, OpenCsdTraceElementSink& elementSink) + : m_route(std::move(route)), + m_elementSink(elementSink) { } @@ -114,6 +115,7 @@ void OpenCsdPacketCollector::prependDiscontinuity(ocsd_trc_index_t index, const element.issueCode = issueCode; element.errorMessage = message; element.rawBytesConsumed = rawBytesConsumed; + element.route = m_route; if (m_transactionActive) { m_transactionElements.insert(m_transactionElements.begin(), std::move(element)); return; @@ -131,6 +133,7 @@ void OpenCsdPacketCollector::prependDataLossError(ocsd_trc_index_t index, const element.errorMessage = message; element.rawBytesConsumed = rawBytesConsumed; element.awaitingResumeTimestamp = true; + element.route = m_route; if (m_transactionActive) { m_transactionElements.insert(m_transactionElements.begin(), std::move(element)); return; @@ -138,8 +141,8 @@ void OpenCsdPacketCollector::prependDataLossError(ocsd_trc_index_t index, const appendElement(std::move(element)); } -ocsd_datapath_resp_t OpenCsdPacketCollector::TraceElemIn(const ocsd_trc_index_t index_sop, - const std::uint8_t trc_chan_id, const OcsdTraceElement& elem) +ocsd_datapath_resp_t OpenCsdPacketCollector::TraceElemIn(const ocsd_trc_index_t index_sop, const std::uint8_t, + const OcsdTraceElement& elem) { try { if (elem.getType() != OCSD_GEN_TRC_ELEM_ITMTRACE) { @@ -147,22 +150,21 @@ ocsd_datapath_resp_t OpenCsdPacketCollector::TraceElemIn(const ocsd_trc_index_t } const auto& info = elem.swt_itm; - const auto traceBusId = CoreSight::isTraceBusId(trc_chan_id) ? trc_chan_id : CoreSight::kUnformattedTraceBusId; switch (info.pkt_type) { case SWIT_PAYLOAD: - appendSoftware(index_sop, traceBusId, elem); + appendSoftware(index_sop, elem); break; case DWT_PAYLOAD: - appendDwt(index_sop, traceBusId, elem); + appendDwt(index_sop, elem); break; case TS_SYNC: case TS_DELAY: case TS_PKT_DELAY: case TS_PKT_TS_DELAY: - appendTimestamp(index_sop, traceBusId, elem); + appendTimestamp(index_sop, elem); break; case TS_GLOBAL: - appendGlobalTimestamp(index_sop, traceBusId, elem); + appendGlobalTimestamp(index_sop, elem); break; } } catch (...) { @@ -236,13 +238,11 @@ void OpenCsdPacketCollector::appendOverflow(ocsd_trc_index_t index) appendElement(std::move(element)); } -void OpenCsdPacketCollector::appendGlobalTimestamp(ocsd_trc_index_t index, std::uint8_t traceBusId, - const OcsdTraceElement& elem) +void OpenCsdPacketCollector::appendGlobalTimestamp(ocsd_trc_index_t index, const OcsdTraceElement& elem) { OpenCsdTraceElement element; element.kind = OpenCsdTraceElement::Kind::GlobalTimestamp; element.sourceIndex = static_cast(index); - element.traceBusId = traceBusId; element.timestampValue = elem.timestamp; element.clockChange = elem.cpu_freq_change != 0U; appendElement(std::move(element)); @@ -258,14 +258,12 @@ void OpenCsdPacketCollector::appendError(ocsd_trc_index_t index, const ItmTrcPac appendElement(std::move(element)); } -void OpenCsdPacketCollector::appendSoftware(ocsd_trc_index_t index, std::uint8_t traceBusId, - const OcsdTraceElement& elem) +void OpenCsdPacketCollector::appendSoftware(ocsd_trc_index_t index, const OcsdTraceElement& elem) { const auto& info = elem.swt_itm; OpenCsdTraceElement element; element.kind = OpenCsdTraceElement::Kind::Software; element.sourceIndex = static_cast(index); - element.traceBusId = traceBusId; element.channel = info.payload_src_id; element.size = info.payload_size; element.value = info.value; @@ -273,13 +271,12 @@ void OpenCsdPacketCollector::appendSoftware(ocsd_trc_index_t index, std::uint8_t appendElement(std::move(element)); } -void OpenCsdPacketCollector::appendDwt(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem) +void OpenCsdPacketCollector::appendDwt(ocsd_trc_index_t index, const OcsdTraceElement& elem) { const auto& info = elem.swt_itm; OpenCsdTraceElement element; element.kind = OpenCsdTraceElement::Kind::Hardware; element.sourceIndex = static_cast(index); - element.traceBusId = traceBusId; element.discriminator = info.payload_src_id; element.size = info.payload_size; element.value = info.value; @@ -287,14 +284,12 @@ void OpenCsdPacketCollector::appendDwt(ocsd_trc_index_t index, std::uint8_t trac appendElement(std::move(element)); } -void OpenCsdPacketCollector::appendTimestamp(ocsd_trc_index_t index, std::uint8_t traceBusId, - const OcsdTraceElement& elem) +void OpenCsdPacketCollector::appendTimestamp(ocsd_trc_index_t index, const OcsdTraceElement& elem) { const auto& info = elem.swt_itm; OpenCsdTraceElement element; element.kind = OpenCsdTraceElement::Kind::LocalTimestamp; element.sourceIndex = static_cast(index); - element.traceBusId = traceBusId; element.timestampRelation = timestampRelation(info.pkt_type); element.tcyc = elem.timestamp; element.overflow = info.overflow; @@ -317,6 +312,7 @@ LocalTimestampRelation OpenCsdPacketCollector::timestampRelation(swt_itm_type ty void OpenCsdPacketCollector::appendElement(OpenCsdTraceElement element) { + element.route = m_route; if (m_transactionActive) { m_transactionElements.push_back(std::move(element)); return; diff --git a/tools/ctrace/src/decode/OpenCsdPacketCollector.h b/tools/ctrace/src/decode/OpenCsdPacketCollector.h index d4e1b63e3..936161072 100644 --- a/tools/ctrace/src/decode/OpenCsdPacketCollector.h +++ b/tools/ctrace/src/decode/OpenCsdPacketCollector.h @@ -10,6 +10,7 @@ #include "TraceEvent.h" #include "OpenCsdTraceElement.h" +#include "TraceRoute.h" #include "common/trc_gen_elem.h" #include "interfaces/trc_gen_elem_in_i.h" #include "interfaces/trc_pkt_raw_in_i.h" @@ -28,9 +29,10 @@ class OpenCsdPacketCollector : public ITrcGenElemIn, public IPktRawDataMon m_transactionElements; diff --git a/tools/ctrace/src/decode/OpenCsdTraceElement.h b/tools/ctrace/src/decode/OpenCsdTraceElement.h index 93abd419e..4d5b5df0a 100644 --- a/tools/ctrace/src/decode/OpenCsdTraceElement.h +++ b/tools/ctrace/src/decode/OpenCsdTraceElement.h @@ -9,6 +9,7 @@ #define CTRACE_SRC_DECODE_OPENCSDTRACEELEMENT_H #include "TraceEvent.h" +#include "TraceRoute.h" #include #include @@ -39,9 +40,8 @@ struct OpenCsdTraceElement { Kind kind = Kind::Error; std::uint64_t sourceIndex = 0; - // CoreSight Trace Bus ID reported by OpenCSD. ID 0 identifies input for - // which no formatted source ID exists, such as the current SWO path. - std::uint8_t traceBusId = 0U; + // Route assigned by the collector; OpenCSD channel 0 is not an architectural ID. + TraceRouteIdentity route; std::uint32_t channel = 0; std::uint32_t discriminator = 0; std::uint8_t size = 0; diff --git a/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp b/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp index bc01c8182..f3b5d0d29 100644 --- a/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp +++ b/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp @@ -9,9 +9,20 @@ #include "DiagnosticSink.h" #include "TraceEvent.h" +#include "TraceRoute.h" #include #include +#include + +/** @brief Builds public stream context when a route has an architectural ID. */ +static std::vector> routeContext(const TraceRouteIdentity& route) +{ + if (!route.traceBusId.has_value()) { + return {}; + } + return {{"stream", std::to_string(*route.traceBusId)}}; +} /** @brief Appends a raw input offset to a diagnostic when available. */ static std::string atRawOffset(const std::string& message, const TraceEvent& event) @@ -76,40 +87,43 @@ void TraceIssueReporter::finish() return; } m_finished = true; - if (m_overflowPackets == 0U) { - return; - } - - const auto additionalOverflows = m_overflowPackets - 1U; - const auto firstOverflow = m_firstOverflowTimestamp.has_value() - ? "cycle timestamp " + std::to_string(*m_firstOverflowTimestamp) - : std::string("an unknown cycle timestamp"); - auto summary = "first overflow occurred at " + firstOverflow; - if (additionalOverflows > 0U) { - summary += "; " + std::to_string(additionalOverflows) + " more occurred"; + for (const auto& [routeId, state] : m_overflowByRoute) { + (void)routeId; + const auto additionalOverflows = state.packetCount - 1U; + const auto firstOverflow = state.firstTimestamp.has_value() + ? "cycle timestamp " + std::to_string(*state.firstTimestamp) + : std::string("an unknown cycle timestamp"); + auto summary = "first overflow occurred at " + firstOverflow; + if (additionalOverflows > 0U) { + summary += "; " + std::to_string(additionalOverflows) + " more occurred"; + } + report(DiagnosticSink::Severity::Warning, std::move(summary), routeContext(state.route)); } - report(DiagnosticSink::Severity::Warning, summary); } void TraceIssueReporter::reportOverflow(const TraceEvent& event) { - if (m_overflowPackets == 0U) { - m_firstOverflowTimestamp = event.tcyc; + auto& state = m_overflowByRoute[event.route.id]; + if (state.packetCount == 0U) { + state.route = event.route; + state.firstTimestamp = event.tcyc; } - ++m_overflowPackets; + ++state.packetCount; } void TraceIssueReporter::reportError(const TraceEvent& event, const TraceIssueEvent& issue) { report(issue.severity == TraceIssueSeverity::Warning ? DiagnosticSink::Severity::Warning : DiagnosticSink::Severity::Error, - displayErrorMessage(event, issue)); + displayErrorMessage(event, issue), routeContext(event.route)); } -void TraceIssueReporter::report(DiagnosticSink::Severity severity, std::string message) +void TraceIssueReporter::report(DiagnosticSink::Severity severity, std::string message, + std::vector> context) { m_diagnostics.report({ severity, std::move(message), + std::move(context), }); } diff --git a/tools/ctrace/src/diagnostics/TraceIssueReporter.h b/tools/ctrace/src/diagnostics/TraceIssueReporter.h index e5741e6f8..33d0c7e89 100644 --- a/tools/ctrace/src/diagnostics/TraceIssueReporter.h +++ b/tools/ctrace/src/diagnostics/TraceIssueReporter.h @@ -10,10 +10,14 @@ #include "DiagnosticSink.h" #include "TraceEvent.h" +#include "TraceRoute.h" #include +#include #include #include +#include +#include /** @brief Converts trace issue and overflow events into structured diagnostics. */ class TraceIssueReporter final : public TraceEventSink { @@ -27,16 +31,23 @@ class TraceIssueReporter final : public TraceEventSink { void finish(); private: + /** @brief Stores deferred overflow state for one normalized route. */ + struct OverflowState { + TraceRouteIdentity route; + std::optional firstTimestamp; + std::uint64_t packetCount = 0U; + }; + /** @brief Accumulates overflow state for the final summary. */ void reportOverflow(const TraceEvent& event); /** @brief Reports one semantic decoder issue. */ void reportError(const TraceEvent& event, const TraceIssueEvent& issue); /** @brief Submits one normalized trace diagnostic to the sink. */ - void report(DiagnosticSink::Severity severity, std::string message); + void report(DiagnosticSink::Severity severity, std::string message, + std::vector> context = {}); DiagnosticSink& m_diagnostics; - std::optional m_firstOverflowTimestamp; - std::uint64_t m_overflowPackets = 0; + std::map m_overflowByRoute; bool m_finished = false; }; diff --git a/tools/ctrace/src/model/TraceEvent.h b/tools/ctrace/src/model/TraceEvent.h index a4c86a040..6c2e549ee 100644 --- a/tools/ctrace/src/model/TraceEvent.h +++ b/tools/ctrace/src/model/TraceEvent.h @@ -8,6 +8,8 @@ #ifndef CTRACE_SRC_MODEL_TRACEEVENT_H #define CTRACE_SRC_MODEL_TRACEEVENT_H +#include "TraceRoute.h" + #include #include #include @@ -269,8 +271,8 @@ struct TraceEvent { } std::uint64_t index = 0; - // CoreSight Trace Bus ID. ID 0 identifies unformatted single-source input. - std::uint8_t traceBusId = 0U; + // Normalized route identity; an unformatted route has no architectural ID. + TraceRouteIdentity route; std::optional tcyc; // Quality is assigned atomically by the post-decoder. Wire/control events diff --git a/tools/ctrace/src/model/TraceRoute.h b/tools/ctrace/src/model/TraceRoute.h new file mode 100644 index 000000000..44e191d79 --- /dev/null +++ b/tools/ctrace/src/model/TraceRoute.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_MODEL_TRACEROUTE_H +#define CTRACE_SRC_MODEL_TRACEROUTE_H + +#include +#include + +/** @brief Opaque identity of one normalized route within a trace-run catalogue. */ +class TraceRouteId final { +public: + /** @brief Creates the first deterministic catalogue identity. */ + constexpr TraceRouteId() = default; + + /** @brief Creates an opaque route identity from its deterministic catalogue ordinal. */ + explicit constexpr TraceRouteId(std::uint32_t value) + : m_value(value) + { + } + + /** @brief Returns the internal catalogue ordinal. */ + constexpr std::uint32_t value() const noexcept + { + return m_value; + } + +private: + std::uint32_t m_value = 0U; +}; + +/** @brief Compares normalized route identities. */ +constexpr bool operator==(TraceRouteId left, TraceRouteId right) noexcept +{ + return left.value() == right.value(); +} + +/** @brief Compares normalized route identities. */ +constexpr bool operator!=(TraceRouteId left, TraceRouteId right) noexcept +{ + return !(left == right); +} + +/** @brief Orders normalized route identities for associative containers. */ +constexpr bool operator<(TraceRouteId left, TraceRouteId right) noexcept +{ + return left.value() < right.value(); +} + +/** @brief Keeps internal route identity separate from an optional architectural ATB ID. */ +struct TraceRouteIdentity { + TraceRouteId id; + std::optional traceBusId; +}; + +/** @brief Compares a complete normalized route identity. */ +constexpr bool operator==(const TraceRouteIdentity& left, const TraceRouteIdentity& right) noexcept +{ + return left.id == right.id && left.traceBusId == right.traceBusId; +} + +/** @brief Compares a complete normalized route identity. */ +constexpr bool operator!=(const TraceRouteIdentity& left, const TraceRouteIdentity& right) noexcept +{ + return !(left == right); +} + +#endif // CTRACE_SRC_MODEL_TRACEROUTE_H diff --git a/tools/ctrace/src/model/TraceSelection.cpp b/tools/ctrace/src/model/TraceSelection.cpp index c0a7d223e..31b44ec8b 100644 --- a/tools/ctrace/src/model/TraceSelection.cpp +++ b/tools/ctrace/src/model/TraceSelection.cpp @@ -139,6 +139,13 @@ bool TraceSelection::includesStream(std::uint8_t traceBusId) const return streams.empty() || std::find(streams.begin(), streams.end(), traceBusId) != streams.end(); } +bool TraceSelection::includesRoute(const TraceRouteIdentity& route) const +{ + // Public stream selector 0 retains the legacy spelling for a route whose + // unformatted bytes carry no architectural Trace Bus ID. + return includesStream(route.traceBusId.value_or(0U)); +} + bool traceEventSelectedForOutput(const TraceEvent& event, const TraceSelection& selection) { const auto type = traceEventType(event); @@ -146,7 +153,7 @@ bool traceEventSelectedForOutput(const TraceEvent& event, const TraceSelection& if (software != nullptr && software->channel == CoreSight::kExcludedItmStimulusPort) { return false; } - if (!selection.includesStream(event.traceBusId)) { + if (!selection.includesRoute(event.route)) { return false; } return type.has_value() && selection.includesType(traceEventTypeName(*type)); diff --git a/tools/ctrace/src/model/TraceSelection.h b/tools/ctrace/src/model/TraceSelection.h index 22cbdd993..2131d5ba9 100644 --- a/tools/ctrace/src/model/TraceSelection.h +++ b/tools/ctrace/src/model/TraceSelection.h @@ -17,6 +17,7 @@ #include struct TraceEvent; +struct TraceRouteIdentity; /** @brief Identifies event families exposed by the public output filters. */ enum class TraceEventType : std::size_t { @@ -62,6 +63,8 @@ struct TraceSelection { bool includesType(const std::string_view& type) const; /** @brief Tests whether a Trace Bus ID is included. */ bool includesStream(std::uint8_t traceBusId) const; + /** @brief Tests whether a normalized route passes the public stream selector. */ + bool includesRoute(const TraceRouteIdentity& route) const; }; /** @brief Tests whether an event passes a complete output selection. */ diff --git a/tools/ctrace/src/output/OutputRequirements.cpp b/tools/ctrace/src/output/OutputRequirements.cpp index a77a4fae7..8df6c2d7e 100644 --- a/tools/ctrace/src/output/OutputRequirements.cpp +++ b/tools/ctrace/src/output/OutputRequirements.cpp @@ -58,7 +58,7 @@ static OutputPaths outputPaths(const std::filesystem::path& rawInputPath) /** @brief Tests whether one configured source route is selected for output. */ static bool routeMatchesSelection(const CtraceRunSourceMeta& source, const TraceSelection& selection) { - return selection.includesType(source.type) && selection.includesStream(source.traceBusId); + return selection.includesType(source.type) && selection.includesRoute(source.route); } static std::vector> @@ -67,8 +67,10 @@ routeContext(const std::string_view& backend, const CtraceRunMeta& ctraceRunMeta std::vector> context{ {"backend", std::string(backend)}, {"channel", std::string(source.type == "itm" ? "ITM" : "DWT") + std::to_string(source.source)}, - {"stream", std::to_string(source.traceBusId)}, }; + if (source.route.traceBusId.has_value()) { + context.emplace_back("stream", std::to_string(*source.route.traceBusId)); + } if (!ctraceRunMeta.configPath().empty()) { context.emplace_back("config", ctraceRunMeta.configPath()); } @@ -108,8 +110,7 @@ static bool validateCtfRouteIdentity(const CtraceRunMeta& ctraceRunMeta, const T first.addressError == source.addressError && first.dataTypeError == source.dataTypeError && first.dataSizeError == source.dataSizeError; - const auto indistinguishableProcessors = - first.traceBusId == source.traceBusId && first.processorName != source.processorName; + const auto indistinguishableProcessors = first.route == source.route && first.processorName != source.processorName; if ((sameMetadata && !indistinguishableProcessors) || !reported.insert(key).second) { continue; } @@ -119,7 +120,8 @@ static bool validateCtfRouteIdentity(const CtraceRunMeta& ctraceRunMeta, const T context.emplace_back("type", source.type); context.emplace_back("firstProcessor", first.processorName.value_or("")); context.emplace_back("otherProcessor", source.processorName.value_or("")); - context.emplace_back("firstStream", std::to_string(first.traceBusId)); + context.emplace_back("firstStream", first.route.traceBusId.has_value() ? std::to_string(*first.route.traceBusId) + : ""); reportRequirementError( diagnostics, "CTF metadata cannot describe conflicting active type/source routes from different processors or Trace Bus IDs", @@ -327,19 +329,19 @@ static bool validateCtfDwtMetadata(const CtraceRunMeta& ctraceRunMeta, const Tra static std::vector resolveCtfSources(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection) { - std::set> resolvedKeys; + std::set> resolvedKeys; std::vector sources; for (const auto& route : ctraceRunMeta.sources()) { if ((route.type != "itm" && route.type != "dwt") || (route.type == "itm" && route.source == 0U) || !routeMatchesSelection(route, selection) || - !resolvedKeys.emplace(route.type, route.source, route.traceBusId).second) { + !resolvedKeys.emplace(route.type, route.source, route.route.id).second) { continue; } sources.push_back({ route.type, route.source, - route.traceBusId, + route.route, route.label, route.address, route.dataType, @@ -349,6 +351,16 @@ static std::vector resolveCtfSources(const CtraceRunMeta& c return sources; } +/** @brief Copies the complete normalized route catalogue for CTF state validation. */ +static std::vector resolveCtfRoutes(const CtraceRunMeta& ctraceRunMeta) +{ + std::vector routes; + for (const auto& route : ctraceRunMeta.routes()) { + routes.push_back(route.identity); + } + return routes; +} + bool TraceOutputPlan::hasRequestedOutputs() const { return csvRequested || ctfRequested; @@ -386,7 +398,8 @@ TraceOutputPlan planTraceOutputs(const TraceOutputRequest& request, const std::f : std::nullopt; if (clock.has_value() && validRoutes && validTypes && sources.has_value()) { plan.ctf = CtfOutputConfig{ - paths.ctf, paths.traceCompassXml, *clock, request.selection, std::move(*sources), + paths.ctf, paths.traceCompassXml, *clock, request.selection, + std::move(*sources), resolveCtfRoutes(ctraceRunMeta), true, }; } } diff --git a/tools/ctrace/src/output/TraceOutputConfig.h b/tools/ctrace/src/output/TraceOutputConfig.h index da0c9e60c..4f272bc49 100644 --- a/tools/ctrace/src/output/TraceOutputConfig.h +++ b/tools/ctrace/src/output/TraceOutputConfig.h @@ -8,6 +8,7 @@ #ifndef CTRACE_SRC_OUTPUT_TRACEOUTPUTCONFIG_H #define CTRACE_SRC_OUTPUT_TRACEOUTPUTCONFIG_H +#include "TraceRoute.h" #include "TraceSelection.h" #include @@ -28,7 +29,7 @@ struct TraceOutputRequest { struct ResolvedTraceSource { std::string type; std::uint32_t source = 0; - std::uint8_t traceBusId = 0U; + TraceRouteIdentity route; std::optional label; std::optional address; std::string dataType = "unsigned"; @@ -45,12 +46,15 @@ struct CsvOutputConfig { struct CtfOutputConfig { /** @brief Creates a complete CTF output configuration. */ CtfOutputConfig(std::filesystem::path outputDirectory, std::filesystem::path traceCompassXmlPath, - std::uint64_t clockHz, TraceSelection selection, std::vector sources) + std::uint64_t clockHz, TraceSelection selection, std::vector sources, + std::vector routes = {}, bool routeCatalogueConfigured = false) : outputDirectory(std::move(outputDirectory)), traceCompassXmlPath(std::move(traceCompassXmlPath)), coreClockHz(clockHz), selection(std::move(selection)), - sources(std::move(sources)) + sources(std::move(sources)), + routes(std::move(routes)), + routeCatalogueConfigured(routeCatalogueConfigured) { } @@ -59,6 +63,9 @@ struct CtfOutputConfig { std::uint64_t coreClockHz = 0; TraceSelection selection; std::vector sources; + std::vector routes; + /** @brief Distinguishes an explicit empty catalogue from legacy route inference. */ + bool routeCatalogueConfigured = false; }; #endif // CTRACE_SRC_OUTPUT_TRACEOUTPUTCONFIG_H diff --git a/tools/ctrace/src/output/csv/CsvRowMapper.cpp b/tools/ctrace/src/output/csv/CsvRowMapper.cpp index 8a97fb930..073d7fbdb 100644 --- a/tools/ctrace/src/output/csv/CsvRowMapper.cpp +++ b/tools/ctrace/src/output/csv/CsvRowMapper.cpp @@ -153,8 +153,8 @@ static CsvRow eventToCsvRow(const TraceEvent& event) if (event.tcyc.has_value()) { row[column(CsvColumn::Cycles)] = std::to_string(*event.tcyc); } - if (event.traceBusId != 0U) { - row[column(CsvColumn::Stream)] = std::to_string(event.traceBusId); + if (event.route.traceBusId.has_value()) { + row[column(CsvColumn::Stream)] = std::to_string(*event.route.traceBusId); } if (const auto type = traceEventType(event)) { row[column(CsvColumn::Type)] = traceEventTypeName(*type); diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp index 46f8e2548..4d4530934 100644 --- a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp @@ -172,6 +172,8 @@ CtfBundleOutput::CtfBundleOutput(CtfOutputConfig config, DiagnosticSink* diagnos std::move(config.selection), std::move(config.sources), diagnostics, + std::move(config.routes), + !config.routeCatalogueConfigured, }) { validateOutputTargets(m_ctfOutputDirectory, m_traceCompassXmlPath); diff --git a/tools/ctrace/src/output/ctf/CtfEncoder.cpp b/tools/ctrace/src/output/ctf/CtfEncoder.cpp index 5f95076b1..66bf61736 100644 --- a/tools/ctrace/src/output/ctf/CtfEncoder.cpp +++ b/tools/ctrace/src/output/ctf/CtfEncoder.cpp @@ -32,6 +32,26 @@ static std::uint32_t ctfOverflowCount(std::uint64_t count) { return static_cast(std::min(count, std::numeric_limits::max())); } + +/** @brief Adapts a normalized route to the legacy CTF event-context field. */ +static std::uint8_t legacyCtfTraceBusId(const TraceRouteIdentity& route) +{ + return route.traceBusId.value_or(0U); +} + +/** @brief Enforces an explicit normalized route catalogue while retaining the legacy lazy fallback. */ +static void validateConfiguredRoute(const CtfEncoderConfig& config, const TraceRouteIdentity& route) +{ + if (config.legacyRouteFallback && config.routes.empty()) { + return; + } + const auto configured = std::find_if(config.routes.begin(), config.routes.end(), + [&](const TraceRouteIdentity& candidate) { return candidate.id == route.id; }); + if (configured == config.routes.end() || *configured != route) { + throw std::runtime_error("CTF route identity does not match the configured normalized route catalogue"); + } +} + /** @brief Resolves the configured CTF value representation for one DWT comparator. */ static const CtfSchema::ValueVariant& dwtValueVariant(const ResolvedTraceSource* source, std::uint32_t comparator) { @@ -55,13 +75,13 @@ static bool equivalentSourceMetadata(const ResolvedTraceSource& left, const Reso /** @brief Finds an unambiguous configured source for one event route. */ static const ResolvedTraceSource* resolvedTraceSource(const CtfEncoderConfig& config, const char* type, - std::uint8_t traceBusId, std::uint32_t source) + const TraceRouteIdentity& route, std::uint32_t source) { const auto exact = std::find_if(config.sources.begin(), config.sources.end(), [&](const ResolvedTraceSource& candidate) { - return candidate.type == type && candidate.source == source && candidate.traceBusId == traceBusId; + return candidate.type == type && candidate.source == source && candidate.route == route; }); - if (exact != config.sources.end() || traceBusId != 0U) { + if (exact != config.sources.end() || route.traceBusId.has_value()) { return exact == config.sources.end() ? nullptr : &*exact; } @@ -158,20 +178,34 @@ void CtfEncoder::start(const std::filesystem::path& outputDirectory) abort(); m_outputDirectory = outputDirectory; try { + m_routeIdentities.clear(); + m_bootstrappedRoutes.clear(); m_streamStates.clear(); m_reportedDwtSizeMismatches.clear(); m_exceptionLanes.clear(); m_stream.open(m_outputDirectory / "stream_0", CtfSchema::SwoStreamId); m_recording = true; - auto initialTraceBusIds = - std::set(m_config.selection.streams.begin(), m_config.selection.streams.end()); - if (initialTraceBusIds.empty()) { - initialTraceBusIds.insert(0U); + std::map initialRoutes; + const auto addInitialRoute = [&](const TraceRouteIdentity& route) { + const auto [found, inserted] = initialRoutes.emplace(route.id, route); + if (!inserted && found->second != route) { + throw std::runtime_error("CTF configuration contains inconsistent normalized route identities"); + } + }; + for (const auto& route : m_config.routes) { + addInitialRoute(route); } - for (const auto traceBusId : initialTraceBusIds) { - writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart), traceBusId, - m_config.selection.types.empty()); - (void)exceptionLane(traceBusId); + for (const auto& source : m_config.sources) { + validateConfiguredRoute(m_config, source.route); + if (m_config.legacyRouteFallback && m_config.routes.empty()) { + addInitialRoute(source.route); + } + } + for (const auto& [routeId, route] : initialRoutes) { + (void)routeId; + if (m_config.selection.includesRoute(route)) { + bootstrapRoute(route); + } } } catch (...) { abort(); @@ -184,6 +218,10 @@ void CtfEncoder::stop() if (!m_recording) { return; } + if (m_bootstrappedRoutes.empty() && m_config.legacyRouteFallback && m_config.routes.empty() && + m_config.sources.empty() && m_config.selection.includesRoute({})) { + bootstrapRoute({}); + } m_recording = false; m_stream.close(); writeMetadataFile(); @@ -201,14 +239,16 @@ void CtfEncoder::writeEvent(const TraceEvent& event) if (!m_recording) { return; } - if (!m_config.selection.includesStream(event.traceBusId)) { + if (!m_config.selection.includesRoute(event.route)) { return; } + validateConfiguredRoute(m_config, event.route); + bootstrapRoute(event.route); if (!isTraceEvent(event) && event.tcyc.has_value()) { - auto& eventTimestamp = m_streamStates[event.traceBusId].eventTimestamp; + auto& eventTimestamp = streamState(event.route).eventTimestamp; eventTimestamp = std::max(eventTimestamp, *event.tcyc); if (event.quality.has_value() && event.quality->timestampReliable) { - m_streamStates[event.traceBusId].localTimestampObserved = true; + streamState(event.route).localTimestampObserved = true; } } @@ -219,7 +259,7 @@ void CtfEncoder::writeEvent(const TraceEvent& event) } } else if (const auto* exception = traceEventPayload(event)) { if (exception->action != ExceptionAction::Unknown) { - writeExceptionEvent(event.traceBusId, *exception); + writeExceptionEvent(event.route, *exception); } } else if (const auto* data = traceEventPayload(event)) { if (selected) { @@ -246,17 +286,17 @@ void CtfEncoder::writeEvent(const TraceEvent& event) writePcSampleEvent(event, *sample); } } else if (isTraceEvent(event)) { - auto& streamState = m_streamStates[event.traceBusId]; + auto& routeState = streamState(event.route); if (event.quality.has_value()) { - streamState.overflowCount = std::max(streamState.overflowCount, event.quality->overflowCount); + routeState.overflowCount = std::max(routeState.overflowCount, event.quality->overflowCount); } else { - ++streamState.overflowCount; + ++routeState.overflowCount; } - writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::Overflow), event.traceBusId, selected); + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::Overflow), event.route, selected); } else if (isTraceEvent(event)) { - m_streamStates[event.traceBusId].localTimestampObserved = true; + streamState(event.route).localTimestampObserved = true; } else if (isTraceEvent(event)) { - writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::Resync), event.traceBusId, + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::Resync), event.route, m_config.selection.types.empty()); } else if (const auto* timestamp = traceEventPayload(event)) { if (selected) { @@ -264,13 +304,13 @@ void CtfEncoder::writeEvent(const TraceEvent& event) } } else if (const auto* issue = traceEventPayload(event)) { if (issue->code == TraceIssueCode::DataLoss) { - writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss), event.traceBusId, selected); + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss), event.route, selected); } else { if (event.quality.has_value() && event.quality->overflow) { - writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss), event.traceBusId, selected); + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss), event.route, selected); } if (selected) { - writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError), event.traceBusId, true); + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError), event.route, true); } } } @@ -280,11 +320,12 @@ void CtfEncoder::writePcSampleEvent(const TraceEvent& event, const PcSampleTrace { const auto pcSize = sample.sleeping ? 0U : 4U; const auto payloadSize = 1U + pcSize + 1U + 4U; - const auto eventTimestamp = allocateEventTimestamp(event.traceBusId); + const auto eventTimestamp = allocateEventTimestamp(event.route); + const auto traceBusId = legacyCtfTraceBusId(event.route); const auto quality = computeSampleQuality(event); const auto state = CtfSchema::value(sample.sleeping ? CtfSchema::PcSampleState::Sleep : CtfSchema::PcSampleState::Pc); - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::PcSample), eventTimestamp, event.traceBusId, payloadSize, + m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::PcSample), eventTimestamp, traceBusId, payloadSize, [&](CtfStreamWriter::Record& record) { record.writeU8(state); if (!sample.sleeping) { @@ -295,11 +336,31 @@ void CtfEncoder::writePcSampleEvent(const TraceEvent& event, const PcSampleTrace }); } -std::uint64_t CtfEncoder::allocateEventTimestamp(std::uint8_t traceBusId) +std::uint64_t CtfEncoder::allocateEventTimestamp(const TraceRouteIdentity& route) { // CtfStreamWriter applies the final monotonic clamp across the multiplexed - // CTF stream. This value remains local to the CoreSight Trace Bus ID. - return m_streamStates[traceBusId].eventTimestamp; + // CTF stream. This value remains local to the normalized route. + return streamState(route).eventTimestamp; +} + +CtfEncoder::StreamState& CtfEncoder::streamState(const TraceRouteIdentity& route) +{ + const auto [identity, inserted] = m_routeIdentities.emplace(route.id, route); + if (!inserted && identity->second != route) { + throw std::runtime_error("CTF event route identity does not match normalized route catalogue"); + } + return m_streamStates[route.id]; +} + +void CtfEncoder::bootstrapRoute(const TraceRouteIdentity& route) +{ + (void)streamState(route); + if (!m_bootstrappedRoutes.insert(route.id).second) { + return; + } + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart), route, + m_config.selection.types.empty()); + (void)exceptionLane(route); } void CtfEncoder::writeSoftwareEvent(const TraceEvent& event, const SoftwareTraceEvent& software) @@ -309,9 +370,10 @@ void CtfEncoder::writeSoftwareEvent(const TraceEvent& event, const SoftwareTrace throw std::runtime_error("CTF ITM value has an invalid SWO payload size"); } const auto quality = computeSampleQuality(event); - const auto eventTimestamp = allocateEventTimestamp(event.traceBusId); + const auto eventTimestamp = allocateEventTimestamp(event.route); + const auto traceBusId = legacyCtfTraceBusId(event.route); const auto payloadSize = 1U + 1U + variant->byteSize + 1U + 4U; - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::Itm), eventTimestamp, event.traceBusId, payloadSize, + m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::Itm), eventTimestamp, traceBusId, payloadSize, [&](CtfStreamWriter::Record& record) { record.writeU8(static_cast(software.channel & 0xffU)); record.writeU8(CtfSchema::value(variant->tag)); @@ -323,16 +385,17 @@ void CtfEncoder::writeSoftwareEvent(const TraceEvent& event, const SoftwareTrace void CtfEncoder::writeDwtValueEvent(const TraceEvent& event, const DwtDataTraceEvent& data) { - const auto* source = resolvedTraceSource(m_config, "dwt", event.traceBusId, data.comparator); + const auto* source = resolvedTraceSource(m_config, "dwt", event.route, data.comparator); reportDwtSizeMismatch(event, data, source); const auto& variant = dwtValueVariant(source, data.comparator); const auto& pcVariant = dwtAddressVariant(data.pc); const auto& addressVariant = dwtAddressVariant(data.address); const auto payloadSize = 1U + 1U + 1U + variant.byteSize + 1U + pcVariant.byteSize + 1U + addressVariant.byteSize + 1U + 4U; - const auto eventTimestamp = allocateEventTimestamp(event.traceBusId); + const auto eventTimestamp = allocateEventTimestamp(event.route); + const auto traceBusId = legacyCtfTraceBusId(event.route); const auto quality = computeSampleQuality(event); - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtValue), eventTimestamp, event.traceBusId, payloadSize, + m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtValue), eventTimestamp, traceBusId, payloadSize, [&](CtfStreamWriter::Record& record) { record.writeU8(static_cast(data.comparator & 0xffU)); record.writeU8(CtfSchema::value(data.access == AccessType::Read @@ -352,7 +415,7 @@ void CtfEncoder::reportDwtSizeMismatch(const TraceEvent& event, const DwtDataTra { const auto configuredSize = source != nullptr ? source->dataSize : ResolvedTraceSource{}.dataSize; if (configuredSize == data.size || m_config.diagnostics == nullptr || - !m_reportedDwtSizeMismatches.insert({event.traceBusId, data.comparator}).second) { + !m_reportedDwtSizeMismatches.insert({event.route.id, data.comparator}).second) { return; } @@ -362,7 +425,9 @@ void CtfEncoder::reportDwtSizeMismatch(const TraceEvent& event, const DwtDataTra {"configuredSize", std::to_string(configuredSize)}, {"swoSize", std::to_string(data.size)}, }; - context.emplace_back("stream", std::to_string(event.traceBusId)); + if (event.route.traceBusId.has_value()) { + context.emplace_back("stream", std::to_string(*event.route.traceBusId)); + } m_config.diagnostics->report({ DiagnosticSink::Severity::Warning, "configured ctrace-run size does not match the decoded SWO payload size", @@ -372,14 +437,15 @@ void CtfEncoder::reportDwtSizeMismatch(const TraceEvent& event, const DwtDataTra void CtfEncoder::writeDwtAddrEvent(const TraceEvent& event, const DwtAddressTraceEvent& data) { - const auto eventTimestamp = allocateEventTimestamp(event.traceBusId); + const auto eventTimestamp = allocateEventTimestamp(event.route); + const auto traceBusId = legacyCtfTraceBusId(event.route); const auto quality = computeSampleQuality(event); const auto pc = dwtAddressPc(data); const auto address = dwtDataAddress(data); const auto& pcVariant = dwtAddressVariant(pc); const auto& addressVariant = dwtAddressVariant(address); const auto payloadSize = 1U + 1U + pcVariant.byteSize + 1U + addressVariant.byteSize + 1U + 4U; - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtAddress), eventTimestamp, event.traceBusId, payloadSize, + m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtAddress), eventTimestamp, traceBusId, payloadSize, [&](CtfStreamWriter::Record& record) { record.writeU8(static_cast(data.comparator & 0xffU)); writeDwtAddress(record, pc, pcVariant); @@ -392,9 +458,10 @@ void CtfEncoder::writeDwtAddrEvent(const TraceEvent& event, const DwtAddressTrac void CtfEncoder::writeDwtMatchEvent(const TraceEvent& event, const DwtMatchTraceEvent& match) { constexpr auto payloadSize = 1U + 1U + 4U; - const auto eventTimestamp = allocateEventTimestamp(event.traceBusId); + const auto eventTimestamp = allocateEventTimestamp(event.route); + const auto traceBusId = legacyCtfTraceBusId(event.route); const auto quality = computeSampleQuality(event); - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtMatch), eventTimestamp, event.traceBusId, payloadSize, + m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtMatch), eventTimestamp, traceBusId, payloadSize, [&](CtfStreamWriter::Record& record) { record.writeU8(static_cast(match.comparator & 0xffU)); record.writeU8(quality.first); @@ -405,14 +472,15 @@ void CtfEncoder::writeDwtMatchEvent(const TraceEvent& event, const DwtMatchTrace void CtfEncoder::writeDwtEvent(const TraceEvent& event, const DwtEventTraceEvent& counters) { constexpr auto payloadSize = 1U + 1U + 4U; - const auto eventTimestamp = allocateEventTimestamp(event.traceBusId); + const auto eventTimestamp = allocateEventTimestamp(event.route); + const auto traceBusId = legacyCtfTraceBusId(event.route); const auto quality = computeSampleQuality(event); for (const auto counter : kDwtEventCounters) { const auto counterBit = dwtEventCounterBit(counter); if ((counters.counterMask & counterBit) == 0U) { continue; } - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtEvent), eventTimestamp, event.traceBusId, payloadSize, + m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtEvent), eventTimestamp, traceBusId, payloadSize, [&](CtfStreamWriter::Record& record) { record.writeU8(CtfSchema::value(counter)); record.writeU8(quality.first); @@ -424,13 +492,14 @@ void CtfEncoder::writeDwtEvent(const TraceEvent& event, const DwtEventTraceEvent void CtfEncoder::writePmuEvent(const TraceEvent& event, const PmuTraceEvent& counters) { constexpr auto payloadSize = 1U + 1U + 4U; - const auto eventTimestamp = allocateEventTimestamp(event.traceBusId); + const auto eventTimestamp = allocateEventTimestamp(event.route); + const auto traceBusId = legacyCtfTraceBusId(event.route); const auto quality = computeSampleQuality(event); for (const auto counter : kPmuEventCounters) { if ((counters.overflowMask & pmuEventCounterBit(counter)) == 0U) { continue; } - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::PmuEvent), eventTimestamp, event.traceBusId, payloadSize, + m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::PmuEvent), eventTimestamp, traceBusId, payloadSize, [&](CtfStreamWriter::Record& record) { record.writeU8(CtfSchema::value(counter)); record.writeU8(quality.first); @@ -442,49 +511,51 @@ void CtfEncoder::writePmuEvent(const TraceEvent& event, const PmuTraceEvent& cou void CtfEncoder::writeGlobalTimestampEvent(const TraceEvent& event, const GlobalTimestampTraceEvent& timestamp) { constexpr auto payloadSize = 8U + 1U; - const auto eventTimestamp = allocateEventTimestamp(event.traceBusId); - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::GlobalTimestamp), eventTimestamp, event.traceBusId, - payloadSize, [&](CtfStreamWriter::Record& record) { + const auto eventTimestamp = allocateEventTimestamp(event.route); + const auto traceBusId = legacyCtfTraceBusId(event.route); + m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::GlobalTimestamp), eventTimestamp, traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { record.writeU64(timestamp.value); record.writeU8(timestamp.clockChange ? 1U : 0U); }); } -void CtfEncoder::writeTraceStatusEvent(std::uint8_t reason, std::uint8_t traceBusId, bool emitEvent) +void CtfEncoder::writeTraceStatusEvent(std::uint8_t reason, const TraceRouteIdentity& route, bool emitEvent) { if (reason == CtfSchema::value(CtfSchema::TraceStatusReason::Overflow) || reason == CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss)) { - const auto lane = m_exceptionLanes.find(traceBusId); + const auto lane = m_exceptionLanes.find(route.id); if (lane != m_exceptionLanes.end()) { - lane->second.resetForDiscontinuity( - [this, traceBusId](ExceptionNumber number, CtfExceptionLaneTracker::RecordAction action, - CtfExceptionLaneTracker::RecordOrigin origin) { - emitExceptionRecord(traceBusId, number, action, origin); - }); + lane->second.resetForDiscontinuity([this, route](ExceptionNumber number, + CtfExceptionLaneTracker::RecordAction action, + CtfExceptionLaneTracker::RecordOrigin origin) { + emitExceptionRecord(route, number, action, origin); + }); } } if (emitEvent) { constexpr auto payloadSize = 1U + 4U; - const auto eventTimestamp = allocateEventTimestamp(traceBusId); + const auto eventTimestamp = allocateEventTimestamp(route); + const auto traceBusId = legacyCtfTraceBusId(route); m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::TraceStatus), eventTimestamp, traceBusId, payloadSize, [&](CtfStreamWriter::Record& record) { record.writeU8(reason); - record.writeU32(ctfOverflowCount(m_streamStates[traceBusId].overflowCount)); + record.writeU32(ctfOverflowCount(streamState(route).overflowCount)); }); } } -void CtfEncoder::writeExceptionEvent(std::uint8_t traceBusId, const ExceptionTraceEvent& exception) +void CtfEncoder::writeExceptionEvent(const TraceRouteIdentity& route, const ExceptionTraceEvent& exception) { - exceptionLane(traceBusId) - .consume(exception, [this, traceBusId](ExceptionNumber number, CtfExceptionLaneTracker::RecordAction action, + exceptionLane(route).consume(exception, + [this, route](ExceptionNumber number, CtfExceptionLaneTracker::RecordAction action, CtfExceptionLaneTracker::RecordOrigin origin) { - emitExceptionRecord(traceBusId, number, action, origin); - }); + emitExceptionRecord(route, number, action, origin); + }); } -void CtfEncoder::emitExceptionRecord(std::uint8_t traceBusId, ExceptionNumber number, +void CtfEncoder::emitExceptionRecord(const TraceRouteIdentity& route, ExceptionNumber number, CtfExceptionLaneTracker::RecordAction action, CtfExceptionLaneTracker::RecordOrigin origin) { @@ -496,12 +567,13 @@ void CtfEncoder::emitExceptionRecord(std::uint8_t traceBusId, ExceptionNumber nu number, semanticAction, }}; - selectionEvent.traceBusId = traceBusId; + selectionEvent.route = route; if (!traceEventSelectedForOutput(selectionEvent, m_config.selection)) { return; } constexpr auto payloadSize = 2U + 1U + 2U + 1U; - const auto eventTimestamp = allocateEventTimestamp(traceBusId); + const auto eventTimestamp = allocateEventTimestamp(route); + const auto traceBusId = legacyCtfTraceBusId(route); const auto encodedAction = CtfSchema::value( action == CtfExceptionLaneTracker::RecordAction::Enter ? CtfSchema::ExceptionAction::Entered @@ -519,39 +591,39 @@ void CtfEncoder::emitExceptionRecord(std::uint8_t traceBusId, ExceptionNumber nu }); } -CtfExceptionLaneTracker& CtfEncoder::exceptionLane(std::uint8_t traceBusId) +CtfExceptionLaneTracker& CtfEncoder::exceptionLane(const TraceRouteIdentity& route) { - const auto [lane, inserted] = m_exceptionLanes.try_emplace(traceBusId); + (void)streamState(route); + const auto [lane, inserted] = m_exceptionLanes.try_emplace(route.id); if (inserted) { - lane->second.startThreadMode( - [this, traceBusId](ExceptionNumber number, CtfExceptionLaneTracker::RecordAction action, - CtfExceptionLaneTracker::RecordOrigin origin) { - emitExceptionRecord(traceBusId, number, action, origin); - }); + lane->second.startThreadMode([this, route](ExceptionNumber number, CtfExceptionLaneTracker::RecordAction action, + CtfExceptionLaneTracker::RecordOrigin origin) { + emitExceptionRecord(route, number, action, origin); + }); } return lane->second; } std::pair CtfEncoder::computeSampleQuality(const TraceEvent& event) { - auto& streamState = m_streamStates[event.traceBusId]; - const auto previousOverflowCount = streamState.overflowCount; - const auto overflowCount = event.quality.has_value() ? event.quality->overflowCount : streamState.overflowCount; + auto& routeState = streamState(event.route); + const auto previousOverflowCount = routeState.overflowCount; + const auto overflowCount = event.quality.has_value() ? event.quality->overflowCount : routeState.overflowCount; const auto timestampReliable = event.quality.has_value() ? event.quality->timestampReliable : true; const auto overflow = event.quality.has_value() ? event.quality->overflow : overflowCount > previousOverflowCount; const auto flags = static_cast((overflow ? CtfSchema::SampleFlagOverflow : 0U) | (timestampReliable ? CtfSchema::SampleFlagTimestampReliable : 0U) | - (streamState.localTimestampObserved ? 0U : CtfSchema::SampleFlagBeforeFirstTimestamp)); - streamState.overflowCount = std::max(streamState.overflowCount, overflowCount); + (routeState.localTimestampObserved ? 0U : CtfSchema::SampleFlagBeforeFirstTimestamp)); + routeState.overflowCount = std::max(routeState.overflowCount, overflowCount); return {flags, ctfOverflowCount(overflowCount)}; } void CtfEncoder::writeMetadataFile() { std::set observedExceptionNumbers; - for (const auto& [traceBusId, lane] : m_exceptionLanes) { - (void)traceBusId; + for (const auto& [routeId, lane] : m_exceptionLanes) { + (void)routeId; observedExceptionNumbers.insert(lane.observedExceptionNumbers().begin(), lane.observedExceptionNumbers().end()); } CtfMetadataWriter::write(m_outputDirectory, m_stream.uuidString(), m_config.coreClockHz, m_config.sources, diff --git a/tools/ctrace/src/output/ctf/CtfEncoder.h b/tools/ctrace/src/output/ctf/CtfEncoder.h index 265549007..5dd7ba1f4 100644 --- a/tools/ctrace/src/output/ctf/CtfEncoder.h +++ b/tools/ctrace/src/output/ctf/CtfEncoder.h @@ -13,6 +13,7 @@ #include "TraceSelection.h" #include "TraceEvent.h" #include "TraceOutputConfig.h" +#include "TraceRoute.h" #include #include @@ -29,6 +30,9 @@ struct CtfEncoderConfig { TraceSelection selection; std::vector sources; DiagnosticSink* diagnostics = nullptr; + std::vector routes; + /** @brief Permits direct legacy callers to infer a route when no catalogue was supplied. */ + bool legacyRouteFallback = true; }; /** @brief Encodes semantic trace events into one CTF stream and metadata set. */ @@ -62,11 +66,15 @@ class CtfEncoder final { }; /** @brief Allocates the next monotonic event timestamp for one stream. */ - std::uint64_t allocateEventTimestamp(std::uint8_t traceBusId); + std::uint64_t allocateEventTimestamp(const TraceRouteIdentity& route); + /** @brief Returns route-local CTF state while rejecting identity mismatches. */ + StreamState& streamState(const TraceRouteIdentity& route); + /** @brief Emits the legacy stream-local bootstrap exactly once. */ + void bootstrapRoute(const TraceRouteIdentity& route); /** @brief Writes metadata that matches the completed binary stream. */ void writeMetadataFile(); /** @brief Emits or applies a trace-status transition. */ - void writeTraceStatusEvent(std::uint8_t reason, std::uint8_t traceBusId, bool emitEvent = true); + void writeTraceStatusEvent(std::uint8_t reason, const TraceRouteIdentity& route, bool emitEvent = true); /** @brief Encodes one ITM software event. */ void writeSoftwareEvent(const TraceEvent& event, const SoftwareTraceEvent& software); /** @brief Encodes one DWT data value event. */ @@ -86,13 +94,12 @@ class CtfEncoder final { /** @brief Encodes one reconstructed global timestamp event. */ void writeGlobalTimestampEvent(const TraceEvent& event, const GlobalTimestampTraceEvent& timestamp); /** @brief Applies one exception transition to its CTF lane state. */ - void writeExceptionEvent(std::uint8_t traceBusId, const ExceptionTraceEvent& exception); + void writeExceptionEvent(const TraceRouteIdentity& route, const ExceptionTraceEvent& exception); /** @brief Emits one concrete exception lane record. */ - void emitExceptionRecord(std::uint8_t traceBusId, ExceptionNumber number, - CtfExceptionLaneTracker::RecordAction action, - CtfExceptionLaneTracker::RecordOrigin origin); + void emitExceptionRecord(const TraceRouteIdentity& route, ExceptionNumber number, + CtfExceptionLaneTracker::RecordAction action, CtfExceptionLaneTracker::RecordOrigin origin); /** @brief Returns the exception tracker for one stream. */ - CtfExceptionLaneTracker& exceptionLane(std::uint8_t traceBusId); + CtfExceptionLaneTracker& exceptionLane(const TraceRouteIdentity& route); /** @brief Computes CTF sample flags and saturated overflow count. */ std::pair computeSampleQuality(const TraceEvent& event); @@ -100,9 +107,11 @@ class CtfEncoder final { std::filesystem::path m_outputDirectory; CtfStreamWriter m_stream; bool m_recording = false; - std::map m_streamStates; - std::set> m_reportedDwtSizeMismatches; - std::map m_exceptionLanes; + std::map m_routeIdentities; + std::set m_bootstrappedRoutes; + std::map m_streamStates; + std::set> m_reportedDwtSizeMismatches; + std::map m_exceptionLanes; }; #endif // CTRACE_SRC_OUTPUT_CTF_CTFENCODER_H diff --git a/tools/ctrace/src/tracerun/CtraceRunMeta.cpp b/tools/ctrace/src/tracerun/CtraceRunMeta.cpp index 38909537c..6e08c31cb 100644 --- a/tools/ctrace/src/tracerun/CtraceRunMeta.cpp +++ b/tools/ctrace/src/tracerun/CtraceRunMeta.cpp @@ -441,7 +441,6 @@ static CtraceRunSourceMeta sourceMeta(const TraceRunConfig& config, const TraceR const auto dataSetup = reference.type == "dwt" ? referencedDataSetup(config, boundReference, *reference.dataSetupIndex) : std::optional{}; - meta.traceBusId = static_cast(reference.stream.value_or(0U)); meta.source = source; meta.label = reference.label; if (reference.type != "dwt") { @@ -812,13 +811,14 @@ struct FormattedRouteState { /** @brief Registers a bound processor route and rejects one processor mapped to two ITM IDs. */ static void registerBoundRoute(const TraceRunConfig& config, const TraceRunReference& reference, - const CtraceRunRoute& route, std::map& boundRoutes) + const CtraceRunRoute& route, std::uint8_t traceBusId, + std::map& boundRoutes) { if (!route.processorName.has_value()) { return; } - const auto [found, inserted] = boundRoutes.emplace(*route.processorName, *route.traceBusId); - if (!inserted && found->second != *route.traceBusId) { + const auto [found, inserted] = boundRoutes.emplace(*route.processorName, traceBusId); + if (!inserted && found->second != traceBusId) { throw std::runtime_error(configError(config, reference.line, "processor '" + *route.processorName + "' has ITM routes bound to multiple CoreSight Trace Bus IDs")); @@ -835,7 +835,6 @@ static FormattedRouteState& mergeFormattedRoute(const TraceRunConfig& config, co auto [found, inserted] = routes.emplace(traceBusId, FormattedRouteState{}); auto& state = found->second; if (inserted) { - state.route.traceBusId = traceBusId; state.route.processorName = processorName; } else if (state.route.processorName.has_value() && processorName.has_value() && state.route.processorName != processorName) { @@ -845,7 +844,7 @@ static FormattedRouteState& mergeFormattedRoute(const TraceRunConfig& config, co } else if (!state.route.processorName.has_value() && processorName.has_value()) { state.route.processorName = processorName; } - registerBoundRoute(config, reference, state.route, boundRoutes); + registerBoundRoute(config, reference, state.route, traceBusId, boundRoutes); return state; } @@ -962,7 +961,7 @@ static CtraceRunSourceMeta formattedSourceMeta(const TraceRunConfig& config, con boundReference.processorName = route.processorName; auto meta = sourceMeta(config, boundReference, source, identity); meta.processorName = route.processorName; - meta.traceBusId = *route.traceBusId; + meta.route = route.identity; return meta; } @@ -995,7 +994,7 @@ static void bindStreamlessRoute(const TraceRunConfig& config, const TraceRunRefe if (!route.processorName.has_value() && processorName.has_value()) { route.processorName = processorName; } - registerBoundRoute(config, reference, route, boundRoutes); + registerBoundRoute(config, reference, route, traceBusId, boundRoutes); } /** @brief Builds the strict formatted route catalogue without constructing decoder objects. */ @@ -1076,8 +1075,11 @@ static std::vector formattedRoutes(const TraceRunConfig& config, std::vector routes; routes.reserve(states.size()); + std::uint32_t routeOrdinal = 0U; for (auto& [traceBusId, state] : states) { auto& route = state.route; + route.identity = {TraceRouteId{routeOrdinal}, traceBusId}; + ++routeOrdinal; applyRouteSetupMetadata(config, route, routeSetupFragments(setups, route), warnings); for (const auto& reference : config.references) { @@ -1119,7 +1121,7 @@ CtraceRunMeta CtraceRunMeta::fromConfig(const TraceRunConfig& config) std::optional commonPrescaler; std::optional commonEnableMask; for (const auto& route : ctraceRunMeta.m_routes) { - const auto traceBusId = *route.traceBusId; + const auto traceBusId = *route.identity.traceBusId; ctraceRunMeta.m_timestampsByTraceBusId.emplace( traceBusId, CtraceRunTimestampMeta{route.processorName, route.timestampClockHz, route.timestampClockError}); ctraceRunMeta.m_timestampPrescalersByTraceBusId.emplace(traceBusId, route.timestampPrescaler); @@ -1250,9 +1252,6 @@ CtraceRunMeta CtraceRunMeta::fromConfig(const TraceRunConfig& config) throw std::runtime_error( config.path + ": unformatted SINGLE trace cannot choose between different timestamps.itm-prescaler values"); } - for (auto& source : ctraceRunMeta.m_sources) { - source.traceBusId = 0U; - } ctraceRunMeta.m_timestampClockHz = commonTimestampClock(processors); ctraceRunMeta.m_timestampPrescaler = timestampPrescaler; ctraceRunMeta.m_itmEnableMask = commonItmEnableMask(processors); diff --git a/tools/ctrace/src/tracerun/CtraceRunMeta.h b/tools/ctrace/src/tracerun/CtraceRunMeta.h index 584efdbc7..02c291680 100644 --- a/tools/ctrace/src/tracerun/CtraceRunMeta.h +++ b/tools/ctrace/src/tracerun/CtraceRunMeta.h @@ -8,6 +8,8 @@ #ifndef CTRACE_SRC_TRACERUN_CTRACERUNMETA_H #define CTRACE_SRC_TRACERUN_CTRACERUNMETA_H +#include "TraceRoute.h" + #include #include #include @@ -23,7 +25,7 @@ enum class TraceRunFormat; struct CtraceRunSourceMeta { std::string type; std::optional processorName; - std::uint8_t traceBusId = 0U; + TraceRouteIdentity route; std::uint32_t source = 0; std::optional label; std::optional address; @@ -71,7 +73,7 @@ struct CtraceRunReferenceDiagnostic { /** @brief Describes one normalized protocol route and its processor metadata. */ struct CtraceRunRoute { CtraceRunProtocol protocol = CtraceRunProtocol::Itm; - std::optional traceBusId; + TraceRouteIdentity identity; std::optional processorName; bool timestampsConfigured = false; std::optional timestampClockHz; diff --git a/tools/ctrace/test/unit/src/control/DecodeConsumersTests.cpp b/tools/ctrace/test/unit/src/control/DecodeConsumersTests.cpp index 3da63b9b3..2adef00bc 100644 --- a/tools/ctrace/test/unit/src/control/DecodeConsumersTests.cpp +++ b/tools/ctrace/test/unit/src/control/DecodeConsumersTests.cpp @@ -11,6 +11,7 @@ #include "DiagnosticSink.h" #include "TraceEvent.h" #include "TraceOutput.h" +#include "TraceRoute.h" #include @@ -77,7 +78,8 @@ TEST(CtraceUnitTests, testDecodeConsumersWarnsForDisabledItmChannelsOnce) EXPECT_TRUE(unknownDiagnostics.events().empty()); CollectingDiagnosticSink diagnostics; - DecodeConsumers consumers({}, diagnostics, 0x00000002U, {{2U, 0x00000004U}}); + const TraceRouteIdentity stream2{TraceRouteId{20U}, 2U}; + DecodeConsumers consumers({}, diagnostics, 0x00000002U, {{stream2.id, 0x00000004U}}); auto enabled = softwarePacket(1U); consumers.append(enabled); @@ -92,12 +94,10 @@ TEST(CtraceUnitTests, testDecodeConsumersWarnsForDisabledItmChannelsOnce) auto invalidChannel = softwarePacket(32U); consumers.append(invalidChannel); - auto streamSpecificDisabled = softwarePacket(1U); - streamSpecificDisabled.traceBusId = 2U; + auto streamSpecificDisabled = onRoute(softwarePacket(1U), stream2); consumers.append(streamSpecificDisabled); - auto streamSpecificEnabled = softwarePacket(2U); - streamSpecificEnabled.traceBusId = 2U; + auto streamSpecificEnabled = onRoute(softwarePacket(2U), stream2); consumers.append(streamSpecificEnabled); ASSERT_EQ(2U, diagnostics.events().size()); @@ -106,3 +106,24 @@ TEST(CtraceUnitTests, testDecodeConsumersWarnsForDisabledItmChannelsOnce) EXPECT_EQ(DiagnosticSink::Impact::NonFailing, event.impact); } } + +TEST(CtraceUnitTests, testDecodeConsumersTracksEnableWarningsByInternalRouteIdentity) +{ + CollectingDiagnosticSink diagnostics; + const TraceRouteIdentity noBusA{TraceRouteId{30U}, std::nullopt}; + const TraceRouteIdentity noBusB{TraceRouteId{31U}, std::nullopt}; + DecodeConsumers consumers({}, diagnostics, std::nullopt, {{noBusA.id, 0U}, {noBusB.id, 0U}}); + + const auto disabledA = onRoute(softwarePacket(3U), noBusA); + const auto disabledB = onRoute(softwarePacket(3U), noBusB); + consumers.append(disabledA); + consumers.append(disabledA); + consumers.append(disabledB); + consumers.append(disabledB); + + ASSERT_EQ(diagnostics.events().size(), 2U) << "warning-once state must be independent for distinct no-bus route IDs"; + EXPECT_TRUE(diagnostics.events()[0].context.size() == 2U && diagnostics.events()[1].context.size() == 2U) + << "an internal route ordinal must not be exposed as public stream context"; + EXPECT_EQ(diagnostics.events()[0].context.front(), (std::pair{"channel", "3"})); + EXPECT_EQ(diagnostics.events()[1].context.front(), (std::pair{"channel", "3"})); +} diff --git a/tools/ctrace/test/unit/src/decode/CortexMPostDecoderTests.cpp b/tools/ctrace/test/unit/src/decode/CortexMPostDecoderTests.cpp index 95b1edef3..420885ef8 100644 --- a/tools/ctrace/test/unit/src/decode/CortexMPostDecoderTests.cpp +++ b/tools/ctrace/test/unit/src/decode/CortexMPostDecoderTests.cpp @@ -31,7 +31,7 @@ TEST(CtraceUnitTests, testCortexMPostDecoderUsesLocalTimestampRelations) for (const auto& [relation, expectedReliable] : cases) { CollectingEventSink sink; - CortexMPostDecoder decoder(sink); + CortexMPostDecoder decoder({}, sink); decoder.append(openCsdSoftwareElement(1U)); @@ -48,7 +48,7 @@ TEST(CtraceUnitTests, testCortexMPostDecoderUsesLocalTimestampRelations) TEST(CtraceUnitTests, testCortexMPostDecoderOffsetsTimestampsAfterOverflow) { CollectingEventSink sink; - CortexMPostDecoder decoder(sink); + CortexMPostDecoder decoder({}, sink); decoder.append(openCsdTimestampElement(100U)); @@ -66,7 +66,7 @@ TEST(CtraceUnitTests, testCortexMPostDecoderOffsetsTimestampsAfterOverflow) TEST(CtraceUnitTests, testCortexMPostDecoderLeavesInitialOverflowTimestampUnknown) { CollectingEventSink sink; - CortexMPostDecoder decoder(sink); + CortexMPostDecoder decoder({}, sink); decoder.append(openCsdElement(OpenCsdTraceElement::Kind::Overflow)); @@ -77,7 +77,7 @@ TEST(CtraceUnitTests, testCortexMPostDecoderLeavesInitialOverflowTimestampUnknow TEST(CtraceUnitTests, testCortexMPostDecoderUsesTimestampOverflowFlag) { CollectingEventSink sink; - CortexMPostDecoder decoder(sink); + CortexMPostDecoder decoder({}, sink); decoder.append(openCsdTimestampElement(100U)); @@ -92,7 +92,7 @@ TEST(CtraceUnitTests, testCortexMPostDecoderUsesTimestampOverflowFlag) TEST(CtraceUnitTests, testCortexMPostDecoderMapsDiscontinuityTimestamp) { CollectingEventSink sink; - CortexMPostDecoder decoder(sink); + CortexMPostDecoder decoder({}, sink); decoder.append(openCsdTimestampElement(200U)); decoder.append(openCsdElement(OpenCsdTraceElement::Kind::Discontinuity)); @@ -105,7 +105,8 @@ TEST(CtraceUnitTests, testCortexMPostDecoderMapsDiscontinuityTimestamp) TEST(CtraceUnitTests, testCortexMPostDecoderLabelsPmuTraceOnOverflowPacket) { CollectingEventSink sink; - CortexMPostDecoder decoder(sink); + const TraceRouteIdentity route{TraceRouteId{2U}, 5U}; + CortexMPostDecoder decoder(route, sink); decoder.append(openCsdTimestampElement(100U, 10U, 5U)); auto pmuElement = openCsdElement(OpenCsdTraceElement::Kind::Hardware, 24U, 5U); @@ -120,7 +121,7 @@ TEST(CtraceUnitTests, testCortexMPostDecoderLabelsPmuTraceOnOverflowPacket) const auto* pmu = traceEventPayload(packet); ASSERT_TRUE(pmu != nullptr && pmu->overflowMask == 0x81U) << "post-decoder must label discriminator 3 as a PMU trace-on-overflow event"; - ASSERT_TRUE(packet.index == 24U && packet.traceBusId == 5U && packet.tcyc == 100U) + ASSERT_TRUE(packet.index == 24U && packet.route == route && packet.tcyc == 100U) << "post-decoder PMU event context mismatch"; ASSERT_TRUE(packet.quality.has_value() && packet.quality->timestampReliable) << "post-decoder PMU timestamp quality mismatch"; diff --git a/tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp b/tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp index cbb9841d4..d8f7a6556 100644 --- a/tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp +++ b/tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp @@ -15,6 +15,7 @@ #include "CortexMStreamDecoder.h" #include "DecodePipeline.h" #include "OpenCsdTraceElement.h" +#include "SaturatingArithmetic.h" #include "TraceEvent.h" #include @@ -98,11 +99,24 @@ struct DecodedTrace { std::vector events; }; +/** @brief Creates the configured synthetic route for the current SINGLE frontend. */ +static CortexMDecodeRoute singleDecodeRoute(std::uint32_t timestampPrescaler = 1U) +{ + return {TraceRouteIdentity{}, timestampPrescaler}; +} + +/** @brief Assigns an exact normalized route to an OpenCSD test element. */ +static OpenCsdTraceElement onDecodeRoute(OpenCsdTraceElement element, TraceRouteIdentity route) +{ + element.route = route; + return element; +} + /** @brief Decodes test chunks and returns counters and collected events. */ static DecodedTrace decodeTrace(std::initializer_list chunks, std::uint32_t timestampPrescaler = 16U) { CollectingEventSink sink; - DecodePipeline pipeline(ItmTimestampPrescalers{timestampPrescaler, {}}, sink); + DecodePipeline pipeline(singleDecodeRoute(timestampPrescaler), sink); for (const auto chunk : chunks) { pipeline.push(chunk); } @@ -112,7 +126,7 @@ static DecodedTrace decodeTrace(std::initializer_list chunks, std:: TEST(CtraceUnitTests, testCortexMPostDecoderSoftwareTimestampBoundary) { CollectingEventSink sink; - CortexMPostDecoder decoder(sink); + CortexMPostDecoder decoder({}, sink); decoder.append(openCsdSoftwareElement(0U, 'A', 4U, 1U)); decoder.append(openCsdTimestampElement(120U, 5U, 1U)); @@ -137,25 +151,31 @@ TEST(CtraceUnitTests, testCortexMPostDecoderSoftwareTimestampBoundary) TEST(CtraceUnitTests, testCortexMStreamDecoderAppliesPerStreamPrescalers) { CollectingEventSink sink; - CortexMStreamDecoder decoder(ItmTimestampPrescalers{1U, {{1U, 4U}, {2U, 16U}}}, sink); + const TraceRouteIdentity route1{TraceRouteId{0U}, 1U}; + const TraceRouteIdentity route2{TraceRouteId{1U}, 2U}; + CortexMStreamDecoder decoder({{route1, 4U}, {route2, 16U}}, sink); - const auto stream1Software = openCsdSoftwareElement(1U, 0x11U, 0U, 1U); + auto stream1Software = openCsdSoftwareElement(1U, 0x11U); + stream1Software.route = route1; decoder.append(stream1Software); - const auto stream2Software = openCsdSoftwareElement(1U, 0x22U, 0U, 2U); + auto stream2Software = openCsdSoftwareElement(1U, 0x22U); + stream2Software.route = route2; decoder.append(stream2Software); - const auto stream1Timestamp = openCsdTimestampElement(10U, 0U, 1U); + auto stream1Timestamp = openCsdTimestampElement(10U); + stream1Timestamp.route = route1; decoder.append(stream1Timestamp); - const auto stream2Timestamp = openCsdTimestampElement(10U, 0U, 2U); + auto stream2Timestamp = openCsdTimestampElement(10U); + stream2Timestamp.route = route2; decoder.append(stream2Timestamp); decoder.finish(); ASSERT_TRUE(sink.events().size() == 4U) << "per-stream timestamp event count mismatch"; - ASSERT_TRUE(sink.events()[0].traceBusId == 1U && sink.events()[0].tcyc == std::optional(40U)) + ASSERT_TRUE(sink.events()[0].route == route1 && sink.events()[0].tcyc == std::optional(40U)) << "stream 1 timestamp prescaler mismatch"; - ASSERT_TRUE(sink.events()[2].traceBusId == 2U && sink.events()[2].tcyc == std::optional(160U)) + ASSERT_TRUE(sink.events()[2].route == route2 && sink.events()[2].tcyc == std::optional(160U)) << "stream 2 timestamp prescaler mismatch"; } @@ -164,29 +184,180 @@ TEST(CtraceUnitTests, testCortexMStreamDecoderValidatesAndSaturatesPrescalers) CollectingEventSink sink; auto timestamp = openCsdTimestampElement(std::numeric_limits::max()); - CortexMStreamDecoder saturating(ItmTimestampPrescalers{2U, {}}, sink); + CortexMStreamDecoder saturating({singleDecodeRoute(2U)}, sink); saturating.append(timestamp); saturating.finish(); ASSERT_EQ(sink.events().size(), 1U); EXPECT_EQ(sink.events().front().tcyc, std::numeric_limits::max()); EXPECT_EQ(saturating.eventCount(), 1U); - CortexMStreamDecoder zero(ItmTimestampPrescalers{0U, {}}, sink); - EXPECT_THROW(zero.append(timestamp), std::invalid_argument); - - CortexMStreamDecoder unresolved(ItmTimestampPrescalers{std::nullopt, {}}, sink); - EXPECT_THROW(unresolved.append(timestamp), std::runtime_error); - timestamp.traceBusId = 7U; - EXPECT_THROW(unresolved.append(timestamp), std::runtime_error); + EXPECT_THROW((void)CortexMStreamDecoder({}, sink), std::invalid_argument); + EXPECT_THROW((void)CortexMStreamDecoder({singleDecodeRoute(0U)}, sink), std::invalid_argument); + EXPECT_THROW((void)SaturatingArithmetic::multiply(1U, 0U), std::invalid_argument); + EXPECT_THROW((void)CortexMStreamDecoder({{{TraceRouteId{0U}, 0U}, 1U}}, sink), std::invalid_argument); + EXPECT_THROW((void)CortexMStreamDecoder({{{TraceRouteId{0U}, 112U}, 1U}}, sink), std::invalid_argument); + EXPECT_THROW((void)CortexMStreamDecoder({{{TraceRouteId{0U}, 1U}, 1U}, {{TraceRouteId{1U}, 1U}, 1U}}, sink), + std::invalid_argument); + EXPECT_THROW((void)CortexMStreamDecoder({{{TraceRouteId{0U}, 1U}, 1U}, {{TraceRouteId{0U}, 2U}, 1U}}, sink), + std::invalid_argument); + + auto unknown = timestamp; + unknown.route = {TraceRouteId{9U}, 9U}; + EXPECT_THROW(saturating.append(unknown), std::runtime_error); + auto mismatched = timestamp; + mismatched.route = {TraceRouteId{0U}, 7U}; + EXPECT_THROW(saturating.append(mismatched), std::runtime_error); const auto timestampWithoutValue = openCsdElement(OpenCsdTraceElement::Kind::LocalTimestamp); EXPECT_NO_THROW(saturating.append(timestampWithoutValue)); } +TEST(CtraceUnitTests, testCortexMStreamDecoderIsolatesInterleavedDwtAndQualityState) +{ + CollectingEventSink sink; + const TraceRouteIdentity route1{TraceRouteId{0U}, 1U}; + const TraceRouteIdentity route2{TraceRouteId{1U}, 2U}; + CortexMStreamDecoder decoder({{route1, 4U}, {route2, 16U}}, sink); + + decoder.append(onDecodeRoute(openCsdTimestampElement(10U), route1)); + decoder.append(onDecodeRoute(openCsdTimestampElement(10U), route2)); + + auto route1Pc = onDecodeRoute(openCsdElement(OpenCsdTraceElement::Kind::Hardware, 10U), route1); + route1Pc.discriminator = 8U; + route1Pc.size = 4U; + route1Pc.value = 0x08001000U; + decoder.append(route1Pc); + auto route2Pc = onDecodeRoute(openCsdElement(OpenCsdTraceElement::Kind::Hardware, 11U), route2); + route2Pc.discriminator = 8U; + route2Pc.size = 4U; + route2Pc.value = 0x08002000U; + decoder.append(route2Pc); + + auto route1Value = onDecodeRoute(openCsdElement(OpenCsdTraceElement::Kind::Hardware, 12U), route1); + route1Value.discriminator = 16U; + route1Value.size = 1U; + route1Value.value = 0U; + decoder.append(route1Value); + auto route2Value = onDecodeRoute(openCsdElement(OpenCsdTraceElement::Kind::Hardware, 13U), route2); + route2Value.discriminator = 16U; + route2Value.size = 2U; + route2Value.value = 0U; + decoder.append(route2Value); + + decoder.append(onDecodeRoute(openCsdTimestampElement(20U), route1)); + decoder.append(onDecodeRoute(openCsdElement(OpenCsdTraceElement::Kind::Overflow, 14U), route1)); + decoder.append(onDecodeRoute(openCsdTimestampElement(20U), route2)); + decoder.finish(); + + std::vector dataEvents; + const TraceEvent* route1Overflow = nullptr; + for (const auto& event : sink.events()) { + if (isTraceEvent(event)) { + dataEvents.push_back(&event); + } + if (event.route == route1 && isTraceEvent(event)) { + route1Overflow = &event; + } + } + ASSERT_EQ(dataEvents.size(), 2U); + const auto* firstData = traceEventPayload(*dataEvents[0]); + const auto* secondData = traceEventPayload(*dataEvents[1]); + ASSERT_NE(firstData, nullptr); + ASSERT_NE(secondData, nullptr); + EXPECT_EQ(dataEvents[0]->route, route1); + EXPECT_EQ(dataEvents[0]->tcyc, 80U); + EXPECT_EQ(firstData->size, 1U); + EXPECT_EQ(firstData->value, 0U); + EXPECT_EQ(firstData->pc, std::optional(DwtAddressFragment{4U, 0x08001000U})); + EXPECT_EQ(dataEvents[1]->route, route2); + EXPECT_EQ(dataEvents[1]->tcyc, 320U); + EXPECT_EQ(secondData->size, 2U); + EXPECT_EQ(secondData->value, 0U); + EXPECT_EQ(secondData->pc, std::optional(DwtAddressFragment{4U, 0x08002000U})); + ASSERT_NE(route1Overflow, nullptr); + ASSERT_TRUE(route1Overflow->quality.has_value()); + EXPECT_EQ(route1Overflow->quality->overflowCount, 1U); + ASSERT_TRUE(dataEvents[1]->quality.has_value()); + EXPECT_EQ(dataEvents[1]->quality->overflowCount, 0U); + EXPECT_TRUE(dataEvents[1]->quality->timestampReliable); +} + +TEST(CtraceUnitTests, testCortexMStreamDecoderKeepsTwoNoBusRoutesIndependent) +{ + CollectingEventSink sink; + const TraceRouteIdentity firstRoute{TraceRouteId{4U}, std::nullopt}; + const TraceRouteIdentity secondRoute{TraceRouteId{9U}, std::nullopt}; + CortexMStreamDecoder decoder({{firstRoute, 2U}, {secondRoute, 3U}}, sink); + + decoder.append(onDecodeRoute(openCsdTimestampElement(10U), firstRoute)); + decoder.append(onDecodeRoute(openCsdTimestampElement(10U), secondRoute)); + + auto firstPc = onDecodeRoute(openCsdElement(OpenCsdTraceElement::Kind::Hardware, 10U), firstRoute); + firstPc.discriminator = 8U; + firstPc.size = 4U; + firstPc.value = 0x08001000U; + decoder.append(firstPc); + auto secondPc = onDecodeRoute(openCsdElement(OpenCsdTraceElement::Kind::Hardware, 11U), secondRoute); + secondPc.discriminator = 8U; + secondPc.size = 4U; + secondPc.value = 0x08002000U; + decoder.append(secondPc); + + auto firstValue = onDecodeRoute(openCsdElement(OpenCsdTraceElement::Kind::Hardware, 12U), firstRoute); + firstValue.discriminator = 16U; + firstValue.size = 1U; + firstValue.value = 0x11U; + decoder.append(firstValue); + auto secondValue = onDecodeRoute(openCsdElement(OpenCsdTraceElement::Kind::Hardware, 13U), secondRoute); + secondValue.discriminator = 16U; + secondValue.size = 2U; + secondValue.value = 0x2222U; + decoder.append(secondValue); + + decoder.append(onDecodeRoute(openCsdTimestampElement(20U), firstRoute)); + decoder.append(onDecodeRoute(openCsdElement(OpenCsdTraceElement::Kind::Overflow, 14U), firstRoute)); + decoder.append(onDecodeRoute(openCsdTimestampElement(30U), secondRoute)); + decoder.finish(); + + const TraceEvent* firstDataEvent = nullptr; + const TraceEvent* secondDataEvent = nullptr; + const TraceEvent* firstOverflow = nullptr; + for (const auto& event : sink.events()) { + if (event.route == firstRoute && isTraceEvent(event)) { + firstDataEvent = &event; + } else if (event.route == secondRoute && isTraceEvent(event)) { + secondDataEvent = &event; + } else if (event.route == firstRoute && isTraceEvent(event)) { + firstOverflow = &event; + } + } + + ASSERT_NE(firstDataEvent, nullptr); + ASSERT_NE(secondDataEvent, nullptr); + ASSERT_NE(firstOverflow, nullptr); + const auto* firstData = traceEventPayload(*firstDataEvent); + const auto* secondData = traceEventPayload(*secondDataEvent); + ASSERT_NE(firstData, nullptr); + ASSERT_NE(secondData, nullptr); + EXPECT_EQ(firstDataEvent->tcyc, 40U); + EXPECT_EQ(firstData->value, 0x11U); + EXPECT_EQ(firstData->pc, std::optional(DwtAddressFragment{4U, 0x08001000U})); + EXPECT_EQ(secondDataEvent->tcyc, 90U); + EXPECT_EQ(secondData->value, 0x2222U); + EXPECT_EQ(secondData->pc, std::optional(DwtAddressFragment{4U, 0x08002000U})); + ASSERT_TRUE(firstOverflow->quality.has_value()); + EXPECT_EQ(firstOverflow->quality->overflowCount, 1U); + ASSERT_TRUE(secondDataEvent->quality.has_value()); + EXPECT_EQ(secondDataEvent->quality->overflowCount, 0U); + EXPECT_TRUE(secondDataEvent->quality->timestampReliable); + EXPECT_FALSE(firstDataEvent->route.traceBusId.has_value()); + EXPECT_FALSE(secondDataEvent->route.traceBusId.has_value()); +} + TEST(CtraceUnitTests, testDecodePipelineRejectsInvalidChunkSizes) { CollectingEventSink sink; - DecodePipeline pipeline(ItmTimestampPrescalers{1U, {}}, sink); + DecodePipeline pipeline(singleDecodeRoute(), sink); EXPECT_NO_THROW(pipeline.push({nullptr, 0U})); EXPECT_THROW(pipeline.push({nullptr, static_cast(std::numeric_limits::max()) + 1U}), std::runtime_error); @@ -198,7 +369,7 @@ TEST(CtraceUnitTests, testDecodePipelineRejectsInvalidChunkSizes) TEST(CtraceUnitTests, testCortexMPostDecoderReportsDiscontinuityInterval) { CollectingEventSink sink; - CortexMPostDecoder decoder(sink); + CortexMPostDecoder decoder({}, sink); auto discontinuity = openCsdElement(OpenCsdTraceElement::Kind::Discontinuity); discontinuity.issueCode = TraceIssueCode::DataLoss; @@ -227,7 +398,7 @@ TEST(CtraceUnitTests, testCortexMPostDecoderReportsDiscontinuityInterval) TEST(CtraceUnitTests, testCortexMPostDecoderSeparatesRecoveryCauseAndDataLoss) { CollectingEventSink sink; - CortexMPostDecoder decoder(sink); + CortexMPostDecoder decoder({}, sink); auto cause = openCsdElement(OpenCsdTraceElement::Kind::Error, 8U); cause.discontinuity = true; @@ -266,7 +437,7 @@ TEST(CtraceUnitTests, testCortexMPostDecoderSeparatesRecoveryCauseAndDataLoss) TEST(CtraceUnitTests, testCortexMPostDecoderOverflowFlushesDwtSegments) { CollectingEventSink sink; - CortexMPostDecoder decoder(sink); + CortexMPostDecoder decoder({}, sink); const auto firstTimestamp = openCsdTimestampElement(100U, 1U, 1U); decoder.append(firstTimestamp); @@ -320,7 +491,7 @@ TEST(CtraceUnitTests, testCortexMPostDecoderOverflowFlushesDwtSegments) TEST(CtraceUnitTests, testCortexMPostDecoderPreservesDecoderTimestamps) { CollectingEventSink sink; - CortexMPostDecoder decoder(sink); + CortexMPostDecoder decoder({}, sink); const auto firstTimestamp = openCsdTimestampElement(100U, (std::uint64_t{1} << 32U) + 1U); decoder.append(firstTimestamp); @@ -339,7 +510,7 @@ TEST(CtraceUnitTests, testCortexMPostDecoderPreservesDecoderTimestamps) TEST(CtraceUnitTests, testCortexMPostDecoderPreservesGlobalTimestampOrder) { CollectingEventSink sink; - CortexMPostDecoder decoder(sink); + CortexMPostDecoder decoder({}, sink); const auto software = openCsdSoftwareElement(1U, 'A', 1U); decoder.append(software); diff --git a/tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp b/tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp index 307c8527f..dc7445ed2 100644 --- a/tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp +++ b/tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp @@ -24,7 +24,10 @@ static DwtPayloadPacket dwtPayload(std::uint8_t discriminator, std::uint8_t size = 0U, std::uint32_t value = 0U, std::uint64_t index = 0U, std::uint8_t traceBusId = 0U, std::uint64_t tcyc = 0U) { - return {index, traceBusId, discriminator, size, value, tcyc, {}}; + const auto route = traceBusId == 0U + ? TraceRouteIdentity{} + : TraceRouteIdentity{TraceRouteId{traceBusId}, std::optional(traceBusId)}; + return {index, route, discriminator, size, value, tcyc, {}}; } TEST(CtraceUnitTests, testDwtPcSampleProducesDedicatedEvent) @@ -40,7 +43,7 @@ TEST(CtraceUnitTests, testDwtPcSampleProducesDedicatedEvent) EXPECT_EQ(sample->pc, 0x08001234U) << "DWT PC sample payload mismatch"; EXPECT_FALSE(sample->sleeping) << "DWT PC sample payload mismatch"; EXPECT_EQ(packets.front().index, 19U) << "DWT PC sample identity mismatch"; - EXPECT_EQ(packets.front().traceBusId, 3U) << "DWT PC sample identity mismatch"; + EXPECT_EQ(packets.front().route.traceBusId, 3U) << "DWT PC sample identity mismatch"; EXPECT_EQ(packets.front().tcyc, std::optional(949339000U)) << "DWT PC sample identity mismatch"; ASSERT_TRUE(packets.front().quality.has_value()) << "DWT PC sample quality mismatch"; @@ -95,7 +98,7 @@ TEST(CtraceUnitTests, testDwtEventCounterPacketIsValidatedAndExposed) ASSERT_NE(event, nullptr); EXPECT_EQ(event->counterMask, 0x21U); EXPECT_EQ(packet.index, 23U); - EXPECT_EQ(packet.traceBusId, 4U); + EXPECT_EQ(packet.route.traceBusId, 4U); EXPECT_EQ(packet.tcyc, std::optional(949339100U)); ASSERT_TRUE(packet.quality.has_value()); EXPECT_TRUE(packet.quality->overflow); @@ -146,7 +149,7 @@ TEST(CtraceUnitTests, testDwtPmuPacketRejectsUnsupportedPayloads) EXPECT_EQ(issue->code, TraceIssueCode::UnsupportedPmuEventCounterPayload); EXPECT_NE(issue->message.find("expected a non-zero 1-byte mask using bits 0..7"), std::string::npos); EXPECT_EQ(packets.front().index, 23U); - EXPECT_EQ(packets.front().traceBusId, 4U); + EXPECT_EQ(packets.front().route.traceBusId, 4U); EXPECT_EQ(packets.front().tcyc, std::optional(949339100U)); EXPECT_TRUE(packets.front().quality.has_value()); EXPECT_EQ(traceEventType(packets.front()), TraceEventType::Error); @@ -181,7 +184,7 @@ TEST(CtraceUnitTests, testDwtEventCounterRejectsUnsupportedPayloadsWithoutPartia EXPECT_EQ(issue->code, TraceIssueCode::UnsupportedDwtEventCounterPayload); EXPECT_NE(issue->message.find("expected a non-zero 1-byte mask using bits 0..5 only"), std::string::npos); EXPECT_EQ(packets.front().index, 23U); - EXPECT_EQ(packets.front().traceBusId, 4U); + EXPECT_EQ(packets.front().route.traceBusId, 4U); EXPECT_EQ(packets.front().tcyc, std::optional(949339100U)); EXPECT_TRUE(packets.front().quality.has_value()); EXPECT_EQ(traceEventType(packets.front()), TraceEventType::Error); @@ -204,7 +207,7 @@ TEST(CtraceUnitTests, testDwtPacketDecoderRejectsReservedExceptionAction) << "DwtPacketDecoder reserved exception action code mismatch"; ASSERT_TRUE(issue->message == "invalid exception action 0x0 for exception 11") << "DwtPacketDecoder reserved exception action message mismatch"; - ASSERT_TRUE(packets[0].index == 17U && packets[0].traceBusId == 3U) + ASSERT_TRUE(packets[0].index == 17U && packets[0].route.traceBusId == 3U) << "DwtPacketDecoder reserved exception action identity mismatch"; ASSERT_TRUE(packets[0].tcyc.has_value() && *packets[0].tcyc == 1234U) << "DwtPacketDecoder reserved exception action timestamp mismatch"; @@ -230,7 +233,8 @@ TEST(CtraceUnitTests, testDwtPacketDecoderFlushesPendingEventsInRawOrder) ASSERT_TRUE(packets.size() == 3U) << "DWT flush must emit all pending comparator events"; ASSERT_TRUE(packets[0].index == 10U && packets[1].index == 10U && packets[2].index == 20U) << "DWT flush must preserve raw-stream order across comparators"; - ASSERT_TRUE(packets[0].traceBusId == 3U && packets[1].traceBusId == 3U && packets[2].traceBusId == 4U) + ASSERT_TRUE(packets[0].route.traceBusId == 3U && packets[1].route.traceBusId == 3U && + packets[2].route.traceBusId == 4U) << "DWT flush must preserve the identity of each pending event"; const auto* first = traceEventPayload(packets[0]); const auto* second = traceEventPayload(packets[1]); @@ -251,7 +255,7 @@ TEST(CtraceUnitTests, testDwtPacketDecoderPreservesRepeatedAddressFragments) auto packets = decoder.decode(dwtPayload(discriminator, size, secondValue, 20U, 4U, 200U)); ASSERT_TRUE(packets.size() == 1U) << "a repeated DWT address fragment must flush its predecessor"; const auto* firstAddress = traceEventPayload(packets.front()); - ASSERT_TRUE(firstAddress != nullptr && packets.front().index == 10U && packets.front().traceBusId == 3U) + ASSERT_TRUE(firstAddress != nullptr && packets.front().index == 10U && packets.front().route.traceBusId == 3U) << "the first repeated DWT address fragment lost its identity"; const auto firstPc = dwtAddressPc(*firstAddress); const auto firstDataAddress = dwtDataAddress(*firstAddress); @@ -259,7 +263,7 @@ TEST(CtraceUnitTests, testDwtPacketDecoderPreservesRepeatedAddressFragments) packets = decoder.flush({}, 300U); ASSERT_TRUE(packets.size() == 1U) << "the second DWT address fragment must remain available"; const auto* secondAddress = traceEventPayload(packets.front()); - ASSERT_TRUE(secondAddress != nullptr && packets.front().index == 20U && packets.front().traceBusId == 4U) + ASSERT_TRUE(secondAddress != nullptr && packets.front().index == 20U && packets.front().route.traceBusId == 4U) << "the second repeated DWT address fragment lost its identity"; if (discriminator == 8U) { @@ -291,7 +295,7 @@ TEST(CtraceUnitTests, testDwtPacketDecoderEmitsComparatorOnlyMatch) ASSERT_NE(match, nullptr); EXPECT_EQ(match->comparator, 2U); EXPECT_EQ(packets.front().index, 17U); - EXPECT_EQ(packets.front().traceBusId, 3U); + EXPECT_EQ(packets.front().route.traceBusId, 3U); EXPECT_EQ(packets.front().tcyc, std::optional(99U)); ASSERT_TRUE(packets.front().quality.has_value()); EXPECT_TRUE(packets.front().quality->overflow); @@ -334,7 +338,7 @@ TEST(CtraceUnitTests, testDwtPacketDecoderRejectsUnsupportedAddressWidths) ASSERT_TRUE(issue != nullptr && issue->code == TraceIssueCode::UnsupportedDwtAddressPayload && issue->severity == TraceIssueSeverity::Error) << "unsupported DWT address width diagnostic mismatch"; - ASSERT_TRUE(packets.front().index == 17U && packets.front().traceBusId == 3U && + ASSERT_TRUE(packets.front().index == 17U && packets.front().route.traceBusId == 3U && packets.front().tcyc == std::optional(99U)) << "unsupported DWT address width diagnostic lost packet identity"; }; diff --git a/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp b/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp index 4d25e105c..c45248640 100644 --- a/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp +++ b/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp @@ -30,7 +30,7 @@ using OpenCsdTestSupport::CollectingOpenCsdElementSink; TEST(CtraceUnitTests, testOpenCsdItmDecoderConstructsDefaultSession) { CollectingOpenCsdElementSink sink; - OpenCsdItmDecoder decoder(sink); + OpenCsdItmDecoder decoder({}, sink); EXPECT_EQ(decoder.finish().bytesIn, 0U); EXPECT_FALSE(sink.hasIssue(TraceIssueCode::OpenCsdInitializationError)); @@ -231,7 +231,7 @@ TEST(CtraceUnitTests, testOpenCsdItmDecoderReportsResetAndInitializationFailures [](OpenCsdPacketCollector&, OpenCsdErrorController&) -> std::unique_ptr { return nullptr; }; - EXPECT_THROW((void)OpenCsdItmDecoder(nullSink, nullFactory), OpenCsdFatalError); + EXPECT_THROW((void)OpenCsdItmDecoder({}, nullSink, nullFactory), OpenCsdFatalError); EXPECT_TRUE(nullSink.hasIssue(TraceIssueCode::OpenCsdInitializationError)); CollectingOpenCsdElementSink errorSink; @@ -239,14 +239,14 @@ TEST(CtraceUnitTests, testOpenCsdItmDecoderReportsResetAndInitializationFailures [](OpenCsdPacketCollector&, OpenCsdErrorController&) -> std::unique_ptr { throw OpenCsdItmSessionError("synthetic session setup failure"); }; - EXPECT_THROW((void)OpenCsdItmDecoder(errorSink, errorFactory), OpenCsdFatalError); + EXPECT_THROW((void)OpenCsdItmDecoder({}, errorSink, errorFactory), OpenCsdFatalError); EXPECT_TRUE(errorSink.hasIssue(TraceIssueCode::OpenCsdInitializationError)); } TEST(CtraceUnitTests, testOpenCsdItmSessionAcceptsEmptyDataPathOperations) { CollectingOpenCsdElementSink sink; - OpenCsdPacketCollector collector(sink); + OpenCsdPacketCollector collector({}, sink); OpenCsdErrorController errors; OpenCsdItmSession session(collector, errors); @@ -272,7 +272,7 @@ TEST(CtraceUnitTests, testOpenCsdSessionValidationRejectsInvalidApiResults) TEST(CtraceUnitTests, testOpenCsdItmSessionRejectsMissingDecoderRegistry) { CollectingOpenCsdElementSink sink; - OpenCsdPacketCollector collector(sink); + OpenCsdPacketCollector collector({}, sink); OpenCsdErrorController errors; const auto missingRegistry = []() -> OcsdLibDcdRegister* { return nullptr; }; diff --git a/tools/ctrace/test/unit/src/decode/OpenCsdPacketCollectorTests.cpp b/tools/ctrace/test/unit/src/decode/OpenCsdPacketCollectorTests.cpp index 6e93be712..a1520ea04 100644 --- a/tools/ctrace/test/unit/src/decode/OpenCsdPacketCollectorTests.cpp +++ b/tools/ctrace/test/unit/src/decode/OpenCsdPacketCollectorTests.cpp @@ -60,7 +60,8 @@ static OcsdTraceElement itmElement(swt_itm_type type, std::uint8_t source = 0U, TEST(CtraceUnitTests, testOpenCsdPacketCollectorUsesReconstructedGlobalTimestamp) { CollectingOpenCsdElementSink sink; - OpenCsdPacketCollector collector(sink); + const TraceRouteIdentity route{TraceRouteId{9U}, 7U}; + OpenCsdPacketCollector collector(route, sink); auto globalTimestamp = itmElement(TS_GLOBAL); globalTimestamp.setTS(0xfedcba9876543210ULL, true); @@ -72,13 +73,13 @@ TEST(CtraceUnitTests, testOpenCsdPacketCollectorUsesReconstructedGlobalTimestamp ASSERT_TRUE(element.kind == OpenCsdTraceElement::Kind::GlobalTimestamp) << "reconstructed global timestamp kind mismatch"; ASSERT_TRUE(element.sourceIndex == 42U) << "reconstructed global timestamp source index mismatch"; - ASSERT_TRUE(element.traceBusId == 7U) << "OpenCSD Trace Bus ID was not preserved"; + ASSERT_EQ(element.route, route) << "bound normalized route was not preserved"; ASSERT_TRUE(element.timestampValue == 0xfedcba9876543210ULL) << "reconstructed global timestamp value mismatch"; ASSERT_TRUE(element.clockChange) << "reconstructed global timestamp clock-change flag missing"; ASSERT_TRUE(collector.TraceElemIn(43U, 0xffU, globalTimestamp) == OCSD_RESP_CONT) << "OpenCSD global timestamp collection with missing source ID failed"; - ASSERT_TRUE(sink.elements().back().traceBusId == 0U) << "an unavailable OpenCSD Trace Bus ID must fall back to zero"; + ASSERT_EQ(sink.elements().back().route, route) << "OpenCSD callback channel must not override the bound route"; ItmTrcPacket rawGts1; rawGts1.setPktType(ITM_PKT_TS_GLOBAL_1); @@ -107,7 +108,7 @@ TEST(CtraceUnitTests, testOpenCsdPacketCollectorMapsLocalTimestampRelations) }}; CollectingOpenCsdElementSink sink; - OpenCsdPacketCollector collector(sink); + OpenCsdPacketCollector collector({}, sink); for (std::size_t index = 0; index < cases.size(); ++index) { auto timestamp = itmElement(cases[index].first); timestamp.setTS(100U + index, false); @@ -127,7 +128,7 @@ TEST(CtraceUnitTests, testOpenCsdPacketCollectorMapsLocalTimestampRelations) TEST(CtraceUnitTests, testOpenCsdPacketCollectorMapsPayloadAndRawPacketKinds) { CollectingOpenCsdElementSink sink; - OpenCsdPacketCollector collector(sink); + OpenCsdPacketCollector collector({}, sink); const auto software = itmElement(SWIT_PAYLOAD, 7U, 4U, 0x12345678U, true); EXPECT_EQ(collector.TraceElemIn(10U, 3U, software), OCSD_RESP_CONT); @@ -192,10 +193,40 @@ TEST(CtraceUnitTests, testOpenCsdPacketCollectorMapsPayloadAndRawPacketKinds) EXPECT_EQ(sink.elements().size(), 6U); } +TEST(CtraceUnitTests, testOpenCsdPacketCollectorNeverDerivesRouteFromCallbackChannelOrPacketKind) +{ + CollectingOpenCsdElementSink sink; + const TraceRouteIdentity route{TraceRouteId{23U}, 11U}; + OpenCsdPacketCollector collector(route, sink); + + auto localTimestamp = itmElement(TS_SYNC); + localTimestamp.setTS(7U, false); + EXPECT_EQ(collector.TraceElemIn(1U, 0U, itmElement(SWIT_PAYLOAD, 1U, 1U, 42U)), OCSD_RESP_CONT); + EXPECT_EQ(collector.TraceElemIn(2U, 7U, itmElement(DWT_PAYLOAD, 2U, 2U, 0x1234U)), OCSD_RESP_CONT); + EXPECT_EQ(collector.TraceElemIn(3U, 0xffU, localTimestamp), OCSD_RESP_CONT); + + ItmTrcPacket packet; + packet.setPktType(ITM_PKT_ASYNC); + collector.RawPacketDataMon(OCSD_OP_DATA, 4U, &packet, 0U, nullptr); + packet.setPktType(ITM_PKT_OVERFLOW); + collector.RawPacketDataMon(OCSD_OP_DATA, 5U, &packet, 0U, nullptr); + packet.setPktType(ITM_PKT_RESERVED); + collector.RawPacketDataMon(OCSD_OP_DATA, 6U, &packet, 0U, nullptr); + + collector.appendDecodeError(7U, "decode"); + collector.prependDiscontinuity(8U, "recovered", TraceIssueCode::DataLoss); + collector.prependDataLossError(9U, "lost", 1U); + + ASSERT_EQ(sink.elements().size(), 9U); + for (const auto& element : sink.elements()) { + EXPECT_EQ(element.route, route) << "callback channel or packet kind replaced the collector's bound route"; + } +} + TEST(CtraceUnitTests, testOpenCsdPacketCollectorTransactionsPreserveOnlyCommittedElements) { CollectingOpenCsdElementSink sink; - OpenCsdPacketCollector collector(sink); + OpenCsdPacketCollector collector({}, sink); EXPECT_NO_THROW(collector.rethrowOutputError()); EXPECT_FALSE(collector.transactionFirstSourceOffset().has_value()); @@ -229,7 +260,7 @@ TEST(CtraceUnitTests, testOpenCsdPacketCollectorTransactionsPreserveOnlyCommitte TEST(CtraceUnitTests, testOpenCsdPacketCollectorDefersOutputFailures) { ThrowingTraceElementSink sink; - OpenCsdPacketCollector collector(sink); + OpenCsdPacketCollector collector({}, sink); const auto software = itmElement(SWIT_PAYLOAD, 1U, 1U, 42U); EXPECT_EQ(collector.TraceElemIn(1U, 1U, software), OCSD_RESP_FATAL_SYS_ERR); diff --git a/tools/ctrace/test/unit/src/diagnostics/DiagnosticsTests.cpp b/tools/ctrace/test/unit/src/diagnostics/DiagnosticsTests.cpp index 710330f67..c83cd7208 100644 --- a/tools/ctrace/test/unit/src/diagnostics/DiagnosticsTests.cpp +++ b/tools/ctrace/test/unit/src/diagnostics/DiagnosticsTests.cpp @@ -10,6 +10,7 @@ #include "DiagnosticSink.h" #include "TraceEvent.h" #include "TraceIssueReporter.h" +#include "TraceRoute.h" #include #include @@ -124,6 +125,39 @@ TEST(CtraceUnitTests, testTraceIssueReporterFormatsUnknownOverflowTimestamp) EXPECT_EQ(diagnostics.events().front().message.find("0 more occurred"), std::string::npos); } +TEST(CtraceUnitTests, testTraceIssueReporterPartitionsIssuesAndOverflowByRoute) +{ + CollectingDiagnosticSink diagnostics; + TraceIssueReporter reporter(diagnostics); + const TraceRouteIdentity firstRoute{TraceRouteId{10U}, 1U}; + const TraceRouteIdentity secondRoute{TraceRouteId{20U}, 111U}; + const TraceRouteIdentity noBusA{TraceRouteId{30U}, std::nullopt}; + const TraceRouteIdentity noBusB{TraceRouteId{31U}, std::nullopt}; + + reporter.append(onRoute(overflowPacket(10U), firstRoute)); + reporter.append(onRoute(overflowPacket(100U), secondRoute)); + reporter.append(onRoute(overflowPacket(20U), firstRoute)); + reporter.append(onRoute(issuePacket(TraceIssueCode::DecodeError), firstRoute)); + reporter.append(onRoute(issuePacket(TraceIssueCode::OpenCsdDecodeError, "route warning", TraceIssueSeverity::Warning), + secondRoute)); + reporter.append(onRoute(overflowPacket(30U), noBusA)); + reporter.append(onRoute(overflowPacket(31U), noBusB)); + reporter.finish(); + + ASSERT_EQ(diagnostics.events().size(), 6U); + EXPECT_EQ(diagnostics.events()[0].context, (std::vector>{{"stream", "1"}})); + EXPECT_EQ(diagnostics.events()[1].context, (std::vector>{{"stream", "111"}})); + EXPECT_EQ(diagnostics.events()[2].context, (std::vector>{{"stream", "1"}})); + EXPECT_NE(diagnostics.events()[2].message.find("cycle timestamp 10; 1 more occurred"), std::string::npos); + EXPECT_EQ(diagnostics.events()[3].context, (std::vector>{{"stream", "111"}})); + EXPECT_NE(diagnostics.events()[3].message.find("cycle timestamp 100"), std::string::npos); + EXPECT_TRUE(diagnostics.events()[4].context.empty()); + EXPECT_TRUE(diagnostics.events()[5].context.empty()); + EXPECT_NE(diagnostics.events()[4].message.find("cycle timestamp 30"), std::string::npos); + EXPECT_NE(diagnostics.events()[5].message.find("cycle timestamp 31"), std::string::npos) + << "distinct no-bus route IDs must not collapse into one overflow summary"; +} + TEST(CtraceUnitTests, testTraceIssueReporterFormatsEveryErrorKind) { CollectingDiagnosticSink diagnostics; diff --git a/tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp b/tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp index 44f3e10ca..8d5c24529 100644 --- a/tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp +++ b/tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp @@ -8,6 +8,7 @@ #include "TestSupport.h" #include #include "TraceEvent.h" +#include "TraceRoute.h" #include "TraceSelection.h" #include "TraceStreamId.h" #include "TraceRunConfig.h" @@ -33,7 +34,11 @@ TEST(CtraceUnitTests, testTraceSelection) itmReference.ctraceRef = "itm"; EXPECT_TRUE(TraceRunSchema::isProcessorItmReference(itmReference)); TraceEvent itm = softwarePacket(1U); - itm.traceBusId = 1; + const TraceRouteIdentity stream1{TraceRouteId{17U}, 1U}; + const TraceRouteIdentity stream111{TraceRouteId{23U}, 111U}; + const TraceRouteIdentity unformattedA{TraceRouteId{41U}, std::nullopt}; + const TraceRouteIdentity unformattedB{TraceRouteId{42U}, std::nullopt}; + itm.route = stream1; ASSERT_TRUE(traceEventSelectedForOutput(itm, TraceSelection{{"itm"}, {}})) << "TraceSelection ITM type mismatch"; ASSERT_TRUE(!traceEventSelectedForOutput(itm, TraceSelection{{"dwt"}, {}})) << "TraceSelection should reject unrelated DWT type"; @@ -45,13 +50,27 @@ TEST(CtraceUnitTests, testTraceSelection) ASSERT_TRUE(!traceEventSelectedForOutput(itm, TraceSelection{{"itm"}, {2U}})) << "TraceSelection combined stream mismatch"; - itm.traceBusId = 0U; + itm.route = unformattedA; ASSERT_TRUE(traceEventSelectedForOutput(itm, TraceSelection{{"itm"}, {}})) - << "TraceSelection type selector must retain Trace Bus ID 0 input"; + << "TraceSelection type selector must retain input without a Trace Bus ID"; ASSERT_TRUE(!traceEventSelectedForOutput(itm, TraceSelection{{}, {1U}})) - << "a non-zero stream selector must not match Trace Bus ID 0 input"; + << "a non-zero stream selector must not match input without a Trace Bus ID"; ASSERT_TRUE(traceEventSelectedForOutput(itm, TraceSelection{{}, {0U}})) << "stream selector 0 must match unformatted single-source input"; + itm.route = unformattedB; + ASSERT_TRUE(traceEventSelectedForOutput(itm, TraceSelection{{}, {0U}})) + << "public stream selector 0 must not depend on an internal route ordinal"; + EXPECT_NE(unformattedA.id, unformattedB.id) << "distinct no-bus routes must retain distinct internal identities"; + + itm.route = stream111; + EXPECT_TRUE(traceEventSelectedForOutput(itm, TraceSelection{{"itm", "dwt"}, {1U, 111U}})) + << "multiple types and streams must each form a union"; + EXPECT_FALSE(traceEventSelectedForOutput(itm, TraceSelection{{"dwt", "event"}, {1U, 111U}})) + << "type and stream unions must still form an intersection"; + EXPECT_FALSE(traceEventSelectedForOutput(itm, TraceSelection{{"itm", "dwt"}, {1U}})) + << "the route ordinal must never substitute for the architectural Trace Bus ID"; + + itm.route = unformattedB; std::get(itm.payload).channel = 0; ASSERT_TRUE(!traceEventSelectedForOutput(itm, TraceSelection{})) << "TraceSelection must exclude software channel zero without selectors"; diff --git a/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp b/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp index 912fe8f88..e94e0bf10 100644 --- a/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp +++ b/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp @@ -447,8 +447,18 @@ TEST(CtraceUnitTests, testOutputRequirementsRejectUnknownStreamWithMultipleClock const auto commonPlan = planOutputs(ctfRequest, "Multicore.SWO.raw", config, commonDiagnostics); ASSERT_TRUE(commonPlan.ctf.has_value()); EXPECT_EQ(commonPlan.ctf->coreClockHz, 100U); + EXPECT_EQ(commonPlan.ctf->routes.size(), 2U); EXPECT_TRUE(commonDiagnostics.events().empty()); + ctfRequest.selection.streams = {1U, 99U}; + CollectingDiagnosticSink mixedDiagnostics; + const auto mixedPlan = planOutputs(ctfRequest, "Multicore.SWO.raw", config, mixedDiagnostics); + ASSERT_TRUE(mixedPlan.ctf.has_value()); + EXPECT_EQ(mixedPlan.ctf->coreClockHz, 100U); + EXPECT_EQ(mixedPlan.ctf->routes.size(), 2U); + EXPECT_TRUE(mixedDiagnostics.events().empty()); + + ctfRequest.selection.streams = {99U}; config.setups[0].timestamps->clockHz.reset(); config.setups[0].timestamps->clockError = "invalid processor clock"; CollectingDiagnosticSink malformedDiagnostics; diff --git a/tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp b/tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp index 3bb0cd1aa..d36ee78ae 100644 --- a/tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp +++ b/tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -91,7 +92,7 @@ TEST(CtraceUnitTests, testCsvFileOutputCriteria) for (const auto stream : {3U, 0U}) { auto excluded = accepted; - excluded.traceBusId = static_cast(stream); + excluded = onStream(std::move(excluded), static_cast(stream)); output.writeEvent(excluded); } @@ -99,7 +100,7 @@ TEST(CtraceUnitTests, testCsvFileOutputCriteria) std::get(excludedChannel.payload).channel = 0U; output.writeEvent(excludedChannel); - output.writeEvent(onStream(issuePacket(TraceIssueCode::DecodeError), accepted.traceBusId)); + output.writeEvent(onRoute(issuePacket(TraceIssueCode::DecodeError), accepted.route)); output.stop(); ASSERT_TRUE( @@ -146,6 +147,30 @@ TEST(CtraceUnitTests, testCsvFileOutputMatchesSpecification) ASSERT_TRUE(lines[3] == "950364900,,pcsample,,,0x08000100,,") << "CSV PC-sample row schema mismatch"; } +TEST(CtraceUnitTests, testCsvFileOutputPreservesInterleavedRouteOrderAndOnlyWritesArchitecturalIds) +{ + const TemporaryTestPath temporaryPath("ctrace-csv-routes-test.csv"); + CsvFileOutput output(temporaryPath.path()); + const TraceRouteIdentity firstRoute{TraceRouteId{90U}, 7U}; + const TraceRouteIdentity secondRoute{TraceRouteId{4U}, 2U}; + const TraceRouteIdentity unformattedRoute{TraceRouteId{17U}, std::nullopt}; + + output.start(); + output.writeEvent(onRoute(softwarePacket(1U, 1U, 'A'), firstRoute)); + output.writeEvent(onRoute(softwarePacket(2U, 1U, 'B'), secondRoute)); + output.writeEvent(onRoute(softwarePacket(3U, 1U, 'C'), firstRoute)); + output.writeEvent(onRoute(softwarePacket(4U, 1U, 'D'), unformattedRoute)); + output.stop(); + + const auto lines = readTestLines(temporaryPath.path()); + ASSERT_EQ(lines.size(), 5U); + EXPECT_EQ(lines[0], "cycles,stream,type,source,value,pc,address,note"); + EXPECT_EQ(lines[1], ",7,itm,1,0x41,,,"); + EXPECT_EQ(lines[2], ",2,itm,2,0x42,,,"); + EXPECT_EQ(lines[3], ",7,itm,3,0x43,,,"); + EXPECT_EQ(lines[4], ",,itm,4,0x44,,,"); +} + TEST(CtraceUnitTests, testCsvFileOutputWritesTraceIssues) { const TemporaryTestPath temporaryPath("ctrace-csv-issue-test.csv"); diff --git a/tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp b/tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp index 70a5f782f..16db3d0d8 100644 --- a/tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp +++ b/tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp @@ -8,6 +8,7 @@ #include "TestSupport.h" #include #include "TraceEvent.h" +#include "TraceRoute.h" #include "TraceSelection.h" #include "csv/CsvRowMapper.h" #include @@ -66,6 +67,12 @@ TEST(CtraceUnitTests, testCsvRowMapperAndTraceEventSchema) << "CSV must render the raw hexadecimal DWT value with the two-byte SWO width"; ASSERT_TRUE(CsvRowMapper::row(softwarePacket(1U)) == ",,itm,1,0x00,,,") << "CSV must leave the stream field empty for unformatted input"; + EXPECT_EQ(CsvRowMapper::row(softwarePacket(1U, 1U, 0U)), ",,itm,1,0x00,,,"); + EXPECT_EQ(CsvRowMapper::row(softwarePacket(1U, 2U, 0U)), ",,itm,1,0x0000,,,"); + EXPECT_EQ(CsvRowMapper::row(softwarePacket(1U, 4U, 0U)), ",,itm,1,0x00000000,,,"); + EXPECT_EQ(CsvRowMapper::row(TraceEvent{DwtDataTraceEvent{0U, 1U, 0U, AccessType::Write}}), ",,dwt,0,0x00,,,"); + EXPECT_EQ(CsvRowMapper::row(TraceEvent{DwtDataTraceEvent{0U, 2U, 0U, AccessType::Write}}), ",,dwt,0,0x0000,,,"); + EXPECT_EQ(CsvRowMapper::row(TraceEvent{DwtDataTraceEvent{0U, 4U, 0U, AccessType::Write}}), ",,dwt,0,0x00000000,,,"); ASSERT_TRUE(CsvRowMapper::row(TraceEvent{DwtMatchTraceEvent{2U}}) == ",,dwt,2,,,,") << "CSV must expose a match only through its DWT comparator source"; ASSERT_TRUE(CsvRowMapper::row(atCycle(TraceEvent{PcSampleTraceEvent{0x08001234U, false}}, 949339000U)) == @@ -101,6 +108,20 @@ TEST(CtraceUnitTests, testCsvRowMapperEscapesDiagnosticText) EXPECT_EQ(CsvRowMapper::row(atCycle(TraceEvent{GlobalTimestampTraceEvent{123U, false}}, 99U)), "123,,global_ts,,,,,"); } +TEST(CtraceUnitTests, testCsvRowMapperSerializesOnlyArchitecturalTraceBusId) +{ + const TraceRouteIdentity noBusRoute{TraceRouteId{97U}, std::nullopt}; + const TraceRouteIdentity formattedRoute{TraceRouteId{97U}, 7U}; + + EXPECT_EQ(CsvRowMapper::row(onRoute(softwarePacket(1U, 1U, 0x2aU), noBusRoute)), ",,itm,1,0x2a,,,") + << "an internal route ordinal must never appear in the CSV stream column"; + EXPECT_EQ(CsvRowMapper::row(onRoute(softwarePacket(1U, 1U, 0x2aU), formattedRoute)), ",7,itm,1,0x2a,,,") + << "CSV must serialize the architectural Trace Bus ID rather than the internal route ordinal"; + + TraceEvent overflow{OverflowTraceEvent{"route overflow"}}; + EXPECT_EQ(CsvRowMapper::row(onRoute(std::move(overflow), formattedRoute)), ",7,overflow,,,,,route overflow"); +} + TEST(CtraceUnitTests, testCsvRowMapperHandlesInternalAndCustomOverflowEvents) { EXPECT_EQ(CsvRowMapper::row(TraceEvent{DwtEventTraceEvent{0x21U}}), ",,event,0,0x21,,,"); diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp index cd2d7ad8b..a5fa2c30f 100644 --- a/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp +++ b/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp @@ -93,7 +93,7 @@ static ResolvedTraceSource resolvedSource(const CtraceRunSourceMeta& source) return { source.type, source.source, - source.traceBusId, + source.route, source.label, source.address, source.dataType, @@ -212,11 +212,13 @@ TEST(CtraceUnitTests, testCtfBundleOutputUsesCtraceRunMeta) ASSERT_TRUE(outputPlan.ctf.has_value() && preflightDiagnostics.events().empty()) << "resolved CTF source missing"; auto options = std::move(*outputPlan.ctf); ASSERT_TRUE(!options.sources.empty()) << "resolved CTF source missing"; - ASSERT_TRUE(options.sources.front().traceBusId == 7U) << "resolved CTF source must retain its Trace Bus ID"; + ASSERT_TRUE(options.sources.front().route.traceBusId == 7U) << "resolved CTF source must retain its Trace Bus ID"; ASSERT_TRUE(options.sources.front().dataType == "signed") << "resolved CTF source must retain its data type"; + ASSERT_EQ(options.routes.size(), 1U); + const auto route = options.routes.front(); CtfBundleOutput output(std::move(options)); output.start(); - output.writeEvent(atCycle(onStream(TraceEvent{DwtDataTraceEvent{0U, 1U, 0xffU, AccessType::Write}}, 7U), 100U)); + output.writeEvent(atCycle(onRoute(TraceEvent{DwtDataTraceEvent{0U, 1U, 0xffU, AccessType::Write}}, route), 100U)); output.stop(); const auto metadata = readTestTextFile(outputDir / "metadata"); @@ -244,6 +246,34 @@ TEST(CtraceUnitTests, testCtfBundleOutputUsesCtraceRunMeta) ASSERT_TRUE(dwtRecord.payload[3U] == 0xffU) << "CTF signed-byte payload mismatch"; } +TEST(CtraceUnitTests, testCtfOutputPlanningKeepsUnknownFilterWithoutLegacyBootstrap) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-unknown-selected-route-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + TraceRunConfig traceRun; + traceRun.path = "SelectedRoute.ctrace-run.yml"; + traceRun.traceFormat = TraceRunFormat::Formatted; + traceRun.setups.push_back(TraceRunTestSupport::makeTimestampSetup("core", 1000000U, 1U)); + traceRun.references.push_back(TraceRunTestSupport::makeReference("itm", "core", 2U, {1U}, "core/itm")); + const auto meta = CtraceRunMeta::fromConfig(traceRun); + + TraceSelection unknownSelection; + unknownSelection.streams = {99U}; + CollectingDiagnosticSink unknownDiagnostics; + auto plan = planTraceOutputs({false, true, unknownSelection}, outputDir.parent_path() / "output.SWO.raw", meta, + unknownDiagnostics); + ASSERT_TRUE(plan.ctf.has_value()); + ASSERT_EQ(plan.ctf->routes.size(), 1U); + EXPECT_EQ(plan.ctf->routes.front().traceBusId, 2U); + CtfBundleOutput output(std::move(*plan.ctf)); + output.start(); + output.stop(); + + const auto records = readCtfRecords(outputDir / "stream_0"); + EXPECT_TRUE(records.empty()) << "an unmatched stream filter must not invent a synthetic no-bus bootstrap"; + EXPECT_TRUE(unknownDiagnostics.events().empty()); +} + TEST(CtraceUnitTests, testCtfBundleOutputDefaultsDwtValueType) { const TemporaryTestPath temporaryPath("ctrace-ctf-default-dwt-type-test"); @@ -313,7 +343,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputDefaultsDwtValueType) TraceRunTestSupport::makeReference("itm", "core-two", 2U, {}, "core-two/itm"), }; const auto meta = CtraceRunMeta::fromConfig(traceRun); - ASSERT_TRUE(meta.sources().size() == 2U && meta.sources().front().traceBusId == 1U && + ASSERT_TRUE(meta.sources().size() == 2U && meta.sources().front().route.traceBusId == 1U && meta.sources().front().label == std::optional("core-one")) << "trace-run metadata must preserve the exact DWT stream route"; } diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp index 65d646bfd..28aa95da6 100644 --- a/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp +++ b/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp @@ -65,7 +65,9 @@ static ResolvedTraceSource resolvedDwtSource(std::uint32_t comparator, std::uint ResolvedTraceSource source; source.type = "dwt"; source.source = comparator; - source.traceBusId = traceBusId; + source.route = traceBusId == 0U + ? TraceRouteIdentity{} + : TraceRouteIdentity{TraceRouteId{traceBusId}, std::optional(traceBusId)}; source.dataType = std::move(type); source.dataSize = size; return source; @@ -466,11 +468,30 @@ TEST(CtraceUnitTests, testCtfEncoderRejectsConflictingUnformattedDwtRoutes) encoder.abort(); } +TEST(CtraceUnitTests, testCtfEncoderReportsRoutedDwtSizeMismatchContext) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-routed-size-warning-test"); + temporaryPath.createDirectory(); + const TraceRouteIdentity route{TraceRouteId{9U}, 7U}; + auto source = resolvedDwtSource(0U, 7U, "unsigned", 4U); + source.route = route; + CollectingDiagnosticSink diagnostics; + CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{"dwt"}, {}}, {source}, &diagnostics, {route}}); + encoder.start(temporaryPath.path()); + encoder.writeEvent(onRoute(TraceEvent{DwtDataTraceEvent{0U, 1U, 0U, AccessType::Read}}, route)); + encoder.stop(); + + ASSERT_EQ(diagnostics.events().size(), 1U); + EXPECT_TRUE(diagnostics.containsContext("stream", "7")); + EXPECT_TRUE(diagnostics.containsContext("channel", "DWT0")); +} + TEST(CtraceUnitTests, testCtfEncoderIgnoresUnselectedStreamTimeAndQuality) { const TemporaryTestPath temporaryPath("ctrace-ctf-filtered-stream-state-test"); temporaryPath.createDirectory(); - CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{"itm"}, {1U}}, {}}); + const TraceRouteIdentity selectedRoute{TraceRouteId{1U}, 1U}; + CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{"itm"}, {1U}}, {}, nullptr, {selectedRoute}}); encoder.start(temporaryPath.path()); auto excludedTimestamp = atCycle(onStream(TraceEvent{LocalTimestampTraceEvent{}}, 2U), 900U); @@ -494,6 +515,32 @@ TEST(CtraceUnitTests, testCtfEncoderIgnoresUnselectedStreamTimeAndQuality) EXPECT_EQ(readLe32(records[0].payload, 4U), 0U); } +TEST(CtraceUnitTests, testCtfEncoderDoesNotBootstrapNoBusRouteForAnotherExplicitStream) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-no-unknown-route-bootstrap-test"); + temporaryPath.createDirectory(); + CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{}, {2U}}, {}}); + encoder.start(temporaryPath.path()); + encoder.stop(); + + EXPECT_TRUE(readCtfRecords(temporaryPath.path() / "stream_0").empty()); +} + +TEST(CtraceUnitTests, testCtfEncoderDoesNotBootstrapNoBusRouteWhenKnownSourceIsFilteredOut) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-no-source-route-bootstrap-test"); + temporaryPath.createDirectory(); + CtfEncoder encoder(CtfEncoderConfig{ + 1000000U, + TraceSelection{{}, {0U}}, + {resolvedDwtSource(0U, 1U, "unsigned", 4U)}, + }); + encoder.start(temporaryPath.path()); + encoder.stop(); + + EXPECT_TRUE(readCtfRecords(temporaryPath.path() / "stream_0").empty()); +} + TEST(CtraceUnitTests, testCtfEncoderStreamSelectionKeepsStartAndResyncContext) { const TemporaryTestPath temporaryPath("ctrace-ctf-selected-stream-status-test"); @@ -557,3 +604,110 @@ TEST(CtraceUnitTests, testCtfEncoderTracksLocalTimeAndUnqualifiedOverflow) ASSERT_NE(overflowStatus, records.end()); EXPECT_EQ(readLe32(overflowStatus->payload, 1U), 1U); } + +TEST(CtraceUnitTests, testCtfEncoderLazilyBootstrapsExactSelectedRoute) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-lazy-route-bootstrap-test"); + temporaryPath.createDirectory(); + const TraceRouteIdentity route{TraceRouteId{9U}, 2U}; + CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{}, {2U}}, {}}); + encoder.start(temporaryPath.path()); + encoder.writeEvent(onRoute(softwarePacket(1U, 1U, 0x5aU), route)); + encoder.stop(); + + const auto records = readCtfRecords(temporaryPath.path() / "stream_0"); + ASSERT_EQ(records.size(), 3U); + EXPECT_EQ(records[0].id, CtfSchema::value(CtfSchema::EventId::TraceStatus)); + EXPECT_EQ(records[0].traceBusId, 2U); + EXPECT_EQ(records[0].payload[0U], CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart)); + EXPECT_EQ(records[1].id, CtfSchema::value(CtfSchema::EventId::Exception)); + EXPECT_EQ(records[1].traceBusId, 2U); + EXPECT_EQ(records[2].id, CtfSchema::value(CtfSchema::EventId::Itm)); + EXPECT_EQ(records[2].traceBusId, 2U); +} + +TEST(CtraceUnitTests, testCtfEncoderRejectsConflictingIdentityForSameRouteId) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-route-mismatch-test"); + temporaryPath.createDirectory(); + const TraceRouteIdentity configured{TraceRouteId{4U}, 1U}; + CtfEncoder invalidConfig( + CtfEncoderConfig{1000000U, TraceSelection{}, {}, nullptr, {configured, {TraceRouteId{4U}, 2U}}}); + EXPECT_THROW(invalidConfig.start(temporaryPath.path()), std::runtime_error); + + CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{}, {}, nullptr, {configured}}); + encoder.start(temporaryPath.path()); + EXPECT_THROW(encoder.writeEvent(onRoute(softwarePacket(1U), {TraceRouteId{4U}, 2U})), std::runtime_error); + EXPECT_THROW(encoder.writeEvent(onRoute(softwarePacket(1U), {TraceRouteId{9U}, 1U})), std::runtime_error); + encoder.abort(); + + CtfEncoder lazyEncoder(CtfEncoderConfig{1000000U, TraceSelection{}, {}}); + lazyEncoder.start(temporaryPath.path()); + lazyEncoder.writeEvent(onRoute(softwarePacket(1U), configured)); + EXPECT_THROW(lazyEncoder.writeEvent(onRoute(softwarePacket(1U), {TraceRouteId{4U}, 2U})), std::runtime_error); + lazyEncoder.abort(); +} + +TEST(CtraceUnitTests, testCtfEncoderKeepsNoBusRouteStateIndependent) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-no-bus-route-state-test"); + temporaryPath.createDirectory(); + const TraceRouteIdentity first{TraceRouteId{4U}, std::nullopt}; + const TraceRouteIdentity second{TraceRouteId{9U}, std::nullopt}; + CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{}, {}, nullptr, {first, second}}); + encoder.start(temporaryPath.path()); + + auto firstOverflow = onRoute(TraceEvent{OverflowTraceEvent{}}, first); + firstOverflow.quality = TraceQuality{true, false, 5U}; + encoder.writeEvent(firstOverflow); + auto secondOverflow = onRoute(TraceEvent{OverflowTraceEvent{}}, second); + secondOverflow.quality = TraceQuality{true, false, 1U}; + encoder.writeEvent(secondOverflow); + encoder.stop(); + + std::vector overflowCounts; + for (const auto& record : readCtfRecords(temporaryPath.path() / "stream_0")) { + EXPECT_EQ(record.traceBusId, 0U) << "opaque route ID must not leak into the CTF Trace Bus ID field"; + if (record.id == CtfSchema::value(CtfSchema::EventId::TraceStatus) && + record.payload[0U] == CtfSchema::value(CtfSchema::TraceStatusReason::Overflow)) { + overflowCounts.push_back(readLe32(record.payload, 1U)); + } + } + EXPECT_EQ(overflowCounts, (std::vector{5U, 1U})); +} + +TEST(CtraceUnitTests, testCtfEncoderKeepsNoBusExceptionResetStateIndependent) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-no-bus-exception-state-test"); + temporaryPath.createDirectory(); + const TraceRouteIdentity first{TraceRouteId{4U}, std::nullopt}; + const TraceRouteIdentity second{TraceRouteId{9U}, std::nullopt}; + CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{}, {}, nullptr, {first, second}}); + encoder.start(temporaryPath.path()); + + encoder.writeEvent(onRoute(exceptionPacket(15U, ExceptionAction::Entered, 10U), first)); + encoder.writeEvent(onRoute(exceptionPacket(54U, ExceptionAction::Entered, 20U), second)); + encoder.writeEvent(atCycle(onRoute(TraceEvent{OverflowTraceEvent{}}, first), 30U)); + encoder.writeEvent(onRoute(exceptionPacket(54U, ExceptionAction::Exited, 40U), second)); + encoder.stop(); + + const auto records = readCtfRecords(temporaryPath.path() / "stream_0"); + for (const auto& record : records) { + EXPECT_EQ(record.traceBusId, 0U) << "opaque route IDs must remain absent from the legacy CTF field"; + } + const auto exceptions = timestampedCtfExceptionRecords(records); + const auto contains = [&](std::uint64_t timestamp, ExceptionNumber number, std::uint8_t action, std::uint8_t origin) { + return std::find(exceptions.begin(), exceptions.end(), + TimestampedCtfExceptionRecord{timestamp, {number, action, origin}}) != exceptions.end(); + }; + EXPECT_TRUE(contains(10U, 15U, CtfSchema::value(CtfSchema::ExceptionAction::Entered), + CtfSchema::value(CtfSchema::ExceptionOrigin::Trace))); + EXPECT_TRUE(contains(20U, 54U, CtfSchema::value(CtfSchema::ExceptionAction::Entered), + CtfSchema::value(CtfSchema::ExceptionOrigin::Trace))); + EXPECT_TRUE(contains(30U, 15U, CtfSchema::value(CtfSchema::ExceptionAction::Exited), + CtfSchema::value(CtfSchema::ExceptionOrigin::Synthetic))); + EXPECT_FALSE(contains(30U, 54U, CtfSchema::value(CtfSchema::ExceptionAction::Exited), + CtfSchema::value(CtfSchema::ExceptionOrigin::Synthetic))); + EXPECT_TRUE(contains(40U, 54U, CtfSchema::value(CtfSchema::ExceptionAction::Exited), + CtfSchema::value(CtfSchema::ExceptionOrigin::Trace))); +} diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp index 5bf5631c9..dc68421aa 100644 --- a/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp +++ b/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp @@ -30,15 +30,16 @@ TEST(CtraceUnitTests, testCtfMetadataWriterEscapesAndDeduplicatesSourceLabels) { const TemporaryTestPath path("ctrace-metadata-writer"); path.createDirectory(); + const TraceRouteIdentity route{TraceRouteId{0U}, 1U}; const std::vector sources{ - {"itm", 1U, 1U, std::string("ITM3"), std::nullopt, "unsigned", 4U}, - {"itm", 2U, 1U, std::string("ITM3_1"), std::nullopt, "unsigned", 4U}, - {"itm", 3U, 1U, std::string("ITM3"), std::nullopt, "unsigned", 4U}, - {"itm", 4U, 1U, std::string("line\rbreak"), std::nullopt, "unsigned", 4U}, - {"itm", 5U, 1U, std::nullopt, std::nullopt, "unsigned", 4U}, - {"itm", 6U, 1U, std::string("ITM3"), std::nullopt, "unsigned", 4U}, - {"future", 7U, 1U, std::string("ignored"), std::nullopt, "unsigned", 4U}, - {"dwt", 0U, 1U, std::nullopt, std::numeric_limits::max(), "unsigned", 4U}, + {"itm", 1U, route, std::string("ITM3"), std::nullopt, "unsigned", 4U}, + {"itm", 2U, route, std::string("ITM3_1"), std::nullopt, "unsigned", 4U}, + {"itm", 3U, route, std::string("ITM3"), std::nullopt, "unsigned", 4U}, + {"itm", 4U, route, std::string("line\rbreak"), std::nullopt, "unsigned", 4U}, + {"itm", 5U, route, std::nullopt, std::nullopt, "unsigned", 4U}, + {"itm", 6U, route, std::string("ITM3"), std::nullopt, "unsigned", 4U}, + {"future", 7U, route, std::string("ignored"), std::nullopt, "unsigned", 4U}, + {"dwt", 0U, route, std::nullopt, std::numeric_limits::max(), "unsigned", 4U}, }; CtfMetadataWriter::write(path.path(), "00000000-0000-4000-8000-000000000000", 1000000U, sources, diff --git a/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp b/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp index 6341b866e..b7a7b4310 100644 --- a/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp +++ b/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp @@ -228,7 +228,8 @@ TEST(CtraceUnitTests, testCtraceRunMetaNormalizesAmbiguousUnformattedProcessorId EXPECT_EQ(selectedMeta.routes().front().processorName, std::optional("a")); EXPECT_EQ(selectedMeta.routes().front().timestampClockHz, std::optional(100U)); EXPECT_EQ(selectedMeta.routes().front().timestampPrescaler, 4U); - EXPECT_EQ(selectedMeta.sources().front().traceBusId, 0U); + EXPECT_EQ(selectedMeta.sources().front().route, selectedMeta.routes().front().identity); + EXPECT_FALSE(selectedMeta.sources().front().route.traceBusId.has_value()); selected.references.push_back(makeReference("itm", std::nullopt, 5U, {2U}, "messages")); const auto inferredReferenceMeta = CtraceRunMeta::fromConfig(selected); @@ -388,8 +389,10 @@ TEST(CtraceUnitTests, testCtraceRunMetaMergesCompatibleUnformattedProcessorSetti std::optional( "unformatted SINGLE trace has ambiguous timestamps.clock values across processor candidates")); ASSERT_EQ(meta.sources().size(), 2U); - EXPECT_EQ(meta.sources()[0].traceBusId, 0U); - EXPECT_EQ(meta.sources()[1].traceBusId, 0U); + EXPECT_EQ(meta.sources()[0].route, meta.routes().front().identity); + EXPECT_EQ(meta.sources()[1].route, meta.routes().front().identity); + EXPECT_FALSE(meta.sources()[0].route.traceBusId.has_value()); + EXPECT_FALSE(meta.sources()[1].route.traceBusId.has_value()); config.setups[1] = makeTimestampSetup("b", 100U, 4U, 1U); const auto equivalent = CtraceRunMeta::fromConfig(config); @@ -537,18 +540,22 @@ TEST(CtraceUnitTests, testCtraceRunMetaCreatesOneSyntheticUnformattedRoute) ASSERT_EQ(meta.routes().size(), 1U); const auto& route = meta.routes().front(); EXPECT_EQ(route.protocol, CtraceRunProtocol::Itm); - EXPECT_FALSE(route.traceBusId.has_value()); + EXPECT_EQ(route.identity.id, TraceRouteId{0U}); + EXPECT_FALSE(route.identity.traceBusId.has_value()); EXPECT_FALSE(route.timestampsConfigured); EXPECT_EQ(route.timestampPrescaler, TraceRunSchema::kDefaultTimestampPrescaler); ASSERT_EQ(route.sources.size(), 1U); - EXPECT_EQ(route.sources.front().traceBusId, 0U); + EXPECT_EQ(route.sources.front().route, route.identity); ASSERT_EQ(meta.sources().size(), 1U); - EXPECT_EQ(meta.sources().front().traceBusId, 0U) << "SINGLE accessors must expose the transport channel"; + EXPECT_EQ(meta.sources().front().route, route.identity); + EXPECT_FALSE(meta.sources().front().route.traceBusId.has_value()) + << "SINGLE metadata must not expose OpenCSD transport channel 0 as an architectural ID"; } const auto emptyMeta = CtraceRunMeta::fromConfig(TraceRunConfig{}); ASSERT_EQ(emptyMeta.routes().size(), 1U); - EXPECT_FALSE(emptyMeta.routes().front().traceBusId.has_value()); + EXPECT_EQ(emptyMeta.routes().front().identity.id, TraceRouteId{0U}); + EXPECT_FALSE(emptyMeta.routes().front().identity.traceBusId.has_value()); } TEST(CtraceUnitTests, testCtraceRunMetaBuildsFormattedAnchorRoutesAndMetadata) @@ -565,14 +572,14 @@ TEST(CtraceUnitTests, testCtraceRunMetaBuildsFormattedAnchorRoutesAndMetadata) auto data = routeReference("dwt", "first/data#0", "first", 1U, {2U}); data.dataSetupIndex = 0U; data.address = 0x20000000U; - auto secondAnchor = routeReference("itm", "second/itm", "second", 111U); + auto secondAnchor = routeReference("itm", "second/itm", "second", 111U, {1U}); const auto config = formattedConfig({firstAnchor, data, secondAnchor}, {firstSetup, secondSetup}); const auto meta = CtraceRunMeta::fromConfig(config); ASSERT_EQ(meta.routes().size(), 2U); const auto& first = meta.routes()[0]; - EXPECT_EQ(first.traceBusId, std::optional(1U)); + EXPECT_EQ(first.identity, (TraceRouteIdentity{TraceRouteId{0U}, 1U})); EXPECT_EQ(first.processorName, std::optional("first")); EXPECT_TRUE(first.timestampsConfigured); EXPECT_EQ(first.timestampClockHz, std::optional(100U)); @@ -582,6 +589,8 @@ TEST(CtraceUnitTests, testCtraceRunMetaBuildsFormattedAnchorRoutesAndMetadata) EXPECT_EQ(first.sources[0].type, "itm"); EXPECT_EQ(first.sources[1].type, "dwt"); EXPECT_EQ(first.sources[1].dataSize, 2U); + EXPECT_EQ(first.sources[0].route, first.identity); + EXPECT_EQ(first.sources[1].route, first.identity); ASSERT_EQ(first.referenceDiagnostics.size(), 3U); EXPECT_EQ(first.referenceDiagnostics[0].severity, CtraceRunReferenceDiagnostic::Severity::Info); EXPECT_EQ(first.referenceDiagnostics[1].severity, CtraceRunReferenceDiagnostic::Severity::Warning); @@ -589,10 +598,20 @@ TEST(CtraceUnitTests, testCtraceRunMetaBuildsFormattedAnchorRoutesAndMetadata) EXPECT_EQ(meta.referenceDiagnostics().size(), 3U); const auto& second = meta.routes()[1]; - EXPECT_EQ(second.traceBusId, std::optional(111U)); + EXPECT_EQ(second.identity, (TraceRouteIdentity{TraceRouteId{1U}, 111U})); EXPECT_EQ(second.processorName, std::optional("second")); EXPECT_EQ(second.timestampClockHz, std::optional(200U)); EXPECT_EQ(second.timestampPrescaler, 16U); + ASSERT_EQ(second.sources.size(), 1U); + EXPECT_EQ(second.sources.front().source, 1U) << "the same ITM source number must remain valid on a distinct route"; + EXPECT_EQ(second.sources.front().route, second.identity); + + const auto reversed = + CtraceRunMeta::fromConfig(formattedConfig({secondAnchor, data, firstAnchor}, {secondSetup, firstSetup})); + ASSERT_EQ(reversed.routes().size(), 2U); + EXPECT_EQ(reversed.routes()[0].identity, first.identity); + EXPECT_EQ(reversed.routes()[1].identity, second.identity) + << "normalized route ordinals must be deterministic rather than reference-order dependent"; } TEST(CtraceUnitTests, testCtraceRunMetaAcceptsOnlyConstrainedFormattedFallbacks) @@ -615,7 +634,7 @@ TEST(CtraceUnitTests, testCtraceRunMetaAcceptsOnlyConstrainedFormattedFallbacks) } const auto meta = CtraceRunMeta::fromConfig(formattedConfig({reference})); ASSERT_EQ(meta.routes().size(), 1U) << fallback.type << " / " << fallback.path; - EXPECT_EQ(meta.routes().front().traceBusId, std::optional(1U)); + EXPECT_EQ(meta.routes().front().identity.traceBusId, std::optional(1U)); } const std::vector rejected{ diff --git a/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp b/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp index 9a668b1c4..674e41bf9 100644 --- a/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp +++ b/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp @@ -117,13 +117,13 @@ TEST(CtraceUnitTests, TraceRunReaderParsesConsumedFields) ASSERT_EQ(meta.sources().size(), 2U); const auto& itm = meta.sources()[0]; EXPECT_EQ(itm.type, "itm"); - EXPECT_EQ(itm.traceBusId, 2U); + EXPECT_EQ(itm.route.traceBusId, 2U); EXPECT_EQ(itm.source, 1U); EXPECT_EQ(itm.label, std::optional("Console")); const auto& dwt = meta.sources()[1]; EXPECT_EQ(dwt.type, "dwt"); - EXPECT_EQ(dwt.traceBusId, 2U); + EXPECT_EQ(dwt.route.traceBusId, 2U); EXPECT_EQ(dwt.source, 0U); EXPECT_EQ(dwt.dataType, "signed"); EXPECT_EQ(dwt.dataSize, 1U); @@ -202,7 +202,7 @@ TEST(CtraceUnitTests, TraceRunReaderIgnoresCopiedItmAtbidForRouting) EXPECT_EQ(config.setups.front().itm->enableMask, 3U); const auto meta = CtraceRunMeta::fromConfig(config); ASSERT_EQ(meta.routes().size(), 1U); - EXPECT_EQ(meta.routes().front().traceBusId, std::optional(1U)); + EXPECT_EQ(meta.routes().front().identity.traceBusId, std::optional(1U)); } TEST(CtraceUnitTests, TraceRunReaderUsesReferencedSetupSizeAsFallback) diff --git a/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp b/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp index 3c7d281af..2d0098d0b 100644 --- a/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp +++ b/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp @@ -156,7 +156,7 @@ TEST(CtraceUnitTests, testTraceRunDiscoveryResolvesOnePreflightedInput) EXPECT_EQ(legacy.framing(), TraceRunInputFraming::MemoryAligned); EXPECT_FALSE(legacy.metadata().traceFormat().has_value()); ASSERT_EQ(legacy.metadata().routes().size(), 1U); - EXPECT_FALSE(legacy.metadata().routes().front().traceBusId.has_value()); + EXPECT_FALSE(legacy.metadata().routes().front().identity.traceBusId.has_value()); } const auto explicitConfig = root / "Explicit.ctrace-run.yml"; @@ -183,7 +183,7 @@ TEST(CtraceUnitTests, testTraceRunDiscoveryResolvesOnePreflightedInput) EXPECT_TRUE(descriptor.formatDeclared()); EXPECT_EQ(descriptor.metadata().traceFormat(), TraceRunFormat::Formatted); ASSERT_EQ(descriptor.metadata().routes().size(), 1U); - EXPECT_EQ(descriptor.metadata().routes().front().traceBusId, 1U); + EXPECT_EQ(descriptor.metadata().routes().front().identity.traceBusId, 1U); } for (const auto size : {1U, 15U, 17U, 31U}) { writeTestFile(formatted, std::string(size, 'f')); diff --git a/tools/ctrace/test/unit/support/OpenCsdSessionTestSupport.h b/tools/ctrace/test/unit/support/OpenCsdSessionTestSupport.h index 0114aeed5..39807eb79 100644 --- a/tools/ctrace/test/unit/support/OpenCsdSessionTestSupport.h +++ b/tools/ctrace/test/unit/support/OpenCsdSessionTestSupport.h @@ -165,7 +165,7 @@ class ScriptedDecoderHarness { /** @brief Creates a decoder connected to a new empty session script. */ ScriptedDecoderHarness() : m_script(std::make_shared()), - m_decoder(m_sink, scriptedFactory(m_script)) + m_decoder({}, m_sink, scriptedFactory(m_script)) { } diff --git a/tools/ctrace/test/unit/support/OpenCsdTestSupport.h b/tools/ctrace/test/unit/support/OpenCsdTestSupport.h index 890552645..89de055f6 100644 --- a/tools/ctrace/test/unit/support/OpenCsdTestSupport.h +++ b/tools/ctrace/test/unit/support/OpenCsdTestSupport.h @@ -12,6 +12,7 @@ #include "TraceEvent.h" #include +#include #include #include @@ -54,7 +55,8 @@ inline OpenCsdTraceElement openCsdElement(OpenCsdTraceElement::Kind kind, std::u OpenCsdTraceElement element; element.kind = kind; element.sourceIndex = index; - element.traceBusId = stream; + element.route = stream == 0U ? TraceRouteIdentity{} + : TraceRouteIdentity{TraceRouteId{stream}, std::optional(stream)}; return element; } diff --git a/tools/ctrace/test/unit/support/TestSupport.h b/tools/ctrace/test/unit/support/TestSupport.h index c9786b8c5..141b29d89 100644 --- a/tools/ctrace/test/unit/support/TestSupport.h +++ b/tools/ctrace/test/unit/support/TestSupport.h @@ -10,6 +10,7 @@ #include "DiagnosticSink.h" #include "TraceEvent.h" +#include "TraceRoute.h" #include #include @@ -211,7 +212,16 @@ inline TraceEvent atCycle(TraceEvent event, std::uint64_t tcyc) /** @brief Assigns a Trace Bus ID to a copied event. */ inline TraceEvent onStream(TraceEvent event, std::uint8_t traceBusId) { - event.traceBusId = traceBusId; + event.route = traceBusId == 0U + ? TraceRouteIdentity{} + : TraceRouteIdentity{TraceRouteId{traceBusId}, std::optional(traceBusId)}; + return event; +} + +/** @brief Assigns an exact normalized route to a copied event. */ +inline TraceEvent onRoute(TraceEvent event, TraceRouteIdentity route) +{ + event.route = std::move(route); return event; } From 6d55dc10da69eaf82b3425e48657774dd2d957bb Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Thu, 10 Sep 2026 09:57:29 +0200 Subject: [PATCH 05/31] feat(ctrace): model CTF streams and clocks --- .../ctrace/docs/multicore-multisource-plan.md | 4 +- tools/ctrace/src/CMakeLists.txt | 4 + .../ctrace/src/output/OutputRequirements.cpp | 246 +++++----- tools/ctrace/src/output/TraceOutputConfig.h | 22 +- .../ctrace/src/output/ctf/CtfBundleOutput.cpp | 7 +- tools/ctrace/src/output/ctf/CtfBundleOutput.h | 2 + tools/ctrace/src/output/ctf/CtfEncoder.cpp | 112 ++--- tools/ctrace/src/output/ctf/CtfEncoder.h | 14 +- .../src/output/ctf/CtfMetadataModel.cpp | 218 +++++++++ .../ctrace/src/output/ctf/CtfMetadataModel.h | 176 ++++++++ .../src/output/ctf/CtfMetadataWriter.cpp | 306 ++++++++++--- .../ctrace/src/output/ctf/CtfMetadataWriter.h | 12 +- .../ctrace/src/output/ctf/CtfStreamWriter.cpp | 42 +- tools/ctrace/src/output/ctf/CtfStreamWriter.h | 15 +- tools/ctrace/src/output/ctf/CtfUuid.cpp | 38 ++ tools/ctrace/src/output/ctf/CtfUuid.h | 61 +++ tools/ctrace/test/unit/CMakeLists.txt | 1 + .../src/output/OutputRequirementsTests.cpp | 314 +++++++++++-- .../src/output/ctf/CtfBundleOutputTests.cpp | 125 ++++-- .../unit/src/output/ctf/CtfEncoderTests.cpp | 425 +++++++++--------- .../src/output/ctf/CtfMetadataModelTests.cpp | 284 ++++++++++++ .../src/output/ctf/CtfMetadataWriterTests.cpp | 77 +++- .../src/output/ctf/CtfStreamWriterTests.cpp | 14 +- .../ctrace/test/unit/support/CtfTestSupport.h | 21 + 24 files changed, 1871 insertions(+), 669 deletions(-) create mode 100644 tools/ctrace/src/output/ctf/CtfMetadataModel.cpp create mode 100644 tools/ctrace/src/output/ctf/CtfMetadataModel.h create mode 100644 tools/ctrace/src/output/ctf/CtfUuid.cpp create mode 100644 tools/ctrace/src/output/ctf/CtfUuid.h create mode 100644 tools/ctrace/test/unit/src/output/ctf/CtfMetadataModelTests.cpp diff --git a/tools/ctrace/docs/multicore-multisource-plan.md b/tools/ctrace/docs/multicore-multisource-plan.md index 4f376cd5b..367d0e351 100644 --- a/tools/ctrace/docs/multicore-multisource-plan.md +++ b/tools/ctrace/docs/multicore-multisource-plan.md @@ -539,8 +539,8 @@ Phase 0 -> Phase 1 -> Phase 2 -> Phase 3 -> Phase 4 | 1 | Trace-run declaration and route normalization | Complete | | 2 | Raw-input discovery and preflight | Complete | | 3 | Route-aware semantic state, diagnostics, and CSV | Complete | -| 4 | CTF descriptors and metadata model | Next | -| 5 | Multi-stream CTF bundle and Trace Compass policy | Pending | +| 4 | CTF descriptors and metadata model | Complete | +| 5 | Multi-stream CTF bundle and Trace Compass policy | Next | | 6 | DecodeTree `SINGLE` migration | Pending | | 7 | Clean formatted decoding and TB integration | Pending | | 8 | Route-local recovery and error isolation | Pending | diff --git a/tools/ctrace/src/CMakeLists.txt b/tools/ctrace/src/CMakeLists.txt index ffd42deea..9bde3fd35 100644 --- a/tools/ctrace/src/CMakeLists.txt +++ b/tools/ctrace/src/CMakeLists.txt @@ -45,9 +45,11 @@ set(CTRACE_OUTPUT_HEADER_FILES output/ctf/CtfBundleOutput.h output/ctf/CtfEncoder.h output/ctf/CtfExceptionLaneTracker.h + output/ctf/CtfMetadataModel.h output/ctf/CtfMetadataWriter.h output/ctf/CtfSchema.h output/ctf/CtfStreamWriter.h + output/ctf/CtfUuid.h output/ctf/TraceCompassXmlWriter.h ) set(CTRACE_CONTROL_HEADER_FILES @@ -150,8 +152,10 @@ add_library(ctrace-output STATIC output/ctf/CtfBundleOutput.cpp output/ctf/CtfEncoder.cpp output/ctf/CtfExceptionLaneTracker.cpp + output/ctf/CtfMetadataModel.cpp output/ctf/CtfMetadataWriter.cpp output/ctf/CtfStreamWriter.cpp + output/ctf/CtfUuid.cpp output/ctf/TraceCompassXmlWriter.cpp output/OutputRequirements.cpp output/TraceOutputLifecycle.cpp diff --git a/tools/ctrace/src/output/OutputRequirements.cpp b/tools/ctrace/src/output/OutputRequirements.cpp index 8df6c2d7e..6b9553062 100644 --- a/tools/ctrace/src/output/OutputRequirements.cpp +++ b/tools/ctrace/src/output/OutputRequirements.cpp @@ -8,6 +8,7 @@ #include "OutputRequirements.h" #include "CtraceRunMeta.h" +#include "ctf/CtfMetadataModel.h" #include "ctf/CtfSchema.h" #include "DiagnosticSink.h" #include "TraceSelection.h" @@ -16,6 +17,7 @@ #include #include +#include #include #include #include @@ -77,6 +79,23 @@ routeContext(const std::string_view& backend, const CtraceRunMeta& ctraceRunMeta return context; } +/** @brief Builds public diagnostic context for one normalized route. */ +static std::vector> +routeContext(const std::string_view& backend, const CtraceRunMeta& ctraceRunMeta, const CtraceRunRoute& route) +{ + std::vector> context{{"backend", std::string(backend)}}; + if (!ctraceRunMeta.configPath().empty()) { + context.emplace_back("config", ctraceRunMeta.configPath()); + } + if (route.identity.traceBusId.has_value()) { + context.emplace_back("stream", std::to_string(*route.identity.traceBusId)); + } + if (route.processorName.has_value()) { + context.emplace_back("pname", *route.processorName); + } + return context; +} + /** @brief Reports one output preflight failure with optional context. */ static void reportRequirementError(DiagnosticSink& diagnostics, std::string message, std::vector> context) @@ -89,18 +108,18 @@ static void reportRequirementError(DiagnosticSink& diagnostics, std::string mess } /** @brief Validates that selected CTF routes have unambiguous stream identities. */ -static bool validateCtfRouteIdentity(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, - DiagnosticSink& diagnostics) +static bool validateCtfSourceIdentity(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, + DiagnosticSink& diagnostics) { bool valid = true; - std::map, const CtraceRunSourceMeta*> routes; - std::set> reported; + std::map, const CtraceRunSourceMeta*> sources; + std::set> reported; for (const auto& source : ctraceRunMeta.sources()) { if (!routeMatchesSelection(source, selection)) { continue; } - const auto key = std::make_pair(source.type, source.source); - const auto [found, inserted] = routes.emplace(key, &source); + const auto key = std::make_tuple(source.route.id, source.type, source.source); + const auto [found, inserted] = sources.emplace(key, &source); if (inserted) { continue; } @@ -110,8 +129,8 @@ static bool validateCtfRouteIdentity(const CtraceRunMeta& ctraceRunMeta, const T first.addressError == source.addressError && first.dataTypeError == source.dataTypeError && first.dataSizeError == source.dataSizeError; - const auto indistinguishableProcessors = first.route == source.route && first.processorName != source.processorName; - if ((sameMetadata && !indistinguishableProcessors) || !reported.insert(key).second) { + const auto sameBinding = first.route == source.route && first.processorName == source.processorName; + if ((sameMetadata && sameBinding) || !reported.insert(key).second) { continue; } @@ -120,146 +139,79 @@ static bool validateCtfRouteIdentity(const CtraceRunMeta& ctraceRunMeta, const T context.emplace_back("type", source.type); context.emplace_back("firstProcessor", first.processorName.value_or("")); context.emplace_back("otherProcessor", source.processorName.value_or("")); - context.emplace_back("firstStream", first.route.traceBusId.has_value() ? std::to_string(*first.route.traceBusId) - : ""); - reportRequirementError( - diagnostics, - "CTF metadata cannot describe conflicting active type/source routes from different processors or Trace Bus IDs", - std::move(context)); + reportRequirementError(diagnostics, + "CTF metadata cannot describe conflicting active metadata for one route/type/source key", + std::move(context)); } return valid; } -/** @brief Stores an unambiguous clock or the diagnostics preventing selection. */ -struct SelectedClockResolution { - std::optional clockHz; - bool hasRoutes{false}; - bool valid{true}; -}; - -/** @brief Resolves a common clock from all selected stream routes. */ -static SelectedClockResolution resolveSelectedCtfClock(const CtraceRunMeta& ctraceRunMeta, - const TraceSelection& selection, DiagnosticSink& diagnostics) +/** @brief Returns exactly the normalized routes selected for CTF output. */ +static std::vector selectedCtfRoutes(const CtraceRunMeta& ctraceRunMeta, + const TraceSelection& selection) { - SelectedClockResolution result; - for (const auto& [traceBusId, timestamp] : ctraceRunMeta.timestampsByTraceBusId()) { - if (!selection.includesStream(traceBusId)) { - continue; - } - result.hasRoutes = true; - if (timestamp.clockError.has_value()) { - result.valid = false; - reportRequirementError(diagnostics, - "CTF output cannot use the configured timestamps.clock", - { - {"backend", "ctf"}, - {"config", ctraceRunMeta.configPath()}, - {"stream", std::to_string(traceBusId)}, - {"pname", timestamp.processorName.value_or("")}, - {"error", *timestamp.clockError}, - }); - continue; - } - if (!timestamp.clockHz.has_value()) { - result.valid = false; - reportRequirementError(diagnostics, - "CTF output requires timestamps.clock for the processor assigned to this Trace Bus ID", - { - {"backend", "ctf"}, - {"config", ctraceRunMeta.configPath()}, - {"stream", std::to_string(traceBusId)}, - {"pname", timestamp.processorName.value_or("")}, - }); - continue; - } - if (*timestamp.clockHz == 0U) { - result.valid = false; - reportRequirementError(diagnostics, - "CTF output requires timestamps.clock to be greater than zero", - { - {"backend", "ctf"}, - {"config", ctraceRunMeta.configPath()}, - {"stream", std::to_string(traceBusId)}, - {"pname", timestamp.processorName.value_or("")}, - }); - continue; - } - if (result.clockHz.has_value() && *result.clockHz != *timestamp.clockHz) { - result.valid = false; - reportRequirementError(diagnostics, - "CTF output cannot combine selected Trace Bus IDs with different timestamps.clock values", - { - {"backend", "ctf"}, - {"config", ctraceRunMeta.configPath()}, - {"stream", std::to_string(traceBusId)}, - {"clock", std::to_string(*timestamp.clockHz)}, - }); - continue; + std::vector routes; + for (const auto& route : ctraceRunMeta.routes()) { + if (selection.includesRoute(route.identity)) { + routes.push_back(&route); } - result.clockHz = timestamp.clockHz; } - return result; + return routes; } -/** @brief Resolves the fallback CTF clock when no selected route supplies one. */ -static std::optional resolveDefaultCtfClock(const CtraceRunMeta& ctraceRunMeta, - const TraceSelection& selection, DiagnosticSink& diagnostics) +/** @brief Resolves route-specific CTF stream and clock-domain descriptors. */ +static std::optional +resolveCtfTopology(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, DiagnosticSink& diagnostics) { - for (const auto& error : ctraceRunMeta.timestampClockErrors()) { - reportRequirementError(diagnostics, - "CTF output cannot use the configured timestamps.clock", - { - {"backend", "ctf"}, - {"config", ctraceRunMeta.configPath()}, - {"error", error}, - }); + auto routes = selectedCtfRoutes(ctraceRunMeta, selection); + const auto legacy = ctraceRunMeta.routes().size() == 1U && + !ctraceRunMeta.routes().front().identity.traceBusId.has_value() && + ctraceRunMeta.traceFormat() != TraceRunFormat::Formatted; + if (legacy && routes.empty()) { + routes.push_back(&ctraceRunMeta.routes().front()); } - if (!ctraceRunMeta.timestampClockErrors().empty()) { - return std::nullopt; + if (routes.empty()) { + return CtfMetadataTopology{}; } - if (!ctraceRunMeta.timestampClockHz().has_value()) { - if (!selection.streams.empty() && !ctraceRunMeta.timestampsByTraceBusId().empty()) { - reportRequirementError(diagnostics, - "CTF output cannot assign unformatted or unknown Trace Bus IDs to processors with " - "different timestamps.clock values", - { - {"backend", "ctf"}, - {"config", ctraceRunMeta.configPath()}, - }); - return std::nullopt; + + bool valid = true; + for (const auto* route : routes) { + auto context = routeContext("ctf", ctraceRunMeta, *route); + if (route->timestampClockError.has_value()) { + valid = false; + context.emplace_back("error", *route->timestampClockError); + reportRequirementError(diagnostics, "CTF output cannot use the configured timestamps.clock", std::move(context)); + } else if (!route->timestampClockHz.has_value()) { + valid = false; + reportRequirementError(diagnostics, "CTF output requires timestamps.clock; no default is assumed", + std::move(context)); + } else if (*route->timestampClockHz == 0U) { + valid = false; + reportRequirementError(diagnostics, "CTF output requires timestamps.clock to be greater than zero", + std::move(context)); } - reportRequirementError(diagnostics, - "CTF output requires timestamps.clock from an active ctrace-setup; no default is assumed", - { - {"backend", "ctf"}, - {"config", ctraceRunMeta.configPath()}, - }); - return std::nullopt; } - if (*ctraceRunMeta.timestampClockHz() == 0U) { - reportRequirementError(diagnostics, - "CTF output requires timestamps.clock to be greater than zero", - { - {"backend", "ctf"}, - {"config", ctraceRunMeta.configPath()}, - }); + if (!valid) { return std::nullopt; } - return ctraceRunMeta.timestampClockHz(); -} -/** @brief Resolves and validates the clock used by a CTF output. */ -static std::optional resolveCtfClock(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, - DiagnosticSink& diagnostics) -{ - const auto selected = resolveSelectedCtfClock(ctraceRunMeta, selection, diagnostics); - if (!selected.valid) { - return std::nullopt; + CtfMetadataTopology topology; + if (legacy) { + topology.clockDomains.push_back( + {CtfClockDomainId{0U}, "swo_clock", std::nullopt, *routes.front()->timestampClockHz, false}); + topology.streams.push_back({CtfStreamClassId{0U}, routes.front()->identity, CtfSourceKind::Itm, + routes.front()->processorName, CtfClockDomainId{0U}}); + return topology; } - if (selected.hasRoutes) { - return selected.clockHz; + + for (const auto* route : routes) { + const auto domainId = CtfClockDomainId{static_cast(topology.clockDomains.size() + 1U)}; + topology.clockDomains.push_back({domainId, "cmsis_clock_" + std::to_string(domainId.value()), CtfUuid::randomV4(), + *route->timestampClockHz, false}); + const auto streamClassId = CtfStreamClassId{route->identity.traceBusId.value_or(0U)}; + topology.streams.push_back({streamClassId, route->identity, CtfSourceKind::Itm, route->processorName, domainId}); } - return resolveDefaultCtfClock(ctraceRunMeta, selection, diagnostics); + return std::optional{std::move(topology)}; } /** @brief Validates address, data type, and size metadata for selected DWT routes. */ @@ -299,6 +251,12 @@ static bool validateCtfDwtMetadata(const CtraceRunMeta& ctraceRunMeta, const Tra if (!sourceValid) { continue; } + if (source.source > 3U) { + valid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + reportRequirementError(diagnostics, "CTF output requires DWT comparator sources between 0 and 3", + std::move(context)); + } const auto validType = TraceRunSchema::isDwtDataType(source.dataType); const auto* valueVariant = CtfSchema::valueVariantForTraceRunType(source.dataType, source.dataSize); if (!validType) { @@ -321,16 +279,27 @@ static bool validateCtfDwtMetadata(const CtraceRunMeta& ctraceRunMeta, const Tra std::string(CtfSchema::ValueTypeRequirements), std::move(context)); } + if (source.address.has_value() && valueVariant != nullptr) { + const auto extent = source.dataSize - 1U; + if (*source.address > std::numeric_limits::max() - extent) { + valid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("address", std::to_string(*source.address)); + context.emplace_back("dataSize", std::to_string(source.dataSize)); + reportRequirementError(diagnostics, "CTF output cannot represent the configured DWT address range", + std::move(context)); + } + } } return valid; } /** @brief Converts selected trace-run sources into normalized CTF routes. */ -static std::vector resolveCtfSources(const CtraceRunMeta& ctraceRunMeta, +static std::vector resolveCtfSources(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection) { std::set> resolvedKeys; - std::vector sources; + std::vector sources; for (const auto& route : ctraceRunMeta.sources()) { if ((route.type != "itm" && route.type != "dwt") || (route.type == "itm" && route.source == 0U) || !routeMatchesSelection(route, selection) || @@ -389,17 +358,14 @@ TraceOutputPlan planTraceOutputs(const TraceOutputRequest& request, const std::f }; } if (plan.ctfRequested) { - auto clock = resolveCtfClock(ctraceRunMeta, request.selection, diagnostics); - const auto validRoutes = validateCtfRouteIdentity(ctraceRunMeta, request.selection, diagnostics); + auto metadata = resolveCtfTopology(ctraceRunMeta, request.selection, diagnostics); + const auto validRoutes = validateCtfSourceIdentity(ctraceRunMeta, request.selection, diagnostics); const auto validTypes = validateCtfDwtMetadata(ctraceRunMeta, request.selection, diagnostics); - auto sources = - clock.has_value() && validRoutes && validTypes - ? std::optional>(resolveCtfSources(ctraceRunMeta, request.selection)) - : std::nullopt; - if (clock.has_value() && validRoutes && validTypes && sources.has_value()) { + if (metadata.has_value() && validRoutes && validTypes) { + metadata->sources = resolveCtfSources(ctraceRunMeta, request.selection); plan.ctf = CtfOutputConfig{ - paths.ctf, paths.traceCompassXml, *clock, request.selection, - std::move(*sources), resolveCtfRoutes(ctraceRunMeta), true, + paths.ctf, paths.traceCompassXml, request.selection, std::move(*metadata), resolveCtfRoutes(ctraceRunMeta), + true, }; } } diff --git a/tools/ctrace/src/output/TraceOutputConfig.h b/tools/ctrace/src/output/TraceOutputConfig.h index 4f272bc49..8d4759b55 100644 --- a/tools/ctrace/src/output/TraceOutputConfig.h +++ b/tools/ctrace/src/output/TraceOutputConfig.h @@ -8,6 +8,7 @@ #ifndef CTRACE_SRC_OUTPUT_TRACEOUTPUTCONFIG_H #define CTRACE_SRC_OUTPUT_TRACEOUTPUTCONFIG_H +#include "ctf/CtfMetadataModel.h" #include "TraceRoute.h" #include "TraceSelection.h" @@ -25,17 +26,6 @@ struct TraceOutputRequest { TraceSelection selection; }; -/** @brief Stores normalized source metadata required by trace outputs. */ -struct ResolvedTraceSource { - std::string type; - std::uint32_t source = 0; - TraceRouteIdentity route; - std::optional label; - std::optional address; - std::string dataType = "unsigned"; - std::uint8_t dataSize = 4U; -}; - /** @brief Configures one CSV output artifact. */ struct CsvOutputConfig { std::filesystem::path outputPath; @@ -46,13 +36,12 @@ struct CsvOutputConfig { struct CtfOutputConfig { /** @brief Creates a complete CTF output configuration. */ CtfOutputConfig(std::filesystem::path outputDirectory, std::filesystem::path traceCompassXmlPath, - std::uint64_t clockHz, TraceSelection selection, std::vector sources, - std::vector routes = {}, bool routeCatalogueConfigured = false) + TraceSelection selection, CtfMetadataTopology metadata, std::vector routes = {}, + bool routeCatalogueConfigured = false) : outputDirectory(std::move(outputDirectory)), traceCompassXmlPath(std::move(traceCompassXmlPath)), - coreClockHz(clockHz), selection(std::move(selection)), - sources(std::move(sources)), + metadata(std::move(metadata)), routes(std::move(routes)), routeCatalogueConfigured(routeCatalogueConfigured) { @@ -60,9 +49,8 @@ struct CtfOutputConfig { std::filesystem::path outputDirectory; std::filesystem::path traceCompassXmlPath; - std::uint64_t coreClockHz = 0; TraceSelection selection; - std::vector sources; + CtfMetadataTopology metadata; std::vector routes; /** @brief Distinguishes an explicit empty catalogue from legacy route inference. */ bool routeCatalogueConfigured = false; diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp index 4d4530934..f0963ca46 100644 --- a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp @@ -8,6 +8,7 @@ #include "CtfBundleOutput.h" #include "CtfEncoder.h" +#include "CtfUuid.h" #include "TraceCompassXmlWriter.h" #include "TraceEvent.h" #include "TraceOutputConfig.h" @@ -168,9 +169,8 @@ CtfBundleOutput::CtfBundleOutput(CtfOutputConfig config, DiagnosticSink* diagnos : m_ctfOutputDirectory(std::move(config.outputDirectory)), m_traceCompassXmlPath(std::move(config.traceCompassXmlPath)), m_encoder(CtfEncoderConfig{ - config.coreClockHz, + std::move(config.metadata), std::move(config.selection), - std::move(config.sources), diagnostics, std::move(config.routes), !config.routeCatalogueConfigured, @@ -207,7 +207,8 @@ void CtfBundleOutput::start() createOutputDirectory(m_ctfOutputDirectory); m_active = true; try { - m_encoder.start(m_ctfOutputDirectory); + m_traceUuid = CtfUuid::randomV4(); + m_encoder.start(m_ctfOutputDirectory, m_traceUuid); TraceCompassXmlWriter::writeFile(m_traceCompassXmlPath); } catch (...) { abort(); diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.h b/tools/ctrace/src/output/ctf/CtfBundleOutput.h index e5e058ef9..3453af35b 100644 --- a/tools/ctrace/src/output/ctf/CtfBundleOutput.h +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.h @@ -9,6 +9,7 @@ #define CTRACE_SRC_OUTPUT_CTF_CTFBUNDLEOUTPUT_H #include "CtfEncoder.h" +#include "CtfUuid.h" #include "TraceEvent.h" #include "TraceOutput.h" #include "TraceOutputConfig.h" @@ -49,6 +50,7 @@ class CtfBundleOutput final : public TraceOutput { std::filesystem::path m_ctfOutputDirectory; std::filesystem::path m_traceCompassXmlPath; CtfEncoder m_encoder; + CtfUuid m_traceUuid; bool m_active = false; }; diff --git a/tools/ctrace/src/output/ctf/CtfEncoder.cpp b/tools/ctrace/src/output/ctf/CtfEncoder.cpp index 66bf61736..4b3a26dc1 100644 --- a/tools/ctrace/src/output/ctf/CtfEncoder.cpp +++ b/tools/ctrace/src/output/ctf/CtfEncoder.cpp @@ -8,6 +8,7 @@ #include "CtfEncoder.h" #include "CtfExceptionLaneTracker.h" +#include "CtfMetadataModel.h" #include "CtfMetadataWriter.h" #include "CtfSchema.h" #include "CtfStreamWriter.h" @@ -53,52 +54,18 @@ static void validateConfiguredRoute(const CtfEncoderConfig& config, const TraceR } /** @brief Resolves the configured CTF value representation for one DWT comparator. */ -static const CtfSchema::ValueVariant& dwtValueVariant(const ResolvedTraceSource* source, std::uint32_t comparator) +static const CtfSchema::ValueVariant& dwtValueVariant(const CtfSourceDescriptor* source) { - static const ResolvedTraceSource defaults; + static const CtfSourceDescriptor defaults; const auto& resolved = source != nullptr ? *source : defaults; - const auto* variant = CtfSchema::valueVariantForTraceRunType(resolved.dataType, resolved.dataSize); - if (variant == nullptr) { - throw std::runtime_error("CTF DWT value for comparator " + std::to_string(comparator) + - " has invalid ctrace-run data-type/size metadata"); - } - return *variant; -} - -/** @brief Tests whether two routes describe equivalent CTF source metadata. */ -static bool equivalentSourceMetadata(const ResolvedTraceSource& left, const ResolvedTraceSource& right) -{ - // Trace Bus ID identifies the route, not the metadata attached to it. - return left.type == right.type && left.source == right.source && left.label == right.label && - left.address == right.address && left.dataType == right.dataType && left.dataSize == right.dataSize; + return *CtfSchema::valueVariantForTraceRunType(resolved.dataType, resolved.dataSize); } -/** @brief Finds an unambiguous configured source for one event route. */ -static const ResolvedTraceSource* resolvedTraceSource(const CtfEncoderConfig& config, const char* type, +/** @brief Finds configured source metadata for one exact event route. */ +static const CtfSourceDescriptor* resolvedTraceSource(const CtfMetadataModel& metadata, const char* type, const TraceRouteIdentity& route, std::uint32_t source) { - const auto exact = - std::find_if(config.sources.begin(), config.sources.end(), [&](const ResolvedTraceSource& candidate) { - return candidate.type == type && candidate.source == source && candidate.route == route; - }); - if (exact != config.sources.end() || route.traceBusId.has_value()) { - return exact == config.sources.end() ? nullptr : &*exact; - } - - const ResolvedTraceSource* unique = nullptr; - for (const auto& candidate : config.sources) { - if (candidate.type != type || candidate.source != source) { - continue; - } - if (unique != nullptr && !equivalentSourceMetadata(*unique, candidate)) { - throw std::runtime_error("CTF cannot resolve conflicting metadata for unformatted " + std::string(type) + - " source " + std::to_string(source)); - } - if (unique == nullptr) { - unique = &candidate; - } - } - return unique; + return metadata.source(route, type, source); } /** @brief Sign-extends a sample from its configured source width. */ @@ -163,9 +130,6 @@ static void writeDwtAddress(CtfStreamWriter::Record& record, const std::optional CtfEncoder::CtfEncoder(CtfEncoderConfig config) : m_config(std::move(config)) { - if (m_config.coreClockHz == 0U) { - throw std::invalid_argument("CTF output requires a non-zero timestamps.clock"); - } } CtfEncoder::~CtfEncoder() @@ -173,17 +137,21 @@ CtfEncoder::~CtfEncoder() abort(); } -void CtfEncoder::start(const std::filesystem::path& outputDirectory) +void CtfEncoder::start(const std::filesystem::path& outputDirectory, const CtfUuid& traceUuid) { abort(); m_outputDirectory = outputDirectory; try { - m_routeIdentities.clear(); + m_metadata.emplace(traceUuid, m_config.metadata); + if (!m_metadata->isLegacySingleStreamLayout()) { + throw std::runtime_error("CTF binary output currently requires exactly one legacy SINGLE stream topology"); + } m_bootstrappedRoutes.clear(); m_streamStates.clear(); m_reportedDwtSizeMismatches.clear(); m_exceptionLanes.clear(); - m_stream.open(m_outputDirectory / "stream_0", CtfSchema::SwoStreamId); + const auto& runtimeStream = m_metadata->topology().streams.front(); + m_stream.open(m_outputDirectory / "stream_0", runtimeStream.streamClassId, traceUuid); m_recording = true; std::map initialRoutes; const auto addInitialRoute = [&](const TraceRouteIdentity& route) { @@ -195,16 +163,21 @@ void CtfEncoder::start(const std::filesystem::path& outputDirectory) for (const auto& route : m_config.routes) { addInitialRoute(route); } - for (const auto& source : m_config.sources) { + for (const auto& stream : m_metadata->topology().streams) { + validateConfiguredRoute(m_config, stream.route); + if (m_config.legacyRouteFallback && m_config.routes.empty()) { + addInitialRoute(stream.route); + } + } + for (const auto& source : m_metadata->topology().sources) { validateConfiguredRoute(m_config, source.route); if (m_config.legacyRouteFallback && m_config.routes.empty()) { addInitialRoute(source.route); } } - for (const auto& [routeId, route] : initialRoutes) { - (void)routeId; - if (m_config.selection.includesRoute(route)) { - bootstrapRoute(route); + for (const auto& stream : m_metadata->topology().streams) { + if (m_config.selection.includesRoute(stream.route)) { + bootstrapRoute(stream.route); } } } catch (...) { @@ -218,10 +191,6 @@ void CtfEncoder::stop() if (!m_recording) { return; } - if (m_bootstrappedRoutes.empty() && m_config.legacyRouteFallback && m_config.routes.empty() && - m_config.sources.empty() && m_config.selection.includesRoute({})) { - bootstrapRoute({}); - } m_recording = false; m_stream.close(); writeMetadataFile(); @@ -231,6 +200,7 @@ void CtfEncoder::abort() noexcept { m_recording = false; m_stream.abort(); + m_metadata.reset(); m_outputDirectory.clear(); } @@ -243,6 +213,11 @@ void CtfEncoder::writeEvent(const TraceEvent& event) return; } validateConfiguredRoute(m_config, event.route); + const auto* stream = m_metadata->streamForRoute(event.route); + if (stream == nullptr || stream->streamClassId != m_metadata->topology().streams.front().streamClassId) { + throw std::runtime_error( + "CTF binary output cannot encode an event route without an exact runtime stream descriptor"); + } bootstrapRoute(event.route); if (!isTraceEvent(event) && event.tcyc.has_value()) { auto& eventTimestamp = streamState(event.route).eventTimestamp; @@ -345,10 +320,6 @@ std::uint64_t CtfEncoder::allocateEventTimestamp(const TraceRouteIdentity& route CtfEncoder::StreamState& CtfEncoder::streamState(const TraceRouteIdentity& route) { - const auto [identity, inserted] = m_routeIdentities.emplace(route.id, route); - if (!inserted && identity->second != route) { - throw std::runtime_error("CTF event route identity does not match normalized route catalogue"); - } return m_streamStates[route.id]; } @@ -385,9 +356,9 @@ void CtfEncoder::writeSoftwareEvent(const TraceEvent& event, const SoftwareTrace void CtfEncoder::writeDwtValueEvent(const TraceEvent& event, const DwtDataTraceEvent& data) { - const auto* source = resolvedTraceSource(m_config, "dwt", event.route, data.comparator); + const auto* source = resolvedTraceSource(*m_metadata, "dwt", event.route, data.comparator); reportDwtSizeMismatch(event, data, source); - const auto& variant = dwtValueVariant(source, data.comparator); + const auto& variant = dwtValueVariant(source); const auto& pcVariant = dwtAddressVariant(data.pc); const auto& addressVariant = dwtAddressVariant(data.address); const auto payloadSize = @@ -411,9 +382,9 @@ void CtfEncoder::writeDwtValueEvent(const TraceEvent& event, const DwtDataTraceE } void CtfEncoder::reportDwtSizeMismatch(const TraceEvent& event, const DwtDataTraceEvent& data, - const ResolvedTraceSource* source) + const CtfSourceDescriptor* source) { - const auto configuredSize = source != nullptr ? source->dataSize : ResolvedTraceSource{}.dataSize; + const auto configuredSize = source != nullptr ? source->dataSize : CtfSourceDescriptor{}.dataSize; if (configuredSize == data.size || m_config.diagnostics == nullptr || !m_reportedDwtSizeMismatches.insert({event.route.id, data.comparator}).second) { return; @@ -425,9 +396,6 @@ void CtfEncoder::reportDwtSizeMismatch(const TraceEvent& event, const DwtDataTra {"configuredSize", std::to_string(configuredSize)}, {"swoSize", std::to_string(data.size)}, }; - if (event.route.traceBusId.has_value()) { - context.emplace_back("stream", std::to_string(*event.route.traceBusId)); - } m_config.diagnostics->report({ DiagnosticSink::Severity::Warning, "configured ctrace-run size does not match the decoded SWO payload size", @@ -621,14 +589,12 @@ std::pair CtfEncoder::computeSampleQuality(const Tr void CtfEncoder::writeMetadataFile() { - std::set observedExceptionNumbers; + const auto streamClassId = m_metadata->topology().streams.front().streamClassId; for (const auto& [routeId, lane] : m_exceptionLanes) { (void)routeId; - observedExceptionNumbers.insert(lane.observedExceptionNumbers().begin(), lane.observedExceptionNumbers().end()); + for (const auto number : lane.observedExceptionNumbers()) { + m_metadata->observeException(streamClassId, number); + } } - CtfMetadataWriter::write(m_outputDirectory, m_stream.uuidString(), m_config.coreClockHz, m_config.sources, - { - observedExceptionNumbers.begin(), - observedExceptionNumbers.end(), - }); + CtfMetadataWriter::write(m_outputDirectory, *m_metadata); } diff --git a/tools/ctrace/src/output/ctf/CtfEncoder.h b/tools/ctrace/src/output/ctf/CtfEncoder.h index 5dd7ba1f4..c6a9f023d 100644 --- a/tools/ctrace/src/output/ctf/CtfEncoder.h +++ b/tools/ctrace/src/output/ctf/CtfEncoder.h @@ -9,7 +9,9 @@ #define CTRACE_SRC_OUTPUT_CTF_CTFENCODER_H #include "CtfExceptionLaneTracker.h" +#include "CtfMetadataModel.h" #include "CtfStreamWriter.h" +#include "CtfUuid.h" #include "TraceSelection.h" #include "TraceEvent.h" #include "TraceOutputConfig.h" @@ -18,17 +20,17 @@ #include #include #include +#include #include #include #include class DiagnosticSink; -/** @brief Stores the clock, selection, sources, and diagnostics for CTF encoding. */ +/** @brief Stores metadata topology, selection, route catalogue, and diagnostics for CTF encoding. */ struct CtfEncoderConfig { - std::uint64_t coreClockHz = 0; + CtfMetadataTopology metadata; TraceSelection selection; - std::vector sources; DiagnosticSink* diagnostics = nullptr; std::vector routes; /** @brief Permits direct legacy callers to infer a route when no catalogue was supplied. */ @@ -49,7 +51,7 @@ class CtfEncoder final { CtfEncoder& operator=(const CtfEncoder&) = delete; /** @brief Starts writing into a prepared CTF directory. */ - void start(const std::filesystem::path& outputDirectory); + void start(const std::filesystem::path& outputDirectory, const CtfUuid& traceUuid); /** @brief Completes stream data and writes final metadata. */ void stop(); /** @brief Aborts stream output without throwing. */ @@ -80,7 +82,7 @@ class CtfEncoder final { /** @brief Encodes one DWT data value event. */ void writeDwtValueEvent(const TraceEvent& event, const DwtDataTraceEvent& data); /** @brief Reports configured and decoded DWT width mismatches once per route. */ - void reportDwtSizeMismatch(const TraceEvent& event, const DwtDataTraceEvent& data, const ResolvedTraceSource* source); + void reportDwtSizeMismatch(const TraceEvent& event, const DwtDataTraceEvent& data, const CtfSourceDescriptor* source); /** @brief Encodes one DWT address event. */ void writeDwtAddrEvent(const TraceEvent& event, const DwtAddressTraceEvent& address); /** @brief Encodes one comparator-only DWT match event. */ @@ -105,9 +107,9 @@ class CtfEncoder final { CtfEncoderConfig m_config; std::filesystem::path m_outputDirectory; + std::optional m_metadata; CtfStreamWriter m_stream; bool m_recording = false; - std::map m_routeIdentities; std::set m_bootstrappedRoutes; std::map m_streamStates; std::set> m_reportedDwtSizeMismatches; diff --git a/tools/ctrace/src/output/ctf/CtfMetadataModel.cpp b/tools/ctrace/src/output/ctf/CtfMetadataModel.cpp new file mode 100644 index 000000000..cca6614ed --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfMetadataModel.cpp @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfMetadataModel.h" + +#include "CtfSchema.h" +#include "TraceStreamId.h" + +#include +#include +#include +#include +#include + +/** @brief Tests whether a name is safe for use as a TSDL identifier. */ +static bool isTsdlIdentifier(const std::string& name) +{ + if (name.empty() || (std::isalpha(static_cast(name.front())) == 0 && name.front() != '_')) { + return false; + } + return std::all_of(name.begin() + 1, name.end(), [](char character) { + const auto value = static_cast(character); + return std::isalnum(value) != 0 || character == '_'; + }); +} + +/** @brief Tests whether two source descriptors carry identical metadata. */ +static bool equivalentSource(const CtfSourceDescriptor& left, const CtfSourceDescriptor& right) +{ + return left.type == right.type && left.source == right.source && left.route == right.route && + left.label == right.label && left.address == right.address && left.dataType == right.dataType && + left.dataSize == right.dataSize; +} + +CtfMetadataModel::CtfMetadataModel(CtfUuid traceUuid, CtfMetadataTopology topology) + : m_traceUuid(std::move(traceUuid)), + m_topology(std::move(topology)) +{ + std::sort( + m_topology.clockDomains.begin(), m_topology.clockDomains.end(), + [](const CtfClockDomainDescriptor& left, const CtfClockDomainDescriptor& right) { return left.id < right.id; }); + std::sort(m_topology.streams.begin(), m_topology.streams.end(), + [](const CtfStreamDescriptor& left, const CtfStreamDescriptor& right) { + return std::tie(left.streamClassId, left.route.id) < std::tie(right.streamClassId, right.route.id); + }); + std::sort(m_topology.sources.begin(), m_topology.sources.end(), + [](const CtfSourceDescriptor& left, const CtfSourceDescriptor& right) { + return std::tie(left.route.id, left.type, left.source) < + std::tie(right.route.id, right.type, right.source); + }); + validate(); +} + +const CtfUuid& CtfMetadataModel::traceUuid() const noexcept +{ + return m_traceUuid; +} + +const CtfMetadataTopology& CtfMetadataModel::topology() const noexcept +{ + return m_topology; +} + +const CtfStreamDescriptor* CtfMetadataModel::streamForRoute(const TraceRouteIdentity& route) const noexcept +{ + const auto found = std::find_if(m_topology.streams.begin(), m_topology.streams.end(), + [&](const CtfStreamDescriptor& stream) { return stream.route == route; }); + return found == m_topology.streams.end() ? nullptr : &*found; +} + +const CtfClockDomainDescriptor* CtfMetadataModel::clockDomain(CtfClockDomainId id) const noexcept +{ + const auto found = std::lower_bound( + m_topology.clockDomains.begin(), m_topology.clockDomains.end(), id, + [](const CtfClockDomainDescriptor& clock, CtfClockDomainId candidate) { return clock.id < candidate; }); + return found == m_topology.clockDomains.end() || found->id != id ? nullptr : &*found; +} + +const CtfSourceDescriptor* CtfMetadataModel::source(const TraceRouteIdentity& route, const char* type, + std::uint32_t sourceNumber) const noexcept +{ + const auto found = + std::find_if(m_topology.sources.begin(), m_topology.sources.end(), [&](const CtfSourceDescriptor& candidate) { + return candidate.route == route && candidate.type == type && candidate.source == sourceNumber; + }); + return found == m_topology.sources.end() ? nullptr : &*found; +} + +void CtfMetadataModel::observeException(CtfStreamClassId streamClassId, ExceptionNumber number) +{ + const auto stream = + std::find_if(m_topology.streams.begin(), m_topology.streams.end(), + [&](const CtfStreamDescriptor& candidate) { return candidate.streamClassId == streamClassId; }); + if (stream == m_topology.streams.end()) { + throw std::runtime_error("CTF exception observation references an unknown stream class"); + } + m_observedExceptions[streamClassId].insert(number); +} + +std::vector CtfMetadataModel::observedExceptions(CtfStreamClassId streamClassId) const +{ + const auto found = m_observedExceptions.find(streamClassId); + return found == m_observedExceptions.end() ? std::vector{} + : std::vector{found->second.begin(), found->second.end()}; +} + +bool CtfMetadataModel::isLegacySingleStreamLayout() const noexcept +{ + if (m_topology.clockDomains.size() != 1U || m_topology.streams.size() != 1U) { + return false; + } + const auto& clock = m_topology.clockDomains.front(); + const auto& stream = m_topology.streams.front(); + return clock.id == CtfClockDomainId{0U} && clock.name == "swo_clock" && !clock.uuid.has_value() && !clock.absolute && + stream.streamClassId == CtfStreamClassId{0U} && !stream.route.traceBusId.has_value() && + stream.sourceKind == CtfSourceKind::Itm && stream.clockDomainId == clock.id; +} + +void CtfMetadataModel::validate() const +{ + std::set clockIds; + std::set clockNames; + std::set clockUuids; + for (const auto& clock : m_topology.clockDomains) { + if (!clockIds.insert(clock.id).second) { + throw std::invalid_argument("CTF metadata contains a duplicate clock-domain ID"); + } + if (!isTsdlIdentifier(clock.name) || !clockNames.insert(clock.name).second) { + throw std::invalid_argument("CTF metadata requires unique valid clock-domain names"); + } + if (clock.frequencyHz == 0U) { + throw std::invalid_argument("CTF metadata requires a non-zero clock-domain frequency"); + } + if (clock.uuid.has_value() && (*clock.uuid == m_traceUuid || !clockUuids.insert(*clock.uuid).second)) { + throw std::invalid_argument("CTF clock UUIDs must be distinct from the trace and other clock domains"); + } + } + + std::set streamIds; + std::set referencedClockIds; + std::map routeIdentities; + for (const auto& stream : m_topology.streams) { + const auto [route, inserted] = routeIdentities.emplace(stream.route.id, stream.route); + if (!inserted) { + throw std::invalid_argument(route->second == stream.route + ? "CTF metadata contains a duplicate normalized route" + : "CTF metadata contains inconsistent normalized route identities"); + } + if (!streamIds.insert(stream.streamClassId).second) { + throw std::invalid_argument("CTF metadata contains a duplicate stream-class ID"); + } + if (clockDomain(stream.clockDomainId) == nullptr) { + throw std::invalid_argument("CTF stream class references an unknown clock domain"); + } + referencedClockIds.insert(stream.clockDomainId); + if (stream.sourceKind == CtfSourceKind::Itm && stream.route.traceBusId.has_value() && + !CoreSight::isAtbTraceId(*stream.route.traceBusId)) { + throw std::invalid_argument("CTF ITM stream route requires a CoreSight ATB trace ID between 1 and 111"); + } + const auto expectedStreamClassId = CtfStreamClassId{stream.route.traceBusId.value_or(0U)}; + if (stream.streamClassId != expectedStreamClassId) { + throw std::invalid_argument("CTF stream-class ID does not match its normalized route identity"); + } + } + + for (const auto& clock : m_topology.clockDomains) { + if (referencedClockIds.find(clock.id) == referencedClockIds.end()) { + throw std::invalid_argument("CTF metadata contains a clock domain without a referencing stream class"); + } + } + + for (std::size_t index = 0U; index < m_topology.sources.size(); ++index) { + const auto& source = m_topology.sources[index]; + if (streamForRoute(source.route) == nullptr) { + throw std::invalid_argument("CTF source metadata references an unknown normalized route"); + } + if (source.type == "itm") { + if (source.source == CoreSight::kExcludedItmStimulusPort || !CoreSight::isItmStimulusPort(source.source)) { + throw std::invalid_argument("CTF ITM source metadata requires a channel between 1 and 31"); + } + } else if (source.type == "dwt") { + if (source.source > 3U) { + throw std::invalid_argument("CTF DWT source metadata requires a comparator between 0 and 3"); + } + if (CtfSchema::valueVariantForTraceRunType(source.dataType, source.dataSize) == nullptr) { + throw std::invalid_argument("CTF DWT source metadata has an invalid data-type/size combination"); + } + const auto extent = static_cast(source.dataSize - 1U); + if (source.address.has_value() && *source.address > std::numeric_limits::max() - extent) { + throw std::invalid_argument("CTF DWT source address range exceeds the unsigned 64-bit metadata domain"); + } + } else { + throw std::invalid_argument("CTF source metadata type must be 'itm' or 'dwt'"); + } + if (index > 0U) { + const auto& previous = m_topology.sources[index - 1U]; + const auto sameKey = + previous.route.id == source.route.id && previous.type == source.type && previous.source == source.source; + if (sameKey) { + throw std::invalid_argument(equivalentSource(previous, source) + ? "CTF metadata contains duplicate source metadata" + : "CTF metadata contains conflicting source metadata for one route"); + } + } + } + + if (!isLegacySingleStreamLayout()) { + for (const auto& clock : m_topology.clockDomains) { + if (!clock.uuid.has_value()) { + throw std::invalid_argument("non-legacy CTF clock domains require an explicit UUID"); + } + } + } +} diff --git a/tools/ctrace/src/output/ctf/CtfMetadataModel.h b/tools/ctrace/src/output/ctf/CtfMetadataModel.h new file mode 100644 index 000000000..18a609160 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfMetadataModel.h @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_CTF_CTFMETADATAMODEL_H +#define CTRACE_SRC_OUTPUT_CTF_CTFMETADATAMODEL_H + +#include "CtfUuid.h" +#include "TraceEvent.h" +#include "TraceRoute.h" + +#include +#include +#include +#include +#include +#include + +/** @brief Identifies one CTF stream class independently from a trace route. */ +class CtfStreamClassId final { +public: + /** @brief Creates CTF stream-class ID zero. */ + constexpr CtfStreamClassId() = default; + + /** @brief Creates a CTF stream-class ID from its encoded value. */ + explicit constexpr CtfStreamClassId(std::uint32_t value) + : m_value(value) + { + } + + /** @brief Returns the encoded stream-class ID. */ + constexpr std::uint32_t value() const noexcept + { + return m_value; + } + +private: + std::uint32_t m_value = 0U; +}; + +/** @brief Compares CTF stream-class IDs. */ +constexpr bool operator==(CtfStreamClassId left, CtfStreamClassId right) noexcept +{ + return left.value() == right.value(); +} + +/** @brief Compares CTF stream-class IDs. */ +constexpr bool operator!=(CtfStreamClassId left, CtfStreamClassId right) noexcept +{ + return !(left == right); +} + +/** @brief Orders CTF stream-class IDs. */ +constexpr bool operator<(CtfStreamClassId left, CtfStreamClassId right) noexcept +{ + return left.value() < right.value(); +} + +/** @brief Identifies one CTF clock domain independently from frequency. */ +class CtfClockDomainId final { +public: + /** @brief Creates CTF clock-domain ID zero. */ + constexpr CtfClockDomainId() = default; + + /** @brief Creates a CTF clock-domain ID from its bundle-local value. */ + explicit constexpr CtfClockDomainId(std::uint32_t value) + : m_value(value) + { + } + + /** @brief Returns the bundle-local clock-domain ID. */ + constexpr std::uint32_t value() const noexcept + { + return m_value; + } + +private: + std::uint32_t m_value = 0U; +}; + +/** @brief Compares CTF clock-domain IDs. */ +constexpr bool operator==(CtfClockDomainId left, CtfClockDomainId right) noexcept +{ + return left.value() == right.value(); +} + +/** @brief Compares CTF clock-domain IDs. */ +constexpr bool operator!=(CtfClockDomainId left, CtfClockDomainId right) noexcept +{ + return !(left == right); +} + +/** @brief Orders CTF clock-domain IDs. */ +constexpr bool operator<(CtfClockDomainId left, CtfClockDomainId right) noexcept +{ + return left.value() < right.value(); +} + +/** @brief Identifies the semantic source family represented by a CTF stream. */ +enum class CtfSourceKind { + Itm, +}; + +/** @brief Describes one counter/timebase declaration in a CTF bundle. */ +struct CtfClockDomainDescriptor { + CtfClockDomainId id; + std::string name; + std::optional uuid; + std::uint64_t frequencyHz = 0U; + bool absolute = false; +}; + +/** @brief Describes one normalized route's CTF stream-class identity. */ +struct CtfStreamDescriptor { + CtfStreamClassId streamClassId; + TraceRouteIdentity route; + CtfSourceKind sourceKind = CtfSourceKind::Itm; + std::optional processorName; + CtfClockDomainId clockDomainId; +}; + +/** @brief Describes one configured ITM or DWT source on an exact route. */ +struct CtfSourceDescriptor { + std::string type; + std::uint32_t source = 0U; + TraceRouteIdentity route; + std::optional label; + std::optional address; + std::string dataType = "unsigned"; + std::uint8_t dataSize = 4U; +}; + +/** @brief Stores the configured stream, clock, and source topology for one CTF bundle. */ +struct CtfMetadataTopology { + std::vector clockDomains; + std::vector streams; + std::vector sources; +}; + +/** @brief Owns validated CTF metadata and runtime observations for one bundle. */ +class CtfMetadataModel final { +public: + /** @brief Creates and validates a bundle-local metadata model. */ + CtfMetadataModel(CtfUuid traceUuid, CtfMetadataTopology topology); + + /** @brief Returns the trace UUID shared by metadata and packet headers. */ + const CtfUuid& traceUuid() const noexcept; + /** @brief Returns the canonical configured topology. */ + const CtfMetadataTopology& topology() const noexcept; + /** @brief Returns the stream descriptor for one exact normalized route. */ + const CtfStreamDescriptor* streamForRoute(const TraceRouteIdentity& route) const noexcept; + /** @brief Returns the clock-domain descriptor with one bundle-local ID. */ + const CtfClockDomainDescriptor* clockDomain(CtfClockDomainId id) const noexcept; + /** @brief Returns exact configured source metadata for one route/type/source key. */ + const CtfSourceDescriptor* source(const TraceRouteIdentity& route, const char* type, + std::uint32_t source) const noexcept; + /** @brief Records one exception number observed on a concrete stream class. */ + void observeException(CtfStreamClassId streamClassId, ExceptionNumber number); + /** @brief Returns sorted exception numbers observed on one stream class. */ + std::vector observedExceptions(CtfStreamClassId streamClassId) const; + /** @brief Tests whether this topology uses the exact legacy single-stream CTF layout. */ + bool isLegacySingleStreamLayout() const noexcept; + +private: + /** @brief Validates topology identity, references, and metadata uniqueness. */ + void validate() const; + + CtfUuid m_traceUuid; + CtfMetadataTopology m_topology; + std::map> m_observedExceptions; +}; + +#endif // CTRACE_SRC_OUTPUT_CTF_CTFMETADATAMODEL_H diff --git a/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp b/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp index 5a22e9069..1c29fb9a0 100644 --- a/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp +++ b/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp @@ -7,8 +7,8 @@ #include "CtfMetadataWriter.h" +#include "CtfMetadataModel.h" #include "CtfSchema.h" -#include "TraceOutputConfig.h" #include #include @@ -202,7 +202,7 @@ struct MetadataSymbols { }; /** @brief Collects deduplicated source and exception symbols for metadata. */ -static MetadataSymbols collectMetadataSymbols(const std::vector& sources) +static MetadataSymbols collectMetadataSymbols(const std::vector& sources) { MetadataSymbols symbols; for (const auto& source : sources) { @@ -213,9 +213,6 @@ static MetadataSymbols collectMetadataSymbols(const std::vector& sources, - const std::vector& observedExceptionNumbers) +/** @brief Returns source metadata attached to one exact stream route. */ +static std::vector sourcesForStream(const CtfMetadataModel& model, + const CtfStreamDescriptor& stream) +{ + std::vector sources; + for (const auto& source : model.topology().sources) { + if (source.route == stream.route) { + sources.push_back(source); + } + } + return sources; +} + +/** @brief Returns the unique TSDL namespace prefix of one non-legacy stream. */ +static std::string streamSymbolPrefix(const CtfStreamDescriptor& stream) +{ + return "cmsis_stream_" + std::to_string(stream.streamClassId.value()); +} + +/** @brief Writes common trace, environment, and clock declarations for a generalized topology. */ +static void writeGeneralTraceEnvironment(std::ostream& out, const CtfMetadataModel& model) +{ + out << R"(/* CTF 1.8 */ +trace { + major = 1; + minor = 8; + uuid = ")" + << model.traceUuid().toString() << R"("; + byte_order = le; + packet.header := struct { + integer { size = 32; align = 8; signed = false; } magic; + integer { size = 8; align = 8; signed = false; } uuid[16]; + integer { size = 32; align = 8; signed = false; } stream_id; + }; +}; + +env { + cmsis_ctf_profile = "cmsis.ctf"; + cmsis_ctf_profile_version = 1; +)"; + for (const auto& stream : model.topology().streams) { + const auto prefix = streamSymbolPrefix(stream); + const auto symbols = collectMetadataSymbols(sourcesForStream(model, stream)); + for (const auto& entry : symbols.dwtValueTypes) { + out << " " << prefix << "_dwt" << entry.first << "_value_type = " << tsdlString(entry.second) << ";\n"; + } + for (const auto& entry : symbols.dwtAddressStarts) { + out << " " << prefix << "_dwt" << entry.first + << "_address_start = " << tsdlString("0x" + hexValue(entry.second)) << ";\n"; + } + for (const auto& entry : symbols.dwtAddressEnds) { + out << " " << prefix << "_dwt" << entry.first << "_address_end = " << tsdlString("0x" + hexValue(entry.second)) + << ";\n"; + } + } + out << "};\n"; + for (const auto& clock : model.topology().clockDomains) { + out << "\nclock {\n" + << " name = " << clock.name << ";\n"; + if (clock.uuid.has_value()) { + out << " uuid = \"" << clock.uuid->toString() << "\";\n"; + } + out << " precision = 0;\n" + << " offset_s = 0;\n" + << " offset = 0;\n" + << " absolute = " << (clock.absolute ? "true" : "false") << ";\n" + << " freq = " << clock.frequencyHz << ";\n" + << "};\n"; + } +} + +/** @brief Writes primitive and fixed CMSIS types shared by all generalized streams. */ +static void writeGeneralCommonTypes(std::ostream& out, const CtfMetadataModel& model) +{ + out << R"( +typealias integer { size = 8; align = 8; signed = false; } := uint8_t; +typealias integer { size = 16; align = 8; signed = false; byte_order = le; } := uint16_t; +typealias integer { size = 32; align = 8; signed = false; byte_order = le; } := uint32_t; +typealias integer { size = 8; align = 8; signed = true; } := int8_t; +typealias integer { size = 16; align = 8; signed = true; byte_order = le; } := int16_t; +typealias integer { size = 32; align = 8; signed = true; byte_order = le; } := int32_t; +typealias floating_point { exp_dig = 8; mant_dig = 24; align = 8; byte_order = le; } := ieee_float32_t; +typealias integer { size = 64; align = 8; signed = false; byte_order = le; } := uint64_t; +)"; + for (const auto& clock : model.topology().clockDomains) { + out << "typealias integer { size = 64; align = 8; signed = false; map = clock." << clock.name + << ".value; } := " << clock.name << "_t;\n"; + } + out << R"( +typealias enum : uint8_t { + "read" = )" + << static_cast(CtfSchema::value(CtfSchema::DwtAccess::Read)) << R"(, + "write" = )" + << static_cast(CtfSchema::value(CtfSchema::DwtAccess::Write)) << R"( +} := cmsis_dwt_access_t; +typealias enum : uint8_t { + "trace_start" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart)) << R"(, + "resync" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::Resync)) << R"(, + "overflow" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::Overflow)) << R"(, + "decode_error" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError)) << R"(, + "data_loss" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss)) << R"( +} := cmsis_trace_status_reason_t; +typealias enum : uint8_t { + "entered" = )" + << static_cast(CtfSchema::value(CtfSchema::ExceptionAction::Entered)) << R"(, + "exited" = )" + << static_cast(CtfSchema::value(CtfSchema::ExceptionAction::Exited)) << R"(, + "returned" = )" + << static_cast(CtfSchema::value(CtfSchema::ExceptionAction::Returned)) << R"( +} := cmsis_exception_action_t; +typealias enum : uint8_t { + "trace" = )" + << static_cast(CtfSchema::value(CtfSchema::ExceptionOrigin::Trace)) << R"(, + "synthetic" = )" + << static_cast(CtfSchema::value(CtfSchema::ExceptionOrigin::Synthetic)) << R"( +} := cmsis_exception_origin_t; +typealias enum : uint8_t { +)"; + for (const auto counter : kDwtEventCounters) { + out << " \"" << CtfSchema::dwtEventCounterName(counter) + << "\" = " << static_cast(CtfSchema::value(counter)) << ",\n"; + } + out << R"(} := cmsis_dwt_event_counter_t; +typealias enum : uint8_t { +)"; + for (const auto counter : kPmuEventCounters) { + out << " \"" << CtfSchema::pmuEventCounterName(counter) + << "\" = " << static_cast(CtfSchema::value(counter)) << ",\n"; + } + out << "} := cmsis_pmu_event_counter_t;\n"; +} + +/** @brief Writes route-specific channel, comparator, and exception types. */ +static void writeGeneralStreamTypes(std::ostream& out, const CtfMetadataModel& model, const CtfStreamDescriptor& stream, + const MetadataSymbols& symbols) +{ + const auto prefix = streamSymbolPrefix(stream); + out << "typealias enum : uint8_t {\n"; + std::set itmLabels; + for (std::uint32_t channel = 1U; channel < 32U; ++channel) { + const auto fallback = "ITM" + std::to_string(channel); + out << " " << tsdlString(uniqueEnumLabel(mapValueOrEmpty(symbols.itmNames, channel), fallback, itmLabels)) + << " = " << channel << ",\n"; + } + out << "} := " << prefix << "_itm_channel_t;\n" + << "typealias enum : uint8_t {\n"; + std::set dwtLabels; + for (std::uint32_t comparator = 0U; comparator < 4U; ++comparator) { + const auto fallback = "DWT" + std::to_string(comparator); + out << " " << tsdlString(uniqueEnumLabel(mapValueOrEmpty(symbols.dwtNames, comparator), fallback, dwtLabels)) + << " = " << comparator << ",\n"; + } + out << "} := " << prefix << "_dwt_comparator_t;\n" + << "typealias enum : uint16_t {\n"; + for (const auto number : exceptionNumbersWithDefaults(model.observedExceptions(stream.streamClassId))) { + out << " " << tsdlString(exceptionName(number)) << " = " << number << ",\n"; + } + out << "} := " << prefix << "_exception_number_t;\n"; +} + +/** @brief Writes every stream and event declaration of a generalized topology. */ +static void writeGeneralStreamSchemas(std::ostream& out, const CtfMetadataModel& model) +{ + for (const auto& stream : model.topology().streams) { + const auto* clock = model.clockDomain(stream.clockDomainId); + const auto prefix = streamSymbolPrefix(stream); + const auto streamClassId = stream.streamClassId.value(); + const auto symbols = collectMetadataSymbols(sourcesForStream(model, stream)); + writeGeneralStreamTypes(out, model, stream, symbols); + writeStreamDefinition(out, streamClassId, clock->name + "_t"); + writeItmEvent(out, streamClassId, prefix + "_itm_channel_t"); + writeDwtValueEvent(out, streamClassId, prefix + "_dwt_comparator_t"); + writeDwtAddressEvent(out, streamClassId, prefix + "_dwt_comparator_t"); + writeDwtMatchEvent(out, streamClassId, prefix + "_dwt_comparator_t"); + writeDwtEvent(out, streamClassId); + writePmuEvent(out, streamClassId); + writeStatusEvents(out, streamClassId, prefix + "_exception_number_t"); + writePcSampleEvent(out, streamClassId); + } +} + +void CtfMetadataWriter::write(const std::filesystem::path& outputDir, const CtfMetadataModel& model) { const auto metadataPath = outputDir / "metadata"; std::ofstream out(metadataPath, std::ios::out | std::ios::trunc); @@ -593,18 +788,25 @@ void CtfMetadataWriter::write(const std::filesystem::path& outputDir, const std: throw std::runtime_error("Failed to write CTF metadata " + metadataPath.string()); } - const auto symbols = collectMetadataSymbols(sources); - writeTraceEnvironment(out, uuidString, coreClockHz, symbols); - writeTypeDefinitions(out, symbols, observedExceptionNumbers); - writeStreamDefinition(out); - writeItmEvent(out); - writeDwtValueEvent(out); - writeDwtAddressEvent(out); - writeDwtMatchEvent(out); - writeDwtEvent(out); - writePmuEvent(out); - writeStatusEvents(out); - writePcSampleEvent(out); + const auto& topology = model.topology(); + if (model.isLegacySingleStreamLayout()) { + const auto symbols = collectMetadataSymbols(topology.sources); + writeTraceEnvironment(out, model.traceUuid().toString(), topology.clockDomains.front().frequencyHz, symbols); + writeTypeDefinitions(out, symbols, model.observedExceptions(topology.streams.front().streamClassId)); + writeStreamDefinition(out); + writeItmEvent(out); + writeDwtValueEvent(out); + writeDwtAddressEvent(out); + writeDwtMatchEvent(out); + writeDwtEvent(out); + writePmuEvent(out); + writeStatusEvents(out); + writePcSampleEvent(out); + } else { + writeGeneralTraceEnvironment(out, model); + writeGeneralCommonTypes(out, model); + writeGeneralStreamSchemas(out, model); + } out.close(); if (!out) { throw std::runtime_error("Failed to write CTF metadata " + metadataPath.string()); diff --git a/tools/ctrace/src/output/ctf/CtfMetadataWriter.h b/tools/ctrace/src/output/ctf/CtfMetadataWriter.h index e385117ee..3564dd25c 100644 --- a/tools/ctrace/src/output/ctf/CtfMetadataWriter.h +++ b/tools/ctrace/src/output/ctf/CtfMetadataWriter.h @@ -8,21 +8,15 @@ #ifndef CTRACE_SRC_OUTPUT_CTF_CTFMETADATAWRITER_H #define CTRACE_SRC_OUTPUT_CTF_CTFMETADATAWRITER_H -#include "TraceEvent.h" -#include "TraceOutputConfig.h" +#include "CtfMetadataModel.h" -#include #include -#include -#include /** @brief Writes the CTF metadata description for a completed trace bundle. */ class CtfMetadataWriter final { public: - /** @brief Writes metadata for clock, event schemas, sources, and exception lanes. */ - static void write(const std::filesystem::path& outputDir, const std::string& uuidString, std::uint64_t coreClockHz, - const std::vector& sources, - const std::vector& observedExceptionNumbers); + /** @brief Serializes one complete bundle-local metadata model. */ + static void write(const std::filesystem::path& outputDir, const CtfMetadataModel& model); private: /** @brief Prevents construction of this stateless metadata utility. */ diff --git a/tools/ctrace/src/output/ctf/CtfStreamWriter.cpp b/tools/ctrace/src/output/ctf/CtfStreamWriter.cpp index adc26a6c7..39524e0f3 100644 --- a/tools/ctrace/src/output/ctf/CtfStreamWriter.cpp +++ b/tools/ctrace/src/output/ctf/CtfStreamWriter.cpp @@ -7,19 +7,16 @@ #include "CtfStreamWriter.h" +#include "CtfMetadataModel.h" #include "CtfSchema.h" +#include "CtfUuid.h" #include -#include #include #include #include -#include #include -#include -#include #include -#include #include constexpr std::size_t kPacketSizeBytes = 65536U; @@ -27,19 +24,6 @@ constexpr std::size_t kPacketHeaderSize = 24U; constexpr std::size_t kPacketContextSize = 32U; constexpr std::size_t kPacketOverhead = kPacketHeaderSize + kPacketContextSize; constexpr std::size_t kEventPrefixSize = 13U; -/** @brief Formats a binary UUID in canonical textual form. */ -static std::string formatUuid(const std::array& uuid) -{ - std::ostringstream out; - out << std::hex << std::setfill('0'); - for (std::size_t index = 0; index < uuid.size(); ++index) { - if (index == 4U || index == 6U || index == 8U || index == 10U) { - out << '-'; - } - out << std::setw(2) << static_cast(uuid[index]); - } - return out.str(); -} CtfStreamWriter::Record::Record(std::vector& buffer, std::size_t offset, std::size_t endOffset) : m_buffer(buffer), @@ -89,22 +73,15 @@ CtfStreamWriter::~CtfStreamWriter() abort(); } -void CtfStreamWriter::open(const std::filesystem::path& filePath, std::uint32_t streamId) +void CtfStreamWriter::open(const std::filesystem::path& filePath, CtfStreamClassId streamClassId, + const CtfUuid& traceUuid) { abort(); m_filePath = filePath; - m_streamId = streamId; + m_streamId = streamClassId.value(); m_packetSequence = 0U; m_lastTimestamp.reset(); - m_uuid.fill(0U); - - std::random_device random; - for (auto& byte : m_uuid) { - byte = static_cast(random()); - } - m_uuid[6] = static_cast((m_uuid[6] & 0x0fU) | 0x40U); - m_uuid[8] = static_cast((m_uuid[8] & 0x3fU) | 0x80U); - m_uuidString = formatUuid(m_uuid); + m_traceUuid = traceUuid; m_packetBuffer.assign(kPacketSizeBytes, 0U); beginPacket(); @@ -176,11 +153,6 @@ void CtfStreamWriter::writeRecord(std::uint32_t eventId, std::uint64_t timestamp ++m_eventCount; } -const std::string& CtfStreamWriter::uuidString() const noexcept -{ - return m_uuidString; -} - void CtfStreamWriter::beginPacket() { std::fill(m_packetBuffer.begin(), m_packetBuffer.end(), std::uint8_t{0}); @@ -198,7 +170,7 @@ void CtfStreamWriter::flushPacket() Record header(m_packetBuffer, 0U, kPacketOverhead); header.writeU32(CtfSchema::Magic); - for (const auto byte : m_uuid) { + for (const auto byte : m_traceUuid.bytes()) { header.writeU8(byte); } header.writeU32(m_streamId); diff --git a/tools/ctrace/src/output/ctf/CtfStreamWriter.h b/tools/ctrace/src/output/ctf/CtfStreamWriter.h index e48ea6fc5..b695049ce 100644 --- a/tools/ctrace/src/output/ctf/CtfStreamWriter.h +++ b/tools/ctrace/src/output/ctf/CtfStreamWriter.h @@ -8,14 +8,15 @@ #ifndef CTRACE_SRC_OUTPUT_CTF_CTFSTREAMWRITER_H #define CTRACE_SRC_OUTPUT_CTF_CTFSTREAMWRITER_H -#include +#include "CtfMetadataModel.h" +#include "CtfUuid.h" + #include #include #include #include #include #include -#include #include /** @brief Writes packetized binary CTF stream records. */ @@ -60,8 +61,8 @@ class CtfStreamWriter final { /** @brief Disables copy assignment because the writer owns an output stream. */ CtfStreamWriter& operator=(const CtfStreamWriter&) = delete; - /** @brief Opens a new CTF stream file with the supplied stream ID. */ - void open(const std::filesystem::path& filePath, std::uint32_t streamId); + /** @brief Opens a new CTF stream file with explicit stream and trace identity. */ + void open(const std::filesystem::path& filePath, CtfStreamClassId streamClassId, const CtfUuid& traceUuid); /** @brief Flushes the final packet and closes the stream. */ void close(); /** @brief Closes and removes an incomplete stream without throwing. */ @@ -71,9 +72,6 @@ class CtfStreamWriter final { void writeRecord(std::uint32_t eventId, std::uint64_t timestamp, std::uint8_t traceBusId, std::size_t payloadSize, const RecordCallback& writePayload); - /** @brief Returns the UUID shared by the stream and metadata. */ - const std::string& uuidString() const noexcept; - private: /** @brief Initializes a new packet buffer and writes its fixed context. */ void beginPacket(); @@ -92,8 +90,7 @@ class CtfStreamWriter final { std::optional m_lastTimestamp; std::uint32_t m_streamId = 0; std::uint32_t m_packetSequence = 0; - std::array m_uuid{}; - std::string m_uuidString; + CtfUuid m_traceUuid; bool m_open = false; }; diff --git a/tools/ctrace/src/output/ctf/CtfUuid.cpp b/tools/ctrace/src/output/ctf/CtfUuid.cpp new file mode 100644 index 000000000..b3d64de2e --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfUuid.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfUuid.h" + +#include +#include +#include +#include + +CtfUuid CtfUuid::randomV4() +{ + std::array bytes{}; + std::random_device random; + for (auto& byte : bytes) { + byte = static_cast(random()); + } + bytes[6U] = static_cast((bytes[6U] & 0x0fU) | 0x40U); + bytes[8U] = static_cast((bytes[8U] & 0x3fU) | 0x80U); + return CtfUuid{bytes}; +} + +std::string CtfUuid::toString() const +{ + std::ostringstream out; + out << std::hex << std::setfill('0'); + for (std::size_t index = 0; index < m_bytes.size(); ++index) { + if (index == 4U || index == 6U || index == 8U || index == 10U) { + out << '-'; + } + out << std::setw(2) << static_cast(m_bytes[index]); + } + return out.str(); +} diff --git a/tools/ctrace/src/output/ctf/CtfUuid.h b/tools/ctrace/src/output/ctf/CtfUuid.h new file mode 100644 index 000000000..164747b4c --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfUuid.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_CTF_CTFUUID_H +#define CTRACE_SRC_OUTPUT_CTF_CTFUUID_H + +#include +#include +#include + +/** @brief Stores one binary UUID used by a CTF trace or clock domain. */ +class CtfUuid final { +public: + /** @brief Creates an all-zero UUID value. */ + constexpr CtfUuid() = default; + + /** @brief Creates a UUID from its 16 encoded bytes. */ + explicit constexpr CtfUuid(std::array bytes) + : m_bytes(bytes) + { + } + + /** @brief Generates a random RFC 4122 version-4 UUID. */ + static CtfUuid randomV4(); + + /** @brief Returns the encoded UUID bytes. */ + constexpr const std::array& bytes() const noexcept + { + return m_bytes; + } + + /** @brief Returns the canonical lower-case textual representation. */ + std::string toString() const; + +private: + std::array m_bytes{}; +}; + +/** @brief Compares complete UUID values. */ +inline bool operator==(const CtfUuid& left, const CtfUuid& right) noexcept +{ + return left.bytes() == right.bytes(); +} + +/** @brief Compares complete UUID values. */ +inline bool operator!=(const CtfUuid& left, const CtfUuid& right) noexcept +{ + return !(left == right); +} + +/** @brief Orders UUID values lexicographically for associative containers. */ +inline bool operator<(const CtfUuid& left, const CtfUuid& right) noexcept +{ + return left.bytes() < right.bytes(); +} + +#endif // CTRACE_SRC_OUTPUT_CTF_CTFUUID_H diff --git a/tools/ctrace/test/unit/CMakeLists.txt b/tools/ctrace/test/unit/CMakeLists.txt index 05aff6161..b2b44da38 100644 --- a/tools/ctrace/test/unit/CMakeLists.txt +++ b/tools/ctrace/test/unit/CMakeLists.txt @@ -43,6 +43,7 @@ add_executable(CtraceUnitTests src/output/csv/CsvRowMapperTests.cpp src/output/ctf/CtfBundleOutputTests.cpp src/output/ctf/CtfEncoderTests.cpp + src/output/ctf/CtfMetadataModelTests.cpp src/output/ctf/CtfMetadataWriterTests.cpp src/output/ctf/CtfSchemaTests.cpp src/output/ctf/CtfStreamWriterTests.cpp diff --git a/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp b/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp index e94e0bf10..4f10fc0b3 100644 --- a/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp +++ b/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp @@ -12,8 +12,10 @@ #include "OutputRequirements.h" #include "TraceOutputConfig.h" #include "TraceRunConfig.h" +#include #include #include +#include #include #include #include @@ -40,6 +42,28 @@ static TraceOutputPlan planOutputs(const TraceOutputRequest& request, const std: return planOutputs(request, rawInputPath, CtraceRunMeta::fromConfig(config), diagnostics); } +/** @brief Finds one configured CTF stream class by its public numeric ID. */ +static const CtfStreamDescriptor* findCtfStream(const CtfOutputConfig& config, std::uint32_t streamClassId) +{ + const auto found = std::find_if( + config.metadata.streams.begin(), config.metadata.streams.end(), + [&](const CtfStreamDescriptor& stream) { return stream.streamClassId == CtfStreamClassId{streamClassId}; }); + return found == config.metadata.streams.end() ? nullptr : &*found; +} + +/** @brief Resolves the clock descriptor referenced by one configured CTF stream. */ +static const CtfClockDomainDescriptor* clockForStream(const CtfOutputConfig& config, std::uint32_t streamClassId) +{ + const auto* stream = findCtfStream(config, streamClassId); + if (stream == nullptr) { + return nullptr; + } + const auto found = + std::find_if(config.metadata.clockDomains.begin(), config.metadata.clockDomains.end(), + [&](const CtfClockDomainDescriptor& clock) { return clock.id == stream->clockDomainId; }); + return found == config.metadata.clockDomains.end() ? nullptr : &*found; +} + /** @brief Creates metadata satisfying the default backend requirements. */ static TraceRunConfig backendRequirementsConfig() { @@ -78,6 +102,25 @@ TEST(CtraceUnitTests, testBackendRequirementsUsePerStreamMetadata) std::map({{1U, 1U}, {2U, 1U}}))) << "the default prescaler must be retained per ATB stream"; + CollectingDiagnosticSink equalClockDiagnostics; + const auto equalClockPlan = planOutputs(outputRequest(false, true), "captures/Multicore.SWO.raw", + missingPrescalerMeta, equalClockDiagnostics); + ASSERT_TRUE(equalClockPlan.ctf.has_value()); + ASSERT_EQ(equalClockPlan.ctf->metadata.streams.size(), 2U); + ASSERT_EQ(equalClockPlan.ctf->metadata.clockDomains.size(), 2U) + << "equal frequency must not merge independent processor clock domains"; + const auto* equalCore0Clock = clockForStream(*equalClockPlan.ctf, 1U); + const auto* equalCore1Clock = clockForStream(*equalClockPlan.ctf, 2U); + ASSERT_NE(equalCore0Clock, nullptr); + ASSERT_NE(equalCore1Clock, nullptr); + ASSERT_TRUE(equalCore0Clock->uuid.has_value()); + ASSERT_TRUE(equalCore1Clock->uuid.has_value()); + EXPECT_NE(equalCore0Clock->id, equalCore1Clock->id); + EXPECT_NE(equalCore0Clock->uuid, equalCore1Clock->uuid); + EXPECT_EQ(equalCore0Clock->frequencyHz, 400000000U); + EXPECT_EQ(equalCore1Clock->frequencyHz, 400000000U); + EXPECT_TRUE(equalClockDiagnostics.events().empty()); + core0.timestamps = TraceRunTimestampSetup{400000000U, 4U}; multicore.setups = {core0, core1}; const auto mixedMissingPrescalerMeta = CtraceRunMeta::fromConfig(multicore); @@ -98,16 +141,32 @@ TEST(CtraceUnitTests, testBackendRequirementsUsePerStreamMetadata) CollectingDiagnosticSink allClockDiagnostics; const auto allClockPlan = planOutputs(ctfRequest, "captures/Multicore.SWO.raw", distinctClockMeta, allClockDiagnostics); - ASSERT_TRUE(!allClockPlan.ctf.has_value()) << "CTF must reject selected Trace Bus IDs with different clocks"; - allClockDiagnostics.singleEvent(); + ASSERT_TRUE(allClockPlan.ctf.has_value()) << "independent CTF clock domains may use different frequencies"; + ASSERT_EQ(allClockPlan.ctf->metadata.streams.size(), 2U); + ASSERT_EQ(allClockPlan.ctf->metadata.clockDomains.size(), 2U); + const auto* core0Clock = clockForStream(*allClockPlan.ctf, 1U); + const auto* core1Clock = clockForStream(*allClockPlan.ctf, 2U); + ASSERT_NE(core0Clock, nullptr); + ASSERT_NE(core1Clock, nullptr); + EXPECT_EQ(core0Clock->frequencyHz, 400000000U); + EXPECT_EQ(core1Clock->frequencyHz, 200000000U); + EXPECT_NE(core0Clock->id, core1Clock->id); + EXPECT_TRUE(allClockDiagnostics.events().empty()); ctfRequest.selection.streams = {2U}; CollectingDiagnosticSink selectedClockDiagnostics; const auto selectedClockPlan = planOutputs(ctfRequest, "captures/Multicore.SWO.raw", distinctClockMeta, selectedClockDiagnostics); - ASSERT_TRUE(selectedClockPlan.ctf.has_value() && selectedClockPlan.ctf->coreClockHz == 200000000U && - selectedClockDiagnostics.events().empty()) - << "a selected Trace Bus ID must use its processor's clock"; + ASSERT_TRUE(selectedClockPlan.ctf.has_value()); + ASSERT_EQ(selectedClockPlan.ctf->metadata.streams.size(), 1U); + ASSERT_EQ(selectedClockPlan.ctf->metadata.clockDomains.size(), 1U); + const auto* selectedClock = clockForStream(*selectedClockPlan.ctf, 2U); + ASSERT_NE(selectedClock, nullptr); + ASSERT_TRUE(selectedClock->uuid.has_value()); + EXPECT_EQ(selectedClock->frequencyHz, 200000000U); + EXPECT_EQ(selectedClockPlan.ctf->routes.size(), 2U) + << "the normalized route catalogue remains authoritative beyond the output filter"; + ASSERT_TRUE(selectedClockDiagnostics.events().empty()) << "a selected Trace Bus ID must use its processor's clock"; } TEST(CtraceUnitTests, testDwtDataMetadataDefaultsAndValidation) @@ -139,10 +198,10 @@ TEST(CtraceUnitTests, testDwtDataMetadataDefaultsAndValidation) << "valid or missing DWT metadata must not disable CSV or CTF"; ASSERT_TRUE(configurationDiagnostics.events().empty()) << "valid or missing DWT metadata must not produce diagnostics"; - ASSERT_TRUE(outputPlan.ctf->sources.size() == 3U && outputPlan.ctf->sources[0].dataType == "unsigned" && - outputPlan.ctf->sources[0].dataSize == 4U && outputPlan.ctf->sources[1].dataType == "unsigned" && - outputPlan.ctf->sources[1].dataSize == 1U && outputPlan.ctf->sources[2].dataType == "signed" && - outputPlan.ctf->sources[2].dataSize == 2U) + const auto& sources = outputPlan.ctf->metadata.sources; + ASSERT_TRUE(sources.size() == 3U && sources[0].dataType == "unsigned" && sources[0].dataSize == 4U && + sources[1].dataType == "unsigned" && sources[1].dataSize == 1U && sources[2].dataType == "signed" && + sources[2].dataSize == 2U) << "CTF data-type/size defaults or explicit values mismatch"; config.setups[0].data[1].size = 0U; @@ -153,6 +212,82 @@ TEST(CtraceUnitTests, testDwtDataMetadataDefaultsAndValidation) invalidSizeDiagnostics.singleEvent(); } +TEST(CtraceUnitTests, testOutputRequirementsValidateDwtAddressRangeForCtfOnly) +{ + auto config = backendRequirementsConfig(); + config.references[0].dataType = "unsigned"; + const auto maximumAddress = std::numeric_limits::max(); + config.references[0].address = maximumAddress - 3U; + + const auto allRequest = outputRequest(true, true); + CollectingDiagnosticSink maximumRangeDiagnostics; + const auto maximumRange = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, maximumRangeDiagnostics); + ASSERT_TRUE(maximumRange.csv.has_value() && maximumRange.ctf.has_value()) + << "a DWT size-4 range ending exactly at UINT64_MAX must remain representable"; + ASSERT_EQ(maximumRange.ctf->metadata.sources.size(), 1U); + EXPECT_EQ(maximumRange.ctf->metadata.sources.front().address, std::optional(maximumAddress - 3U)); + EXPECT_TRUE(maximumRangeDiagnostics.events().empty()); + + config.references[0].address = maximumAddress - 2U; + CollectingDiagnosticSink overflowDiagnostics; + const auto overflow = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, overflowDiagnostics); + ASSERT_TRUE(overflow.csv.has_value() && !overflow.ctf.has_value()) + << "an overflowing DWT address range must disable only CTF for --all"; + EXPECT_EQ(overflowDiagnostics.singleEvent().message, "CTF output cannot represent the configured DWT address range"); + EXPECT_TRUE(overflowDiagnostics.containsContext("backend", "ctf")); + EXPECT_TRUE(overflowDiagnostics.containsContext("address", std::to_string(maximumAddress - 2U))); + EXPECT_TRUE(overflowDiagnostics.containsContext("dataSize", "4")); + EXPECT_GT(overflowDiagnostics.failureCount(), 0U); +} + +TEST(CtraceUnitTests, testOutputRequirementsRejectDwtComparatorOutsideCtfDomainOnly) +{ + auto config = backendRequirementsConfig(); + config.references[0].sources = {4U}; + config.references[0].dataType = "unsigned"; + + CollectingDiagnosticSink diagnostics; + const auto plan = planOutputs(outputRequest(true, true), "BackendRequirements.SWO.raw", config, diagnostics); + ASSERT_TRUE(plan.csv.has_value() && !plan.ctf.has_value()) + << "a DWT comparator outside the CTF schema must disable only CTF for --all"; + EXPECT_EQ(diagnostics.singleEvent().message, "CTF output requires DWT comparator sources between 0 and 3"); + EXPECT_TRUE(diagnostics.containsContext("backend", "ctf")); + EXPECT_TRUE(diagnostics.containsContext("channel", "DWT4")); + EXPECT_GT(diagnostics.failureCount(), 0U); +} + +TEST(CtraceUnitTests, testOutputRequirementsReportRepeatedRouteSourceConflictOnce) +{ + auto config = backendRequirementsConfig(); + auto first = config.references.front(); + first.dataType = "unsigned"; + auto second = first; + second.dataSize = 4U; + second.dataSizeError = "second invalid size"; + auto third = first; + third.dataSize = 4U; + third.dataSizeError = "third invalid size"; + config.references = {first, second, third}; + + const auto meta = CtraceRunMeta::fromConfig(config); + ASSERT_EQ(meta.sources().size(), 3U) + << "normalization must retain conflicting route-local source metadata for output-specific validation"; + CollectingDiagnosticSink diagnostics; + const auto plan = planOutputs(outputRequest(true, true), "BackendRequirements.SWO.raw", meta, diagnostics); + ASSERT_TRUE(plan.csv.has_value() && !plan.ctf.has_value()) + << "conflicting CTF-only source metadata must not disable CSV for --all"; + const auto conflictCount = + std::count_if(diagnostics.events().begin(), diagnostics.events().end(), [](const auto& event) { + return event.message == + "CTF metadata cannot describe conflicting active metadata for one route/type/source key"; + }); + EXPECT_EQ(conflictCount, 1U) << "repeated conflicts for one route/type/source key must be diagnosed exactly once"; + EXPECT_EQ(diagnostics.events().size(), 3U) << "each malformed source must retain its own targeted size diagnostic"; + EXPECT_TRUE(diagnostics.containsContext("backend", "ctf")); + EXPECT_TRUE(diagnostics.containsContext("channel", "DWT0")); + EXPECT_GT(diagnostics.failureCount(), 0U); +} + TEST(CtraceUnitTests, testOutputRequirementsAreBackendSpecific) { auto config = backendRequirementsConfig(); @@ -183,8 +318,10 @@ TEST(CtraceUnitTests, testOutputRequirementsAreBackendSpecific) (missingType.csv->outputPath == std::filesystem::path("BackendRequirements.SWO.csv") && missingType.ctf->outputDirectory == std::filesystem::path("BackendRequirements.ctf") && missingType.ctf->traceCompassXmlPath == std::filesystem::path("BackendRequirements.SWO.traceanalysis.xml") && - missingType.ctf->coreClockHz == 400000000U && missingType.ctf->sources.size() == 1U && - missingType.ctf->sources[0].dataType == "unsigned" && missingType.ctf->sources[0].dataSize == 4U)) + missingType.ctf->metadata.clockDomains.size() == 1U && + missingType.ctf->metadata.clockDomains[0].frequencyHz == 400000000U && + missingType.ctf->metadata.sources.size() == 1U && missingType.ctf->metadata.sources[0].dataType == "unsigned" && + missingType.ctf->metadata.sources[0].dataSize == 4U)) << "output preflight must resolve artifact paths, clock, routes, and defaults"; ASSERT_TRUE(missingTypeDiagnostics.events().empty()) << "missing optional data-type must not produce diagnostics"; @@ -208,8 +345,8 @@ TEST(CtraceUnitTests, testOutputRequirementsAreBackendSpecific) CollectingDiagnosticSink currentMetadataDiagnostics; const auto currentMetadata = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, currentMetadataDiagnostics); - ASSERT_TRUE(currentMetadata.ctf.has_value() && currentMetadata.ctf->sources[0].dataType == "signed" && - currentMetadata.ctf->sources[0].dataSize == 1U) + ASSERT_TRUE(currentMetadata.ctf.has_value() && currentMetadata.ctf->metadata.sources[0].dataType == "signed" && + currentMetadata.ctf->metadata.sources[0].dataSize == 1U) << "reference data-type/size must be retained for CTF"; ASSERT_TRUE(currentMetadataDiagnostics.events().empty()); @@ -226,24 +363,67 @@ TEST(CtraceUnitTests, testCtfOutputRequiresAValidClock) { auto config = backendRequirementsConfig(); config.setups[0].data[0] = TraceRunDataSetup{}; + config.references[0].dataType.reset(); const auto allRequest = outputRequest(true, true); config.setups[0].timestamps->clockHz.reset(); CollectingDiagnosticSink missingClockDiagnostics; const auto missingClock = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, missingClockDiagnostics); ASSERT_TRUE(missingClock.csv.has_value() && !missingClock.ctf.has_value()) << "missing timestamps.clock must disable only CTF"; + EXPECT_EQ(missingClockDiagnostics.singleEvent().message, + "CTF output requires timestamps.clock; no default is assumed"); + EXPECT_TRUE(missingClockDiagnostics.containsContext("backend", "ctf")); + EXPECT_GT(missingClockDiagnostics.failureCount(), 0U) + << "--all must remain unsuccessful when its requested CTF backend is invalid"; config.setups[0].timestamps->clockError = "timestamps.clock must be unsigned"; CollectingDiagnosticSink malformedClockDiagnostics; const auto malformedClock = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, malformedClockDiagnostics); ASSERT_TRUE(malformedClock.csv.has_value() && !malformedClock.ctf.has_value()) << "malformed timestamps.clock must disable only CTF"; + EXPECT_EQ(malformedClockDiagnostics.singleEvent().message, "CTF output cannot use the configured timestamps.clock"); + EXPECT_GT(malformedClockDiagnostics.failureCount(), 0U); config.setups[0].timestamps->clockError.reset(); config.setups[0].timestamps->clockHz = 0U; CollectingDiagnosticSink zeroClockDiagnostics; const auto zeroClock = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, zeroClockDiagnostics); ASSERT_TRUE(zeroClock.csv.has_value() && !zeroClock.ctf.has_value()) << "zero timestamps.clock must disable only CTF"; + EXPECT_EQ(zeroClockDiagnostics.singleEvent().message, "CTF output requires timestamps.clock to be greater than zero"); + EXPECT_GT(zeroClockDiagnostics.failureCount(), 0U); + + CollectingDiagnosticSink csvOnlyDiagnostics; + const auto csvOnly = + planOutputs(outputRequest(true, false), "BackendRequirements.SWO.raw", config, csvOnlyDiagnostics); + EXPECT_TRUE(csvOnly.csv.has_value()); + EXPECT_FALSE(csvOnly.ctf.has_value()); + EXPECT_TRUE(csvOnlyDiagnostics.events().empty()) + << "CSV-only planning must not inspect or report CTF clock requirements"; +} + +TEST(CtraceUnitTests, testCtfOutputRejectsConflictingClockFragmentsForSharedProcessorRoute) +{ + TraceRunConfig config; + config.path = "SharedProcessorClock.ctrace-run.yml"; + config.traceFormat = TraceRunFormat::Formatted; + config.setups = { + TraceRunTestSupport::makeTimestampSetup("core", 100000000U, 1U), + TraceRunTestSupport::makeTimestampSetup("core", 200000000U, 1U), + }; + config.references = { + TraceRunTestSupport::makeReference("itm", "core", 1U, {1U}, "core/itm"), + }; + + CollectingDiagnosticSink diagnostics; + const auto plan = planOutputs(outputRequest(true, true), "SharedProcessorClock.TB.raw", config, diagnostics); + EXPECT_TRUE(plan.csv.has_value()); + EXPECT_FALSE(plan.ctf.has_value()); + const auto& error = diagnostics.singleEvent(); + EXPECT_EQ(error.message, "CTF output cannot use the configured timestamps.clock"); + EXPECT_TRUE(diagnostics.containsContext("backend", "ctf")); + EXPECT_TRUE(diagnostics.containsContext("stream", "1")); + EXPECT_TRUE(diagnostics.containsContext("pname", "core")); + EXPECT_TRUE(diagnostics.containsContext("error", "conflicting active ctrace-setup timestamps.clock values")); } TEST(CtraceUnitTests, testOutputRequirementsHonorFiltersAndCheckOnlyMode) @@ -282,6 +462,9 @@ TEST(CtraceUnitTests, testOutputPreflightRejectsAmbiguousRoutesForCtfOnly) first.label = "core-one"; TraceRunReference second = first; second.label = "core-two"; + second.address = 0x2000U; + second.dataType = "signed"; + second.dataSize = 2U; const auto firstAnchor = TraceRunTestSupport::makeReference("itm", "core0", 1U, {}, "core0/itm"); const auto secondAnchor = TraceRunTestSupport::makeReference("itm", "core1", 2U, {}, "core1/itm"); config.references = {first, second, firstAnchor, secondAnchor}; @@ -296,13 +479,22 @@ TEST(CtraceUnitTests, testOutputPreflightRejectsAmbiguousRoutesForCtfOnly) config.references[1].stream = 2U; config.references[1].processorName = "core1"; config.references[1].ctraceRef = "core1/data#0"; - config.references[1].label = "core-one"; CollectingDiagnosticSink routeDiagnostics; const auto routePlan = planOutputs(allRequest, "captures/AmbiguousRoutes.SWO.raw", config, routeDiagnostics); - ASSERT_TRUE(routePlan.csv.has_value() && routePlan.ctf.has_value() && routePlan.ctf->sources.size() == 2U) - << "CTF must retain equivalent routes with distinct Trace Bus IDs"; + ASSERT_TRUE(routePlan.csv.has_value() && routePlan.ctf.has_value() && routePlan.ctf->metadata.sources.size() == 2U) + << "CTF must retain the same source number independently on distinct routes"; + const auto& routeSources = routePlan.ctf->metadata.sources; + EXPECT_EQ(routeSources[0].route.traceBusId, 1U); + EXPECT_EQ(routeSources[0].label, std::optional("core-one")); + EXPECT_EQ(routeSources[0].dataType, "unsigned"); + EXPECT_EQ(routeSources[0].dataSize, 4U); + EXPECT_EQ(routeSources[1].route.traceBusId, 2U); + EXPECT_EQ(routeSources[1].label, std::optional("core-two")); + EXPECT_EQ(routeSources[1].address, std::optional(0x2000U)); + EXPECT_EQ(routeSources[1].dataType, "signed"); + EXPECT_EQ(routeSources[1].dataSize, 2U); ASSERT_TRUE(routeDiagnostics.events().empty()) - << "equivalent CTF metadata on distinct Trace Bus IDs must not be ambiguous"; + << "route-local CTF metadata with the same source number must not be ambiguous"; TraceRunConfig processorConfig; processorConfig.path = "AmbiguousProcessors.ctrace-run.yml"; @@ -325,8 +517,9 @@ TEST(CtraceUnitTests, testOutputPreflightRejectsAmbiguousRoutesForCtfOnly) allRequest.selection.streams = {1U}; CollectingDiagnosticSink selectedDiagnostics; const auto selectedPlan = planOutputs(allRequest, "captures/AmbiguousRoutes.SWO.raw", config, selectedDiagnostics); - ASSERT_TRUE(selectedPlan.csv.has_value() && selectedPlan.ctf.has_value() && selectedPlan.ctf->sources.size() == 1U && - selectedPlan.ctf->sources[0].label == std::optional("core-one")) + ASSERT_TRUE(selectedPlan.csv.has_value() && selectedPlan.ctf.has_value() && + selectedPlan.ctf->metadata.sources.size() == 1U && + selectedPlan.ctf->metadata.sources[0].label == std::optional("core-one")) << "an explicit stream selection must produce one resolved CTF route"; ASSERT_TRUE(selectedDiagnostics.events().empty()) << "an unambiguous selected route must not produce preflight diagnostics"; @@ -421,7 +614,7 @@ TEST(CtraceUnitTests, testOutputRequirementsDeferUnformattedSingleClockAmbiguity EXPECT_EQ(missingCandidateDiagnostics.singleEvent().message, "CTF output cannot use the configured timestamps.clock"); } -TEST(CtraceUnitTests, testOutputRequirementsRejectUnknownStreamWithMultipleClocks) +TEST(CtraceUnitTests, testOutputRequirementsTreatUnknownStreamsAsPureFilters) { TraceRunConfig config; config.path = "Multicore.ctrace-run.yml"; @@ -439,41 +632,72 @@ TEST(CtraceUnitTests, testOutputRequirementsRejectUnknownStreamWithMultipleClock ctfRequest.selection.streams = {99U}; CollectingDiagnosticSink diagnostics; const auto plan = planOutputs(ctfRequest, "Multicore.SWO.raw", config, diagnostics); - ASSERT_FALSE(plan.ctf.has_value()); - diagnostics.singleEvent(); - - config.setups[1].timestamps->clockHz = 100U; - CollectingDiagnosticSink commonDiagnostics; - const auto commonPlan = planOutputs(ctfRequest, "Multicore.SWO.raw", config, commonDiagnostics); - ASSERT_TRUE(commonPlan.ctf.has_value()); - EXPECT_EQ(commonPlan.ctf->coreClockHz, 100U); - EXPECT_EQ(commonPlan.ctf->routes.size(), 2U); - EXPECT_TRUE(commonDiagnostics.events().empty()); + ASSERT_TRUE(plan.ctf.has_value()); + EXPECT_TRUE(plan.ctf->metadata.streams.empty()); + EXPECT_TRUE(plan.ctf->metadata.clockDomains.empty()); + EXPECT_TRUE(plan.ctf->metadata.sources.empty()); + EXPECT_EQ(plan.ctf->routes.size(), 2U); + EXPECT_TRUE(diagnostics.events().empty()) << "an unknown-only stream filter must not inspect unrelated route clocks"; ctfRequest.selection.streams = {1U, 99U}; CollectingDiagnosticSink mixedDiagnostics; const auto mixedPlan = planOutputs(ctfRequest, "Multicore.SWO.raw", config, mixedDiagnostics); ASSERT_TRUE(mixedPlan.ctf.has_value()); - EXPECT_EQ(mixedPlan.ctf->coreClockHz, 100U); + ASSERT_EQ(mixedPlan.ctf->metadata.streams.size(), 1U); + ASSERT_EQ(mixedPlan.ctf->metadata.clockDomains.size(), 1U); + const auto* firstClock = clockForStream(*mixedPlan.ctf, 1U); + ASSERT_NE(firstClock, nullptr); + EXPECT_EQ(firstClock->frequencyHz, 100U); EXPECT_EQ(mixedPlan.ctf->routes.size(), 2U); EXPECT_TRUE(mixedDiagnostics.events().empty()); + config.setups[1].timestamps->clockHz.reset(); + config.setups[1].timestamps->clockError = "invalid second processor clock"; + CollectingDiagnosticSink unselectedErrorDiagnostics; + const auto unselectedErrorPlan = planOutputs(ctfRequest, "Multicore.SWO.raw", config, unselectedErrorDiagnostics); + ASSERT_TRUE(unselectedErrorPlan.ctf.has_value()); + EXPECT_EQ(unselectedErrorPlan.ctf->metadata.streams.size(), 1U); + EXPECT_TRUE(unselectedErrorDiagnostics.events().empty()) + << "an invalid clock on an unselected route must not disable CTF"; + + CollectingDiagnosticSink allDiagnostics; + const auto allPlan = planOutputs(outputRequest(true, true), "Multicore.SWO.raw", config, allDiagnostics); + EXPECT_TRUE(allPlan.csv.has_value()); + EXPECT_FALSE(allPlan.ctf.has_value()); + EXPECT_EQ(allDiagnostics.singleEvent().message, "CTF output cannot use the configured timestamps.clock"); + EXPECT_TRUE(allDiagnostics.containsContext("stream", "2")); + ctfRequest.selection.streams = {99U}; - config.setups[0].timestamps->clockHz.reset(); - config.setups[0].timestamps->clockError = "invalid processor clock"; - CollectingDiagnosticSink malformedDiagnostics; - const auto malformedPlan = planOutputs(ctfRequest, "Multicore.SWO.raw", config, malformedDiagnostics); - ASSERT_FALSE(malformedPlan.ctf.has_value()); - EXPECT_EQ(malformedDiagnostics.singleEvent().message, "CTF output cannot use the configured timestamps.clock"); + config.setups[0].timestamps->clockHz = 0U; + CollectingDiagnosticSink unknownInvalidDiagnostics; + const auto unknownInvalidPlan = planOutputs(ctfRequest, "Multicore.SWO.raw", config, unknownInvalidDiagnostics); + ASSERT_TRUE(unknownInvalidPlan.ctf.has_value()); + EXPECT_TRUE(unknownInvalidPlan.ctf->metadata.streams.empty()); + EXPECT_TRUE(unknownInvalidPlan.ctf->metadata.clockDomains.empty()); + EXPECT_TRUE(unknownInvalidDiagnostics.events().empty()) + << "unknown-only selection must not report malformed or zero clocks on filtered routes"; +} - for (auto& setup : config.setups) { - setup.timestamps->clockError.reset(); - setup.timestamps->clockHz = 0U; - } - CollectingDiagnosticSink zeroDiagnostics; - const auto zeroPlan = planOutputs(ctfRequest, "Multicore.SWO.raw", config, zeroDiagnostics); - ASSERT_FALSE(zeroPlan.ctf.has_value()); - EXPECT_EQ(zeroDiagnostics.singleEvent().message, "CTF output requires timestamps.clock to be greater than zero"); +TEST(CtraceUnitTests, testOutputRequirementsPreserveLegacyTopologyForUnknownStreamFilter) +{ + TraceRunConfig config; + config.path = "Legacy.ctrace-run.yml"; + config.setups.push_back(TraceRunTestSupport::makeTimestampSetup("core", 1000000U, 1U)); + + auto request = outputRequest(false, true); + request.selection.streams = {99U}; + CollectingDiagnosticSink diagnostics; + const auto plan = planOutputs(request, "Legacy.SWO.raw", config, diagnostics); + + ASSERT_TRUE(plan.ctf.has_value()); + ASSERT_EQ(plan.ctf->metadata.streams.size(), 1U); + ASSERT_EQ(plan.ctf->metadata.clockDomains.size(), 1U); + EXPECT_EQ(plan.ctf->metadata.streams.front().streamClassId, CtfStreamClassId{0U}); + EXPECT_FALSE(plan.ctf->metadata.streams.front().route.traceBusId.has_value()); + EXPECT_EQ(plan.ctf->metadata.clockDomains.front().name, "swo_clock"); + EXPECT_EQ(plan.ctf->metadata.clockDomains.front().frequencyHz, 1000000U); + EXPECT_FALSE(plan.ctf->metadata.clockDomains.front().uuid.has_value()); + EXPECT_TRUE(diagnostics.events().empty()); } TEST(CtraceUnitTests, testOutputRequirementsRejectsInputWithoutArtifactName) diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp index a5fa2c30f..f0d5d4fee 100644 --- a/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp +++ b/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp @@ -14,6 +14,7 @@ #include #include "ctf/CtfBundleOutput.h" +#include "ctf/CtfMetadataWriter.h" #include "ctf/CtfSchema.h" #include "CtraceRunMeta.h" #include "OutputRequirements.h" @@ -54,7 +55,15 @@ static std::filesystem::path testTraceCompassXmlPath(const std::filesystem::path /** @brief Creates a CTF bundle configuration with test defaults. */ static CtfOutputConfig makeCtfBundleConfig(const std::filesystem::path& outputDirectory, std::uint64_t coreClockHz) { - return CtfOutputConfig(outputDirectory, testTraceCompassXmlPath(outputDirectory), coreClockHz, {}, {}); + return CtfOutputConfig(outputDirectory, testTraceCompassXmlPath(outputDirectory), {}, + CtfTestSupport::legacyTopology(coreClockHz)); +} + +/** @brief Creates a legacy CTF bundle configuration with explicit target paths. */ +static CtfOutputConfig makeCtfBundleConfig(const std::filesystem::path& outputDirectory, + const std::filesystem::path& traceCompassXmlPath, std::uint64_t coreClockHz) +{ + return CtfOutputConfig(outputDirectory, traceCompassXmlPath, {}, CtfTestSupport::legacyTopology(coreClockHz)); } /** @brief Requires all files of a completed CTF bundle. */ @@ -88,7 +97,7 @@ class TemporaryCtfOutput { }; /** @brief Converts normalized trace-run source metadata for output tests. */ -static ResolvedTraceSource resolvedSource(const CtraceRunSourceMeta& source) +static CtfSourceDescriptor resolvedSource(const CtraceRunSourceMeta& source) { return { source.type, @@ -211,39 +220,32 @@ TEST(CtraceUnitTests, testCtfBundleOutputUsesCtraceRunMeta) CtraceRunMeta::fromConfig(traceRun), preflightDiagnostics); ASSERT_TRUE(outputPlan.ctf.has_value() && preflightDiagnostics.events().empty()) << "resolved CTF source missing"; auto options = std::move(*outputPlan.ctf); - ASSERT_TRUE(!options.sources.empty()) << "resolved CTF source missing"; - ASSERT_TRUE(options.sources.front().route.traceBusId == 7U) << "resolved CTF source must retain its Trace Bus ID"; - ASSERT_TRUE(options.sources.front().dataType == "signed") << "resolved CTF source must retain its data type"; + ASSERT_TRUE(!options.metadata.sources.empty()) << "resolved CTF source missing"; + ASSERT_TRUE(options.metadata.sources.front().route.traceBusId == 7U) + << "resolved CTF source must retain its Trace Bus ID"; + ASSERT_TRUE(options.metadata.sources.front().dataType == "signed") << "resolved CTF source must retain its data type"; ASSERT_EQ(options.routes.size(), 1U); - const auto route = options.routes.front(); - CtfBundleOutput output(std::move(options)); - output.start(); - output.writeEvent(atCycle(onRoute(TraceEvent{DwtDataTraceEvent{0U, 1U, 0xffU, AccessType::Write}}, route), 100U)); - output.stop(); + ASSERT_EQ(options.metadata.streams.size(), 1U); + EXPECT_EQ(options.metadata.streams.front().streamClassId, CtfStreamClassId{7U}); + std::filesystem::create_directories(outputDir); + CtfMetadataModel model(CtfTestSupport::testUuid(), std::move(options.metadata)); + CtfMetadataWriter::write(outputDir, model); const auto metadata = readTestTextFile(outputDir / "metadata"); - const auto stream = readTestBinaryFile(outputDir / "stream_0"); ASSERT_TRUE(metadata.find("freq = 280000000") != std::string::npos) << "CTF trace-run clock mismatch"; - ASSERT_TRUE(metadata.find("cmsis_dwt0_value_type = \"signed\"") != std::string::npos) + ASSERT_TRUE(metadata.find("cmsis_stream_7_dwt0_value_type = \"signed\"") != std::string::npos) << "CTF signed-byte source type mismatch"; - ASSERT_TRUE(metadata.find("cmsis_dwt2_value_type = \"signed\"") != std::string::npos) + ASSERT_TRUE(metadata.find("cmsis_stream_7_dwt2_value_type = \"signed\"") != std::string::npos) << "CTF trace-run type mismatch"; - ASSERT_TRUE(metadata.find("cmsis_dwt2_address_start = \"0x24000E88\"") != std::string::npos) + ASSERT_TRUE(metadata.find("cmsis_stream_7_dwt2_address_start = \"0x24000E88\"") != std::string::npos) << "CTF trace-run start address mismatch"; - ASSERT_TRUE(metadata.find("cmsis_dwt2_address_end = \"0x24000E8B\"") != std::string::npos) + ASSERT_TRUE(metadata.find("cmsis_stream_7_dwt2_address_end = \"0x24000E8B\"") != std::string::npos) << "CTF trace-run end address mismatch"; ASSERT_TRUE(metadata.find("\"Current\\n\\t\\\"\\\\\\x01\" = 2") != std::string::npos) << "CTF trace-run label escaping mismatch"; - - const auto records = parseCtfRecords(stream); - ASSERT_TRUE(records.front().id == CtfSchema::value(CtfSchema::EventId::TraceStatus)) - << "CTF trace-start event missing"; - const auto& dwtRecord = - requireFirstCtfRecord(records, CtfSchema::EventId::DwtValue, "expected CTF DWT value event missing"); - ASSERT_TRUE(dwtRecord.traceBusId == 7U) << "CTF event context must preserve the CoreSight Trace Bus ID"; - ASSERT_TRUE(dwtRecord.payload[2U] == 0U) << "CTF one-byte int payload must select the i8 variant"; - ASSERT_TRUE(dwtRecord.payload[3U] == 0xffU) << "CTF signed-byte payload mismatch"; + ASSERT_TRUE(metadata.find("stream_id = 7") != std::string::npos) + << "generalized metadata must bind events to the configured stream class"; } TEST(CtraceUnitTests, testCtfOutputPlanningKeepsUnknownFilterWithoutLegacyBootstrap) @@ -262,16 +264,40 @@ TEST(CtraceUnitTests, testCtfOutputPlanningKeepsUnknownFilterWithoutLegacyBootst CollectingDiagnosticSink unknownDiagnostics; auto plan = planTraceOutputs({false, true, unknownSelection}, outputDir.parent_path() / "output.SWO.raw", meta, unknownDiagnostics); - ASSERT_TRUE(plan.ctf.has_value()); - ASSERT_EQ(plan.ctf->routes.size(), 1U); - EXPECT_EQ(plan.ctf->routes.front().traceBusId, 2U); + ASSERT_TRUE(plan.ctf.has_value()) << "an unmatched stream filter must retain the requested CTF plan"; + EXPECT_TRUE(plan.ctf->metadata.streams.empty()) + << "an unmatched formatted stream filter must not invent a CTF topology"; + EXPECT_TRUE(unknownDiagnostics.events().empty()); + EXPECT_FALSE(std::filesystem::exists(outputDir)); +} + +TEST(CtraceUnitTests, testCtfBundleOutputPreservesLegacyUnknownStreamFilter) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-legacy-unknown-stream-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + TraceRunConfig traceRun; + traceRun.path = "Legacy.ctrace-run.yml"; + traceRun.setups.push_back(TraceRunTestSupport::makeTimestampSetup(std::nullopt, 1000000U)); + traceRun.references.push_back(TraceRunTestSupport::makeReference("itm", std::nullopt, std::nullopt, {1U}, "itm")); + TraceSelection selection; + selection.streams = {99U}; + CollectingDiagnosticSink diagnostics; + auto plan = planTraceOutputs({false, true, selection}, outputDir.parent_path() / "output.SWO.raw", + CtraceRunMeta::fromConfig(traceRun), diagnostics); + + ASSERT_TRUE(plan.ctf.has_value() && diagnostics.events().empty()); + ASSERT_TRUE(CtfMetadataModel(CtfTestSupport::testUuid(), plan.ctf->metadata).isLegacySingleStreamLayout()); CtfBundleOutput output(std::move(*plan.ctf)); output.start(); output.stop(); - const auto records = readCtfRecords(outputDir / "stream_0"); - EXPECT_TRUE(records.empty()) << "an unmatched stream filter must not invent a synthetic no-bus bootstrap"; - EXPECT_TRUE(unknownDiagnostics.events().empty()); + ASSERT_TRUE(std::filesystem::is_regular_file(outputDir / "stream_0")); + EXPECT_EQ(std::filesystem::file_size(outputDir / "stream_0"), 0U); + EXPECT_TRUE(readCtfRecords(outputDir / "stream_0").empty()); + const auto metadata = readTestTextFile(outputDir / "metadata"); + EXPECT_NE(metadata.find("name = swo_clock;"), std::string::npos); + EXPECT_NE(metadata.find("stream {\n id = 0;"), std::string::npos); + EXPECT_TRUE(std::filesystem::is_regular_file(testTraceCompassXmlPath(outputDir))); } TEST(CtraceUnitTests, testCtfBundleOutputDefaultsDwtValueType) @@ -291,7 +317,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputDefaultsDwtValueType) const auto defaultOutputDir = root / "default"; auto defaultOptions = makeCtfBundleConfig(defaultOutputDir, 1000000U); - defaultOptions.sources = {resolvedSource(defaultMeta.sources().front())}; + defaultOptions.metadata.sources = {resolvedSource(defaultMeta.sources().front())}; CollectingDiagnosticSink diagnostics; CtfBundleOutput defaultOutput(std::move(defaultOptions), &diagnostics); defaultOutput.start(); @@ -313,7 +339,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputDefaultsDwtValueType) ASSERT_TRUE(signedMeta.sources().size() == 1U) << "signed DWT source missing"; const auto signedOutputDir = root / "signed"; auto signedOptions = makeCtfBundleConfig(signedOutputDir, 1000000U); - signedOptions.sources = {resolvedSource(signedMeta.sources().front())}; + signedOptions.metadata.sources = {resolvedSource(signedMeta.sources().front())}; CollectingDiagnosticSink signedDiagnostics; CtfBundleOutput signedOutput(std::move(signedOptions), &signedDiagnostics); signedOutput.start(); @@ -494,7 +520,8 @@ TEST(CtraceUnitTests, testCtfBundleOutputExcludesSoftwareChannelZero) CollectingDiagnosticSink preflightDiagnostics; auto outputPlan = planTraceOutputs({false, true, selection}, outputDir.parent_path() / "output.SWO.raw", CtraceRunMeta::fromConfig(traceRun), preflightDiagnostics); - ASSERT_TRUE(outputPlan.ctf.has_value() && outputPlan.ctf->sources.empty() && preflightDiagnostics.events().empty()) + ASSERT_TRUE(outputPlan.ctf.has_value() && outputPlan.ctf->metadata.sources.empty() && + preflightDiagnostics.events().empty()) << "CTF preflight must exclude software channel zero metadata"; auto options = std::move(*outputPlan.ctf); CtfBundleOutput output(std::move(options)); @@ -576,7 +603,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputRejectsOverlappingTargetsBeforeDeletion writeTestFile(nestedXml, "old-xml"); const auto rejected = throwsException( - [&] { CtfBundleOutput output(CtfOutputConfig(ctfDirectory, nestedXml, 1000000U, {}, {})); }); + [&] { CtfBundleOutput output(makeCtfBundleConfig(ctfDirectory, nestedXml, 1000000U)); }); ASSERT_TRUE(rejected && readTestTextFile(ctfDirectory / "old-marker") == "old-ctf" && readTestTextFile(nestedXml) == "old-xml") << "overlapping CTF targets must be rejected before either existing target is deleted"; @@ -586,7 +613,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputRejectsOverlappingTargetsBeforeDeletion writeTestFile(wrongTypeCtf, "not-a-directory"); std::filesystem::create_directory(wrongTypeXml); const auto rejectedWrongTypes = throwsException([&] { - CtfBundleOutput output(CtfOutputConfig(wrongTypeCtf, wrongTypeXml, 1000000U, {}, {})); + CtfBundleOutput output(makeCtfBundleConfig(wrongTypeCtf, wrongTypeXml, 1000000U)); output.start(); }); ASSERT_TRUE(rejectedWrongTypes && readTestTextFile(wrongTypeCtf) == "not-a-directory" && @@ -595,7 +622,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputRejectsOverlappingTargetsBeforeDeletion const auto rejectedCaseInsensitiveOverlap = throwsException([&] { CtfBundleOutput output( - CtfOutputConfig(root / "Bundle.ctf", root / "BUNDLE.CTF" / "Bundle.SWO.traceanalysis.xml", 1000000U, {}, {})); + makeCtfBundleConfig(root / "Bundle.ctf", root / "BUNDLE.CTF" / "Bundle.SWO.traceanalysis.xml", 1000000U)); }); ASSERT_TRUE(rejectedCaseInsensitiveOverlap) << "CTF target overlap checks must conservatively ignore ASCII case"; } @@ -659,8 +686,8 @@ TEST(CtraceUnitTests, testCtfBundleOutputRejectsUnsafeTargets) const auto safeCtf = temporaryPath.path() / "safe.ctf"; const auto safeXml = temporaryPath.path() / "safe.xml"; for (const auto& unsafe : {std::filesystem::path{}, std::filesystem::path("."), std::filesystem::path("..")}) { - EXPECT_THROW((void)CtfBundleOutput(CtfOutputConfig(unsafe, safeXml, 1000000U, {}, {})), std::invalid_argument); - EXPECT_THROW((void)CtfBundleOutput(CtfOutputConfig(safeCtf, unsafe, 1000000U, {}, {})), std::invalid_argument); + EXPECT_THROW((void)CtfBundleOutput(makeCtfBundleConfig(unsafe, safeXml, 1000000U)), std::invalid_argument); + EXPECT_THROW((void)CtfBundleOutput(makeCtfBundleConfig(safeCtf, unsafe, 1000000U)), std::invalid_argument); } } @@ -670,15 +697,15 @@ TEST(CtraceUnitTests, testCtfBundleOutputRejectsInvalidExistingXmlAndLongPaths) const auto ctfDirectory = temporaryPath.path() / "output.ctf"; const auto xmlDirectory = temporaryPath.path() / "output.xml"; std::filesystem::create_directories(xmlDirectory); - CtfBundleOutput directoryXml(CtfOutputConfig(ctfDirectory, xmlDirectory, 1000000U, {}, {})); + CtfBundleOutput directoryXml(makeCtfBundleConfig(ctfDirectory, xmlDirectory, 1000000U)); EXPECT_THROW(directoryXml.start(), std::runtime_error); const auto longName = std::string(1024U, 'x'); CtfBundleOutput longCtf( - CtfOutputConfig(temporaryPath.path() / longName, temporaryPath.path() / "long-ctf.xml", 1000000U, {}, {})); + makeCtfBundleConfig(temporaryPath.path() / longName, temporaryPath.path() / "long-ctf.xml", 1000000U)); EXPECT_THROW(longCtf.start(), std::runtime_error); CtfBundleOutput longXml( - CtfOutputConfig(temporaryPath.path() / "long-xml.ctf", temporaryPath.path() / longName, 1000000U, {}, {})); + makeCtfBundleConfig(temporaryPath.path() / "long-xml.ctf", temporaryPath.path() / longName, 1000000U)); EXPECT_THROW(longXml.start(), std::runtime_error); } @@ -704,7 +731,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputCleansUpAfterTraceCompassStartFailure) const auto outputDirectory = root.path() / "trace.ctf"; const auto xmlPath = blockedParent / "trace.xml"; - CtfBundleOutput output(CtfOutputConfig(outputDirectory, xmlPath, 1000000U, {}, {})); + CtfBundleOutput output(makeCtfBundleConfig(outputDirectory, xmlPath, 1000000U)); EXPECT_THROW(output.start(), std::runtime_error); EXPECT_FALSE(std::filesystem::exists(outputDirectory)); } @@ -716,8 +743,8 @@ TEST(CtraceUnitTests, testCtfBundleOutputReportsPseudoFilesystemStartFailure) } const TemporaryTestPath temporaryPath("ctrace-ctf-pseudo-filesystem-test"); const auto outputDirectory = temporaryPath.path() / "output.ctf"; - CtfBundleOutput output(CtfOutputConfig( - outputDirectory, TestPlatform::creationFailurePath("ctrace-coverage-output.xml"), 1000000U, {}, {})); + CtfBundleOutput output( + makeCtfBundleConfig(outputDirectory, TestPlatform::creationFailurePath("ctrace-coverage-output.xml"), 1000000U)); EXPECT_THROW(output.start(), std::runtime_error); EXPECT_FALSE(std::filesystem::exists(outputDirectory)); } @@ -733,7 +760,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputReportsPermissionFailures) const auto destructorCtf = root / "destructor.ctf"; const auto destructorXml = root / "destructor.xml"; { - CtfBundleOutput output(CtfOutputConfig(destructorCtf, destructorXml, 1000000U, {}, {})); + CtfBundleOutput output(makeCtfBundleConfig(destructorCtf, destructorXml, 1000000U)); output.start(); std::filesystem::permissions(root, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec); } @@ -747,7 +774,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputReportsPermissionFailures) const auto existingXml = root / "existing.xml"; writeTestFile(existingCtf / "marker", "existing"); std::filesystem::permissions(root, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec); - CtfBundleOutput removeDirectoryFailure(CtfOutputConfig(existingCtf, existingXml, 1000000U, {}, {})); + CtfBundleOutput removeDirectoryFailure(makeCtfBundleConfig(existingCtf, existingXml, 1000000U)); EXPECT_THROW(removeDirectoryFailure.start(), std::runtime_error); std::filesystem::permissions(root, std::filesystem::perms::owner_all); std::filesystem::remove_all(existingCtf); @@ -759,20 +786,20 @@ TEST(CtraceUnitTests, testCtfBundleOutputReportsPermissionFailures) const auto blockedXml = blockedParent / "existing.xml"; writeTestFile(blockedXml, "existing"); std::filesystem::permissions(blockedParent, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec); - CtfBundleOutput removeFileFailure(CtfOutputConfig(writableParent / "output.ctf", blockedXml, 1000000U, {}, {})); + CtfBundleOutput removeFileFailure(makeCtfBundleConfig(writableParent / "output.ctf", blockedXml, 1000000U)); EXPECT_THROW(removeFileFailure.start(), std::runtime_error); std::filesystem::permissions(blockedParent, std::filesystem::perms::owner_all); const auto cleanupCtf = writableParent / "cleanup.ctf"; std::filesystem::permissions(blockedParent, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec); - CtfBundleOutput startCleanupFailure(CtfOutputConfig(cleanupCtf, blockedParent / "new.xml", 1000000U, {}, {})); + CtfBundleOutput startCleanupFailure(makeCtfBundleConfig(cleanupCtf, blockedParent / "new.xml", 1000000U)); EXPECT_THROW(startCleanupFailure.start(), std::runtime_error); EXPECT_FALSE(std::filesystem::exists(cleanupCtf)); std::filesystem::permissions(blockedParent, std::filesystem::perms::owner_all); const auto blockedCtf = blockedParent / "new.ctf"; std::filesystem::permissions(blockedParent, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec); - CtfBundleOutput createDirectoryFailure(CtfOutputConfig(blockedCtf, writableParent / "new.xml", 1000000U, {}, {})); + CtfBundleOutput createDirectoryFailure(makeCtfBundleConfig(blockedCtf, writableParent / "new.xml", 1000000U)); EXPECT_THROW(createDirectoryFailure.start(), std::runtime_error); std::filesystem::permissions(blockedParent, std::filesystem::perms::owner_all); } diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp index 28aa95da6..26258d557 100644 --- a/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp +++ b/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp @@ -58,21 +58,42 @@ static std::string formatCtfUuid(const std::vector& bytes, std::s return result; } -/** @brief Creates resolved DWT source metadata for encoder tests. */ -static ResolvedTraceSource resolvedDwtSource(std::uint32_t comparator, std::uint8_t traceBusId, std::string type, - std::uint8_t size) +/** @brief Creates resolved DWT source metadata for one exact legacy route. */ +static CtfSourceDescriptor resolvedDwtSource(std::uint32_t comparator, std::string type, std::uint8_t size, + TraceRouteIdentity route = {}) { - ResolvedTraceSource source; + CtfSourceDescriptor source; source.type = "dwt"; source.source = comparator; - source.route = traceBusId == 0U - ? TraceRouteIdentity{} - : TraceRouteIdentity{TraceRouteId{traceBusId}, std::optional(traceBusId)}; + source.route = route; source.dataType = std::move(type); source.dataSize = size; return source; } +/** @brief Creates an explicit legacy SINGLE-stream encoder configuration. */ +static CtfEncoderConfig legacyEncoderConfig(std::uint64_t clockHz, TraceSelection selection = {}, + std::vector sources = {}, + DiagnosticSink* diagnostics = nullptr, + std::vector routes = {}, + bool legacyRouteFallback = true) +{ + const auto metadataRoute = routes.empty() ? TraceRouteIdentity{} : routes.front(); + return { + CtfTestSupport::legacyTopology(clockHz, metadataRoute, std::move(sources)), + std::move(selection), + diagnostics, + std::move(routes), + legacyRouteFallback, + }; +} + +/** @brief Starts an encoder with a deterministic bundle UUID. */ +static void startEncoder(CtfEncoder& encoder, const std::filesystem::path& outputDirectory) +{ + encoder.start(outputDirectory, CtfTestSupport::testUuid()); +} + TEST(CtraceUnitTests, testCtfEncoderWritesOnlyIntoProvidedDirectory) { const TemporaryTestPath temporaryPath("ctrace-ctf-encoder-boundary-test"); @@ -80,17 +101,13 @@ TEST(CtraceUnitTests, testCtfEncoderWritesOnlyIntoProvidedDirectory) const auto missingDirectory = root / "missing"; const auto outputDirectory = root / "provided"; - CtfEncoder encoder(CtfEncoderConfig{ - 1000000U, - TraceSelection{{"itm"}, {}}, - {}, - }); - const auto rejectedMissingDirectory = throwsException([&] { encoder.start(missingDirectory); }); + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{"itm"}, {}})); + const auto rejectedMissingDirectory = throwsException([&] { startEncoder(encoder, missingDirectory); }); ASSERT_TRUE(rejectedMissingDirectory && !std::filesystem::exists(missingDirectory)) << "CtfEncoder must not create or own its output directory"; std::filesystem::create_directories(outputDirectory); - encoder.start(outputDirectory); + startEncoder(encoder, outputDirectory); encoder.writeEvent(atCycle(softwarePacket(1U, 1U, 'A'), 10U)); encoder.stop(); ASSERT_TRUE(std::filesystem::is_regular_file(outputDirectory / "metadata") && @@ -108,16 +125,12 @@ TEST(CtraceUnitTests, testCtfEncoderPcSampleEncoding) const TemporaryTestPath temporaryPath("ctrace-ctf-pc-sample-test"); const auto& outputDirectory = temporaryPath.createDirectory(); - CtfEncoder encoder(CtfEncoderConfig{ - 1000000U, - TraceSelection{{"pcsample"}, {}}, - {}, - }); - encoder.start(outputDirectory); - auto pc = onStream(atCycle(TraceEvent{PcSampleTraceEvent{0x08001234U, false}}, 10U), 3U); + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{"pcsample"}, {}})); + startEncoder(encoder, outputDirectory); + auto pc = atCycle(TraceEvent{PcSampleTraceEvent{0x08001234U, false}}, 10U); pc.quality = TraceQuality{false, true, 0U}; encoder.writeEvent(pc); - auto sleep = onStream(atCycle(TraceEvent{PcSampleTraceEvent{0x12345678U, true}}, 11U), 3U); + auto sleep = atCycle(TraceEvent{PcSampleTraceEvent{0x12345678U, true}}, 11U); sleep.quality = TraceQuality{true, false, 7U}; encoder.writeEvent(sleep); encoder.stop(); @@ -126,7 +139,7 @@ TEST(CtraceUnitTests, testCtfEncoderPcSampleEncoding) ASSERT_EQ(records.size(), 2U); for (const auto& record : records) { EXPECT_EQ(record.id, CtfSchema::value(CtfSchema::EventId::PcSample)); - EXPECT_EQ(record.traceBusId, 3U); + EXPECT_EQ(record.traceBusId, 0U); } ASSERT_EQ(records[0].payload.size(), 10U); EXPECT_EQ(records[0].timestamp, 10U); @@ -153,13 +166,9 @@ TEST(CtraceUnitTests, testCtfEncoderExpandsDwtEventCounterMask) const TemporaryTestPath temporaryPath("ctrace-ctf-dwt-event-test"); const auto& outputDirectory = temporaryPath.createDirectory(); - CtfEncoder encoder(CtfEncoderConfig{ - 1000000U, - TraceSelection{{"event"}, {}}, - {}, - }); - encoder.start(outputDirectory); - auto event = onStream(atCycle(TraceEvent{DwtEventTraceEvent{0x3fU}}, 123U), 3U); + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{"event"}, {}})); + startEncoder(encoder, outputDirectory); + auto event = atCycle(TraceEvent{DwtEventTraceEvent{0x3fU}}, 123U); event.quality = TraceQuality{true, false, 7U}; encoder.writeEvent(event); encoder.stop(); @@ -171,7 +180,7 @@ TEST(CtraceUnitTests, testCtfEncoderExpandsDwtEventCounterMask) const auto& record = records[index]; EXPECT_EQ(record.id, CtfSchema::value(CtfSchema::EventId::DwtEvent)); EXPECT_EQ(record.timestamp, 123U); - EXPECT_EQ(record.traceBusId, 3U); + EXPECT_EQ(record.traceBusId, 0U); ASSERT_EQ(record.payload.size(), 6U); EXPECT_EQ(record.payload[0U], expectedCounters[index]); EXPECT_EQ(record.payload[1U], CtfSchema::SampleFlagOverflow | CtfSchema::SampleFlagBeforeFirstTimestamp); @@ -188,13 +197,9 @@ TEST(CtraceUnitTests, testCtfEncoderWritesDwtMatch) const TemporaryTestPath temporaryPath("ctrace-ctf-dwt-match-test"); const auto& outputDirectory = temporaryPath.createDirectory(); - CtfEncoder encoder(CtfEncoderConfig{ - 1000000U, - TraceSelection{{"dwt"}, {}}, - {}, - }); - encoder.start(outputDirectory); - auto match = onStream(atCycle(TraceEvent{DwtMatchTraceEvent{2U}}, 123U), 3U); + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{"dwt"}, {}})); + startEncoder(encoder, outputDirectory); + auto match = atCycle(TraceEvent{DwtMatchTraceEvent{2U}}, 123U); match.quality = TraceQuality{true, false, 7U}; encoder.writeEvent(match); encoder.stop(); @@ -204,7 +209,7 @@ TEST(CtraceUnitTests, testCtfEncoderWritesDwtMatch) const auto& record = records.front(); EXPECT_EQ(record.id, CtfSchema::value(CtfSchema::EventId::DwtMatch)); EXPECT_EQ(record.timestamp, 123U); - EXPECT_EQ(record.traceBusId, 3U); + EXPECT_EQ(record.traceBusId, 0U); ASSERT_EQ(record.payload.size(), 6U); EXPECT_EQ(record.payload[0U], 2U); EXPECT_EQ(record.payload[1U], CtfSchema::SampleFlagOverflow | CtfSchema::SampleFlagBeforeFirstTimestamp); @@ -220,13 +225,9 @@ TEST(CtraceUnitTests, testCtfEncoderExpandsPmuEventCounterMask) const TemporaryTestPath temporaryPath("ctrace-ctf-pmu-event-test"); const auto& outputDirectory = temporaryPath.createDirectory(); - CtfEncoder encoder(CtfEncoderConfig{ - 1000000U, - TraceSelection{{"pmu"}, {}}, - {}, - }); - encoder.start(outputDirectory); - auto event = onStream(atCycle(TraceEvent{PmuTraceEvent{0x81U}}, 124U), 5U); + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{"pmu"}, {}})); + startEncoder(encoder, outputDirectory); + auto event = atCycle(TraceEvent{PmuTraceEvent{0x81U}}, 124U); event.quality = TraceQuality{false, true, 9U}; encoder.writeEvent(event); encoder.stop(); @@ -238,7 +239,7 @@ TEST(CtraceUnitTests, testCtfEncoderExpandsPmuEventCounterMask) const auto& record = records[index]; EXPECT_EQ(record.id, CtfSchema::value(CtfSchema::EventId::PmuEvent)); EXPECT_EQ(record.timestamp, 124U); - EXPECT_EQ(record.traceBusId, 5U); + EXPECT_EQ(record.traceBusId, 0U); ASSERT_EQ(record.payload.size(), 6U); EXPECT_EQ(record.payload[0U], expectedCounters[index]); EXPECT_EQ(record.payload[1U], CtfSchema::SampleFlagTimestampReliable); @@ -255,12 +256,8 @@ TEST(CtraceUnitTests, testCtfEncoderPacketBoundaryAndUuid) const TemporaryTestPath temporaryPath("ctrace-ctf-encoder-packet-boundary-test"); const auto& outputDirectory = temporaryPath.createDirectory(); - CtfEncoder encoder(CtfEncoderConfig{ - 1000000U, - TraceSelection{{"itm"}, {}}, - {}, - }); - encoder.start(outputDirectory); + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{"itm"}, {}})); + startEncoder(encoder, outputDirectory); // A one-byte ITM event occupies 21 bytes. Exactly 3118 events fit after // the 56-byte header/context; event 3119 starts packet 2. @@ -289,9 +286,15 @@ TEST(CtraceUnitTests, testCtfEncoderPacketBoundaryAndUuid) << "CTF packet sequence must advance across a 64-KiB boundary"; ASSERT_TRUE(std::equal(stream.begin() + 4U, stream.begin() + 20U, stream.begin() + kCtfPacketSize + 4U)) << "CTF packet UUID must remain stable across packet rollover"; + const auto& injectedUuid = CtfTestSupport::testUuid(); + ASSERT_TRUE( + std::equal(injectedUuid.bytes().begin(), injectedUuid.bytes().end(), stream.begin() + 4U) && + std::equal(injectedUuid.bytes().begin(), injectedUuid.bytes().end(), stream.begin() + kCtfPacketSize + 4U)) + << "every packet header must use the explicitly injected bundle UUID"; const auto uuid = formatCtfUuid(stream, 4U); - ASSERT_TRUE(metadata.find("uuid = \"" + uuid + "\";") != std::string::npos) + ASSERT_EQ(uuid, injectedUuid.toString()); + ASSERT_TRUE(metadata.find("uuid = \"" + injectedUuid.toString() + "\";") != std::string::npos) << "CTF metadata UUID must match the binary packet UUID"; ASSERT_TRUE((stream[4U + 6U] & 0xf0U) == 0x40U && (stream[4U + 8U] & 0xc0U) == 0x80U) << "CTF UUID must use RFC 4122 version-4 and variant bits"; @@ -304,12 +307,8 @@ TEST(CtraceUnitTests, testCtfEncoderDwtAddressEncoding) const TemporaryTestPath temporaryPath("ctrace-ctf-encoder-dwt-address-test"); const auto& outputDirectory = temporaryPath.createDirectory(); - CtfEncoder encoder(CtfEncoderConfig{ - 1000000U, - TraceSelection{{"dwt"}, {}}, - {}, - }); - encoder.start(outputDirectory); + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{"dwt"}, {}})); + startEncoder(encoder, outputDirectory); encoder.writeEvent(atCycle(TraceEvent{DwtAddressTraceEvent{ 3U, DwtPcAndDataAddressTraceLocation{{4U, 0x12345678U}, {2U, 0x0000abcdU}}, @@ -358,72 +357,107 @@ TEST(CtraceUnitTests, testCtfEncoderDwtAddressEncoding) TEST(CtraceUnitTests, testCtfEncoderRejectsInvalidClockAndPayloadMetadata) { - EXPECT_THROW((void)CtfEncoder(CtfEncoderConfig{}), std::invalid_argument); - const TemporaryTestPath temporaryPath("ctrace-ctf-invalid-payload-test"); temporaryPath.createDirectory(); - CtfEncoder invalidItm(CtfEncoderConfig{1000000U, TraceSelection{{"itm"}, {}}, {}}); + CtfEncoder missingTopology(CtfEncoderConfig{}); + EXPECT_THROW(startEncoder(missingTopology, temporaryPath.path()), std::runtime_error); + + CtfEncoder zeroClock(legacyEncoderConfig(0U)); + EXPECT_THROW(startEncoder(zeroClock, temporaryPath.path()), std::invalid_argument); + + CtfEncoder invalidItm(legacyEncoderConfig(1000000U, TraceSelection{{"itm"}, {}})); invalidItm.stop(); invalidItm.writeEvent(softwarePacket(1U)); - invalidItm.start(temporaryPath.path()); + startEncoder(invalidItm, temporaryPath.path()); EXPECT_THROW(invalidItm.writeEvent(softwarePacket(1U, 3U, 0U)), std::runtime_error); invalidItm.abort(); - CtfEncoder invalidDwt(CtfEncoderConfig{ - 1000000U, - TraceSelection{{"dwt"}, {}}, - {resolvedDwtSource(0U, 1U, "unsupported", 3U)}, - }); - invalidDwt.start(temporaryPath.path()); - EXPECT_THROW(invalidDwt.writeEvent(onStream(TraceEvent{DwtDataTraceEvent{0U, 1U, 0U, AccessType::Read}}, 1U)), - std::runtime_error); - EXPECT_THROW( - invalidDwt.writeEvent(TraceEvent{DwtAddressTraceEvent{0U, DwtDataAddressTraceLocation{{3U, 0U}}}}), + CtfEncoder invalidDwt( + legacyEncoderConfig(1000000U, TraceSelection{{"dwt"}, {}}, {resolvedDwtSource(0U, "unsupported", 3U)})); + EXPECT_THROW(startEncoder(invalidDwt, temporaryPath.path()), std::invalid_argument); + + CtfEncoder invalidAddress(legacyEncoderConfig(1000000U, TraceSelection{{"dwt"}, {}})); + startEncoder(invalidAddress, temporaryPath.path()); + EXPECT_THROW(invalidAddress.writeEvent(TraceEvent{DwtAddressTraceEvent{0U, DwtDataAddressTraceLocation{{3U, 0U}}}}), std::runtime_error); - invalidDwt.abort(); + invalidAddress.abort(); } -TEST(CtraceUnitTests, testCtfEncoderWritesAllDwtValueVariants) +TEST(CtraceUnitTests, testCtfEncoderRejectsFormattedTopologiesBeforeOpeningAStream) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-formatted-runtime-boundary-test"); + temporaryPath.createDirectory(); + const auto singleDirectory = temporaryPath.path() / "single"; + const auto multipleDirectory = temporaryPath.path() / "multiple"; + std::filesystem::create_directories(singleDirectory); + std::filesystem::create_directories(multipleDirectory); + const auto hardStop = "CTF binary output currently requires exactly one legacy SINGLE stream topology"; + + CtfMetadataTopology singleTopology{ + {{CtfClockDomainId{1U}, "formatted_clock", CtfTestSupport::testUuid(1U), 1000000U, false}}, + {{CtfStreamClassId{1U}, {TraceRouteId{7U}, 1U}, CtfSourceKind::Itm, std::string("core"), CtfClockDomainId{1U}}}, + {}, + }; + CtfEncoder single(CtfEncoderConfig{std::move(singleTopology), {}, nullptr, {}, false}); + EXPECT_TRUE(throwsWithMessage([&] { startEncoder(single, singleDirectory); }, hardStop)); + EXPECT_FALSE(std::filesystem::exists(singleDirectory / "stream_0")); + + CtfMetadataTopology multipleTopology{ + { + {CtfClockDomainId{3U}, "first_clock", CtfTestSupport::testUuid(3U), 1000000U, false}, + {CtfClockDomainId{9U}, "second_clock", CtfTestSupport::testUuid(9U), 2000000U, false}, + }, + { + {CtfStreamClassId{1U}, + {TraceRouteId{4U}, 1U}, + CtfSourceKind::Itm, + std::string("first"), + CtfClockDomainId{3U}}, + {CtfStreamClassId{111U}, + {TraceRouteId{90U}, 111U}, + CtfSourceKind::Itm, + std::string("second"), + CtfClockDomainId{9U}}, + }, + {}, + }; + CtfEncoder multiple(CtfEncoderConfig{std::move(multipleTopology), {}, nullptr, {}, false}); + EXPECT_TRUE(throwsWithMessage([&] { startEncoder(multiple, multipleDirectory); }, hardStop)); + EXPECT_FALSE(std::filesystem::exists(multipleDirectory / "stream_0")); +} + +TEST(CtraceUnitTests, testCtfEncoderWritesConfiguredDwtValueVariantsAndDefault) { const TemporaryTestPath temporaryPath("ctrace-ctf-value-variants-test"); temporaryPath.createDirectory(); - std::vector sources{ - resolvedDwtSource(0U, 1U, "signed", 2U), resolvedDwtSource(1U, 1U, "float", 4U), - resolvedDwtSource(2U, 1U, "signed", 4U), resolvedDwtSource(3U, 7U, "unsigned", 1U), - resolvedDwtSource(4U, 1U, "signed", 1U), resolvedDwtSource(4U, 2U, "signed", 1U), + std::vector sources{ + resolvedDwtSource(0U, "signed", 2U), + resolvedDwtSource(1U, "float", 4U), + resolvedDwtSource(2U, "signed", 4U), }; - sources.push_back(resolvedDwtSource(99U, 7U, "unsigned", 1U)); - - CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{"dwt"}, {}}, sources}); - encoder.start(temporaryPath.path()); - - auto signed16 = atCycle( - onStream(TraceEvent{DwtDataTraceEvent{ - 0U, - 2U, - 0xff80U, - AccessType::Write, - DwtAddressFragment{2U, 0x1234U}, - DwtAddressFragment{2U, 0x5678U}}}, - 1U), - 10U); + + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{"dwt"}, {}}, sources)); + startEncoder(encoder, temporaryPath.path()); + + auto signed16 = + atCycle(TraceEvent{DwtDataTraceEvent{0U, 2U, 0xff80U, AccessType::Write, DwtAddressFragment{2U, 0x1234U}, + DwtAddressFragment{2U, 0x5678U}}}, + 10U); signed16.quality = TraceQuality{false, true, 0U}; encoder.writeEvent(signed16); - encoder.writeEvent(atCycle(onStream(TraceEvent{DwtDataTraceEvent{1U, 4U, 0x3f800000U, AccessType::Read}}, 1U), 11U)); - encoder.writeEvent(atCycle(onStream(TraceEvent{DwtDataTraceEvent{2U, 4U, 0xffffffffU, AccessType::Read}}, 1U), 12U)); - encoder.writeEvent(atCycle(TraceEvent{DwtDataTraceEvent{3U, 1U, 0x12U, AccessType::Read}}, 13U)); - encoder.writeEvent(atCycle(TraceEvent{DwtDataTraceEvent{4U, 1U, 0xffU, AccessType::Read}}, 14U)); - encoder.writeEvent(atCycle(TraceEvent{DwtDataTraceEvent{6U, 4U, 0x12345678U, AccessType::Read}}, 15U)); + encoder.writeEvent(atCycle(TraceEvent{DwtDataTraceEvent{1U, 4U, 0x3f800000U, AccessType::Read}}, 11U)); + encoder.writeEvent(atCycle(TraceEvent{DwtDataTraceEvent{2U, 4U, 0xffffffffU, AccessType::Read}}, 12U)); + encoder.writeEvent(atCycle(TraceEvent{DwtDataTraceEvent{3U, 4U, 0x12345678U, AccessType::Read}}, 13U)); encoder.stop(); const auto records = readCtfRecords(temporaryPath.path() / "stream_0"); - ASSERT_EQ(records.size(), 6U); + ASSERT_EQ(records.size(), 4U); for (const auto& record : records) { EXPECT_EQ(record.id, CtfSchema::value(CtfSchema::EventId::DwtValue)); } EXPECT_EQ(records[0].timestamp, 10U); - EXPECT_EQ(records[0].traceBusId, 1U); + EXPECT_EQ(records[0].traceBusId, 0U); EXPECT_EQ(records[0].payload[0U], 0U); EXPECT_EQ(records[0].payload[1U], CtfSchema::value(CtfSchema::DwtAccess::Write)); EXPECT_EQ(records[0].payload[2U], CtfSchema::value(CtfSchema::ValueTag::Signed16)); @@ -441,30 +475,23 @@ TEST(CtraceUnitTests, testCtfEncoderWritesAllDwtValueVariants) EXPECT_EQ(readLe32(records[2].payload, 3U), 0xffffffffU); EXPECT_EQ(records[3].timestamp, 13U); EXPECT_EQ(records[3].traceBusId, 0U); - EXPECT_EQ(records[3].payload[2U], CtfSchema::value(CtfSchema::ValueTag::Unsigned8)); - EXPECT_EQ(records[3].payload[3U], 0x12U); - // The unformatted stream must retain equivalent metadata from both configured routes. - EXPECT_EQ(records[4].timestamp, 14U); - EXPECT_EQ(records[4].payload[2U], CtfSchema::value(CtfSchema::ValueTag::Signed8)); - EXPECT_EQ(records[4].payload[3U], 0xffU); - EXPECT_EQ(records[5].timestamp, 15U); - EXPECT_EQ(records[5].payload[2U], CtfSchema::value(CtfSchema::ValueTag::Unsigned32)); - EXPECT_EQ(readLe32(records[5].payload, 3U), 0x12345678U); + EXPECT_EQ(records[3].payload[2U], CtfSchema::value(CtfSchema::ValueTag::Unsigned32)); + EXPECT_EQ(readLe32(records[3].payload, 3U), 0x12345678U); } -TEST(CtraceUnitTests, testCtfEncoderRejectsConflictingUnformattedDwtRoutes) +TEST(CtraceUnitTests, testCtfEncoderDoesNotBorrowDwtMetadataFromAnotherRoute) { - const TemporaryTestPath temporaryPath("ctrace-ctf-conflicting-dwt-routes-test"); + const TemporaryTestPath temporaryPath("ctrace-ctf-exact-dwt-route-test"); temporaryPath.createDirectory(); - const std::vector sources{ - resolvedDwtSource(0U, 1U, "signed", 1U), - resolvedDwtSource(0U, 2U, "float", 4U), - }; - CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{"dwt"}, {}}, sources}); - encoder.start(temporaryPath.path()); - EXPECT_TRUE( - throwsWithMessage([&] { encoder.writeEvent(TraceEvent{DwtDataTraceEvent{0U, 1U, 0xffU, AccessType::Read}}); }, - "conflicting metadata for unformatted dwt source 0")); + const TraceRouteIdentity configured{TraceRouteId{0U}, std::nullopt}; + const TraceRouteIdentity other{TraceRouteId{9U}, std::nullopt}; + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{"dwt"}, {}}, + {resolvedDwtSource(0U, "signed", 1U, configured)}, nullptr, + {configured, other}, false)); + startEncoder(encoder, temporaryPath.path()); + EXPECT_TRUE(throwsWithMessage( + [&] { encoder.writeEvent(onRoute(TraceEvent{DwtDataTraceEvent{0U, 1U, 0xffU, AccessType::Read}}, other)); }, + "without an exact runtime stream descriptor")); encoder.abort(); } @@ -472,17 +499,16 @@ TEST(CtraceUnitTests, testCtfEncoderReportsRoutedDwtSizeMismatchContext) { const TemporaryTestPath temporaryPath("ctrace-ctf-routed-size-warning-test"); temporaryPath.createDirectory(); - const TraceRouteIdentity route{TraceRouteId{9U}, 7U}; - auto source = resolvedDwtSource(0U, 7U, "unsigned", 4U); - source.route = route; + const TraceRouteIdentity route{TraceRouteId{9U}, std::nullopt}; + auto source = resolvedDwtSource(0U, "unsigned", 4U, route); CollectingDiagnosticSink diagnostics; - CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{"dwt"}, {}}, {source}, &diagnostics, {route}}); - encoder.start(temporaryPath.path()); + CtfEncoder encoder( + legacyEncoderConfig(1000000U, TraceSelection{{"dwt"}, {}}, {source}, &diagnostics, {route}, false)); + startEncoder(encoder, temporaryPath.path()); encoder.writeEvent(onRoute(TraceEvent{DwtDataTraceEvent{0U, 1U, 0U, AccessType::Read}}, route)); encoder.stop(); ASSERT_EQ(diagnostics.events().size(), 1U); - EXPECT_TRUE(diagnostics.containsContext("stream", "7")); EXPECT_TRUE(diagnostics.containsContext("channel", "DWT0")); } @@ -490,9 +516,9 @@ TEST(CtraceUnitTests, testCtfEncoderIgnoresUnselectedStreamTimeAndQuality) { const TemporaryTestPath temporaryPath("ctrace-ctf-filtered-stream-state-test"); temporaryPath.createDirectory(); - const TraceRouteIdentity selectedRoute{TraceRouteId{1U}, 1U}; - CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{"itm"}, {1U}}, {}, nullptr, {selectedRoute}}); - encoder.start(temporaryPath.path()); + const TraceRouteIdentity selectedRoute{TraceRouteId{1U}, std::nullopt}; + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{"itm"}, {0U}}, {}, nullptr, {selectedRoute}, false)); + startEncoder(encoder, temporaryPath.path()); auto excludedTimestamp = atCycle(onStream(TraceEvent{LocalTimestampTraceEvent{}}, 2U), 900U); excludedTimestamp.quality = TraceQuality{false, true, 0U}; @@ -501,7 +527,7 @@ TEST(CtraceUnitTests, testCtfEncoderIgnoresUnselectedStreamTimeAndQuality) excludedOverflow.quality = TraceQuality{true, false, 99U}; encoder.writeEvent(excludedOverflow); - auto selected = atCycle(onStream(softwarePacket(1U, 1U, 'A'), 1U), 10U); + auto selected = atCycle(onRoute(softwarePacket(1U, 1U, 'A'), selectedRoute), 10U); selected.quality = TraceQuality{false, true, 0U}; encoder.writeEvent(selected); encoder.stop(); @@ -510,7 +536,7 @@ TEST(CtraceUnitTests, testCtfEncoderIgnoresUnselectedStreamTimeAndQuality) ASSERT_EQ(records.size(), 1U); EXPECT_EQ(records[0].id, CtfSchema::value(CtfSchema::EventId::Itm)); EXPECT_EQ(records[0].timestamp, 10U); - EXPECT_EQ(records[0].traceBusId, 1U); + EXPECT_EQ(records[0].traceBusId, 0U); EXPECT_EQ(records[0].payload[3U], CtfSchema::SampleFlagTimestampReliable); EXPECT_EQ(readLe32(records[0].payload, 4U), 0U); } @@ -519,8 +545,8 @@ TEST(CtraceUnitTests, testCtfEncoderDoesNotBootstrapNoBusRouteForAnotherExplicit { const TemporaryTestPath temporaryPath("ctrace-ctf-no-unknown-route-bootstrap-test"); temporaryPath.createDirectory(); - CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{}, {2U}}, {}}); - encoder.start(temporaryPath.path()); + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{}, {2U}})); + startEncoder(encoder, temporaryPath.path()); encoder.stop(); EXPECT_TRUE(readCtfRecords(temporaryPath.path() / "stream_0").empty()); @@ -530,12 +556,8 @@ TEST(CtraceUnitTests, testCtfEncoderDoesNotBootstrapNoBusRouteWhenKnownSourceIsF { const TemporaryTestPath temporaryPath("ctrace-ctf-no-source-route-bootstrap-test"); temporaryPath.createDirectory(); - CtfEncoder encoder(CtfEncoderConfig{ - 1000000U, - TraceSelection{{}, {0U}}, - {resolvedDwtSource(0U, 1U, "unsigned", 4U)}, - }); - encoder.start(temporaryPath.path()); + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{}, {1U}}, {resolvedDwtSource(0U, "unsigned", 4U)})); + startEncoder(encoder, temporaryPath.path()); encoder.stop(); EXPECT_TRUE(readCtfRecords(temporaryPath.path() / "stream_0").empty()); @@ -545,17 +567,17 @@ TEST(CtraceUnitTests, testCtfEncoderStreamSelectionKeepsStartAndResyncContext) { const TemporaryTestPath temporaryPath("ctrace-ctf-selected-stream-status-test"); temporaryPath.createDirectory(); - CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{}, {3U}}, {}}); - encoder.start(temporaryPath.path()); - encoder.writeEvent(onStream(exceptionPacket(15U, ExceptionAction::Entered, 10U), 3U)); - encoder.writeEvent(atCycle(onStream(TraceEvent{SyncTraceEvent{}}, 3U), 11U)); - encoder.writeEvent(onStream(exceptionPacket(54U, ExceptionAction::Entered, 20U), 3U)); + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{}, {0U}})); + startEncoder(encoder, temporaryPath.path()); + encoder.writeEvent(exceptionPacket(15U, ExceptionAction::Entered, 10U)); + encoder.writeEvent(atCycle(TraceEvent{SyncTraceEvent{}}, 11U)); + encoder.writeEvent(exceptionPacket(54U, ExceptionAction::Entered, 20U)); encoder.stop(); const auto records = readCtfRecords(temporaryPath.path() / "stream_0"); std::vector statusReasons; for (const auto& record : records) { - EXPECT_EQ(record.traceBusId, 3U); + EXPECT_EQ(record.traceBusId, 0U); if (record.id == CtfSchema::value(CtfSchema::EventId::TraceStatus)) { statusReasons.push_back(record.payload[0U]); } @@ -578,13 +600,13 @@ TEST(CtraceUnitTests, testCtfEncoderTracksLocalTimeAndUnqualifiedOverflow) { const TemporaryTestPath temporaryPath("ctrace-ctf-time-quality-test"); temporaryPath.createDirectory(); - CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{}, {}}); - encoder.start(temporaryPath.path()); + CtfEncoder encoder(legacyEncoderConfig(1000000U)); + startEncoder(encoder, temporaryPath.path()); - encoder.writeEvent(atCycle(onStream(TraceEvent{LocalTimestampTraceEvent{}}, 3U), 20U)); - encoder.writeEvent(onStream(TraceEvent{OverflowTraceEvent{}}, 3U)); + encoder.writeEvent(atCycle(TraceEvent{LocalTimestampTraceEvent{}}, 20U)); + encoder.writeEvent(TraceEvent{OverflowTraceEvent{}}); - auto saturated = atCycle(onStream(softwarePacket(1U, 1U, 0U), 3U), 21U); + auto saturated = atCycle(softwarePacket(1U, 1U, 0U), 21U); saturated.quality = TraceQuality{true, true, std::numeric_limits::max()}; encoder.writeEvent(saturated); encoder.stop(); @@ -593,12 +615,12 @@ TEST(CtraceUnitTests, testCtfEncoderTracksLocalTimeAndUnqualifiedOverflow) const auto records = readCtfRecords(temporaryPath.path() / "stream_0"); const auto& itm = requireFirstCtfRecord(records, CtfSchema::EventId::Itm, "saturated CTF ITM sample missing"); EXPECT_EQ(itm.timestamp, 21U); - EXPECT_EQ(itm.traceBusId, 3U); + EXPECT_EQ(itm.traceBusId, 0U); EXPECT_EQ(itm.payload[3U], CtfSchema::SampleFlagOverflow | CtfSchema::SampleFlagTimestampReliable); EXPECT_EQ(readLe32(itm.payload, 4U), std::numeric_limits::max()); const auto overflowStatus = std::find_if(records.begin(), records.end(), [](const CtfRecord& record) { - return record.id == CtfSchema::value(CtfSchema::EventId::TraceStatus) && record.traceBusId == 3U && + return record.id == CtfSchema::value(CtfSchema::EventId::TraceStatus) && record.traceBusId == 0U && record.payload[0U] == CtfSchema::value(CtfSchema::TraceStatusReason::Overflow); }); ASSERT_NE(overflowStatus, records.end()); @@ -609,105 +631,76 @@ TEST(CtraceUnitTests, testCtfEncoderLazilyBootstrapsExactSelectedRoute) { const TemporaryTestPath temporaryPath("ctrace-ctf-lazy-route-bootstrap-test"); temporaryPath.createDirectory(); - const TraceRouteIdentity route{TraceRouteId{9U}, 2U}; - CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{}, {2U}}, {}}); - encoder.start(temporaryPath.path()); + const TraceRouteIdentity route{TraceRouteId{9U}, std::nullopt}; + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{}, {0U}}, {}, nullptr, {route}, false)); + startEncoder(encoder, temporaryPath.path()); encoder.writeEvent(onRoute(softwarePacket(1U, 1U, 0x5aU), route)); encoder.stop(); const auto records = readCtfRecords(temporaryPath.path() / "stream_0"); ASSERT_EQ(records.size(), 3U); EXPECT_EQ(records[0].id, CtfSchema::value(CtfSchema::EventId::TraceStatus)); - EXPECT_EQ(records[0].traceBusId, 2U); + EXPECT_EQ(records[0].traceBusId, 0U); EXPECT_EQ(records[0].payload[0U], CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart)); EXPECT_EQ(records[1].id, CtfSchema::value(CtfSchema::EventId::Exception)); - EXPECT_EQ(records[1].traceBusId, 2U); + EXPECT_EQ(records[1].traceBusId, 0U); EXPECT_EQ(records[2].id, CtfSchema::value(CtfSchema::EventId::Itm)); - EXPECT_EQ(records[2].traceBusId, 2U); + EXPECT_EQ(records[2].traceBusId, 0U); } TEST(CtraceUnitTests, testCtfEncoderRejectsConflictingIdentityForSameRouteId) { const TemporaryTestPath temporaryPath("ctrace-ctf-route-mismatch-test"); temporaryPath.createDirectory(); - const TraceRouteIdentity configured{TraceRouteId{4U}, 1U}; + const TraceRouteIdentity configured{TraceRouteId{4U}, std::nullopt}; CtfEncoder invalidConfig( - CtfEncoderConfig{1000000U, TraceSelection{}, {}, nullptr, {configured, {TraceRouteId{4U}, 2U}}}); - EXPECT_THROW(invalidConfig.start(temporaryPath.path()), std::runtime_error); + legacyEncoderConfig(1000000U, TraceSelection{}, {}, nullptr, {configured, {TraceRouteId{4U}, 2U}}, false)); + EXPECT_THROW(startEncoder(invalidConfig, temporaryPath.path()), std::runtime_error); - CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{}, {}, nullptr, {configured}}); - encoder.start(temporaryPath.path()); + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{}, {}, nullptr, {configured}, false)); + startEncoder(encoder, temporaryPath.path()); EXPECT_THROW(encoder.writeEvent(onRoute(softwarePacket(1U), {TraceRouteId{4U}, 2U})), std::runtime_error); - EXPECT_THROW(encoder.writeEvent(onRoute(softwarePacket(1U), {TraceRouteId{9U}, 1U})), std::runtime_error); + EXPECT_THROW(encoder.writeEvent(onRoute(softwarePacket(1U), {TraceRouteId{9U}, std::nullopt})), std::runtime_error); encoder.abort(); - CtfEncoder lazyEncoder(CtfEncoderConfig{1000000U, TraceSelection{}, {}}); - lazyEncoder.start(temporaryPath.path()); - lazyEncoder.writeEvent(onRoute(softwarePacket(1U), configured)); - EXPECT_THROW(lazyEncoder.writeEvent(onRoute(softwarePacket(1U), {TraceRouteId{4U}, 2U})), std::runtime_error); + CtfEncoder lazyEncoder(legacyEncoderConfig(1000000U)); + startEncoder(lazyEncoder, temporaryPath.path()); + EXPECT_TRUE(throwsWithMessage([&] { lazyEncoder.writeEvent(onRoute(softwarePacket(1U), configured)); }, + "without an exact runtime stream descriptor")); lazyEncoder.abort(); } -TEST(CtraceUnitTests, testCtfEncoderKeepsNoBusRouteStateIndependent) +TEST(CtraceUnitTests, testCtfEncoderBootstrapsOnlyTheMetadataStreamRoute) { - const TemporaryTestPath temporaryPath("ctrace-ctf-no-bus-route-state-test"); + const TemporaryTestPath temporaryPath("ctrace-ctf-metadata-route-bootstrap-test"); temporaryPath.createDirectory(); const TraceRouteIdentity first{TraceRouteId{4U}, std::nullopt}; const TraceRouteIdentity second{TraceRouteId{9U}, std::nullopt}; - CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{}, {}, nullptr, {first, second}}); - encoder.start(temporaryPath.path()); - - auto firstOverflow = onRoute(TraceEvent{OverflowTraceEvent{}}, first); - firstOverflow.quality = TraceQuality{true, false, 5U}; - encoder.writeEvent(firstOverflow); - auto secondOverflow = onRoute(TraceEvent{OverflowTraceEvent{}}, second); - secondOverflow.quality = TraceQuality{true, false, 1U}; - encoder.writeEvent(secondOverflow); + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{}, {}, nullptr, {first, second}, false)); + startEncoder(encoder, temporaryPath.path()); encoder.stop(); - std::vector overflowCounts; - for (const auto& record : readCtfRecords(temporaryPath.path() / "stream_0")) { + const auto records = readCtfRecords(temporaryPath.path() / "stream_0"); + ASSERT_EQ(records.size(), 2U); + for (const auto& record : records) { EXPECT_EQ(record.traceBusId, 0U) << "opaque route ID must not leak into the CTF Trace Bus ID field"; - if (record.id == CtfSchema::value(CtfSchema::EventId::TraceStatus) && - record.payload[0U] == CtfSchema::value(CtfSchema::TraceStatusReason::Overflow)) { - overflowCounts.push_back(readLe32(record.payload, 1U)); - } } - EXPECT_EQ(overflowCounts, (std::vector{5U, 1U})); + EXPECT_EQ(records[0].id, CtfSchema::value(CtfSchema::EventId::TraceStatus)); + EXPECT_EQ(records[1].id, CtfSchema::value(CtfSchema::EventId::Exception)); } -TEST(CtraceUnitTests, testCtfEncoderKeepsNoBusExceptionResetStateIndependent) +TEST(CtraceUnitTests, testCtfEncoderRejectsCataloguedRouteWithoutRuntimeStreamDescriptor) { - const TemporaryTestPath temporaryPath("ctrace-ctf-no-bus-exception-state-test"); + const TemporaryTestPath temporaryPath("ctrace-ctf-no-runtime-stream-test"); temporaryPath.createDirectory(); const TraceRouteIdentity first{TraceRouteId{4U}, std::nullopt}; const TraceRouteIdentity second{TraceRouteId{9U}, std::nullopt}; - CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{}, {}, nullptr, {first, second}}); - encoder.start(temporaryPath.path()); + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{}, {}, nullptr, {first, second}, false)); + startEncoder(encoder, temporaryPath.path()); encoder.writeEvent(onRoute(exceptionPacket(15U, ExceptionAction::Entered, 10U), first)); - encoder.writeEvent(onRoute(exceptionPacket(54U, ExceptionAction::Entered, 20U), second)); - encoder.writeEvent(atCycle(onRoute(TraceEvent{OverflowTraceEvent{}}, first), 30U)); - encoder.writeEvent(onRoute(exceptionPacket(54U, ExceptionAction::Exited, 40U), second)); - encoder.stop(); - - const auto records = readCtfRecords(temporaryPath.path() / "stream_0"); - for (const auto& record : records) { - EXPECT_EQ(record.traceBusId, 0U) << "opaque route IDs must remain absent from the legacy CTF field"; - } - const auto exceptions = timestampedCtfExceptionRecords(records); - const auto contains = [&](std::uint64_t timestamp, ExceptionNumber number, std::uint8_t action, std::uint8_t origin) { - return std::find(exceptions.begin(), exceptions.end(), - TimestampedCtfExceptionRecord{timestamp, {number, action, origin}}) != exceptions.end(); - }; - EXPECT_TRUE(contains(10U, 15U, CtfSchema::value(CtfSchema::ExceptionAction::Entered), - CtfSchema::value(CtfSchema::ExceptionOrigin::Trace))); - EXPECT_TRUE(contains(20U, 54U, CtfSchema::value(CtfSchema::ExceptionAction::Entered), - CtfSchema::value(CtfSchema::ExceptionOrigin::Trace))); - EXPECT_TRUE(contains(30U, 15U, CtfSchema::value(CtfSchema::ExceptionAction::Exited), - CtfSchema::value(CtfSchema::ExceptionOrigin::Synthetic))); - EXPECT_FALSE(contains(30U, 54U, CtfSchema::value(CtfSchema::ExceptionAction::Exited), - CtfSchema::value(CtfSchema::ExceptionOrigin::Synthetic))); - EXPECT_TRUE(contains(40U, 54U, CtfSchema::value(CtfSchema::ExceptionAction::Exited), - CtfSchema::value(CtfSchema::ExceptionOrigin::Trace))); + EXPECT_TRUE(throwsWithMessage( + [&] { encoder.writeEvent(onRoute(exceptionPacket(54U, ExceptionAction::Entered, 20U), second)); }, + "without an exact runtime stream descriptor")); + encoder.abort(); } diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfMetadataModelTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfMetadataModelTests.cpp new file mode 100644 index 000000000..961f9dc10 --- /dev/null +++ b/tools/ctrace/test/unit/src/output/ctf/CtfMetadataModelTests.cpp @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfTestSupport.h" +#include "TestSupport.h" + +#include + +#include "ctf/CtfMetadataModel.h" + +#include +#include +#include +#include +#include +#include + +/** @brief Creates one formatted ITM stream descriptor for model tests. */ +static CtfStreamDescriptor modelStream(std::uint32_t routeId, std::uint8_t traceBusId, std::uint32_t clockDomainId) +{ + return { + CtfStreamClassId{traceBusId}, {TraceRouteId{routeId}, traceBusId}, CtfSourceKind::Itm, std::nullopt, + CtfClockDomainId{clockDomainId}, + }; +} + +/** @brief Creates one clock-domain descriptor with a deterministic UUID. */ +static CtfClockDomainDescriptor modelClock(std::uint32_t id, std::string name, std::uint8_t uuidDiscriminator, + std::uint64_t frequencyHz = 1000000U) +{ + return { + CtfClockDomainId{id}, std::move(name), CtfTestSupport::testUuid(uuidDiscriminator), frequencyHz, false, + }; +} + +/** @brief Creates a valid two-route topology with independent equal-frequency domains. */ +static CtfMetadataTopology independentModelTopology() +{ + const TraceRouteIdentity first{TraceRouteId{5U}, 1U}; + const TraceRouteIdentity second{TraceRouteId{70U}, 111U}; + return { + {modelClock(9U, "clock_nine", 9U), modelClock(2U, "clock_two", 2U)}, + {modelStream(70U, 111U, 9U), modelStream(5U, 1U, 2U)}, + { + {"dwt", 0U, second, std::string("second"), 0x2000U, "signed", 2U}, + {"dwt", 0U, first, std::string("first"), 0x1000U, "unsigned", 4U}, + {"itm", 1U, second, std::string("console"), std::nullopt, "unsigned", 4U}, + {"itm", 1U, first, std::string("console"), std::nullopt, "unsigned", 4U}, + }, + }; +} + +TEST(CtraceUnitTests, testCtfMetadataModelKeepsRouteClockAndSourceIdentityIndependent) +{ + CtfMetadataModel model(CtfTestSupport::testUuid(), independentModelTopology()); + const auto& topology = model.topology(); + + ASSERT_EQ(topology.clockDomains.size(), 2U); + EXPECT_EQ(topology.clockDomains[0].id, CtfClockDomainId{2U}); + EXPECT_EQ(topology.clockDomains[1].id, CtfClockDomainId{9U}); + ASSERT_EQ(topology.streams.size(), 2U); + EXPECT_EQ(topology.streams[0].streamClassId, CtfStreamClassId{1U}); + EXPECT_EQ(topology.streams[1].streamClassId, CtfStreamClassId{111U}); + EXPECT_EQ(topology.clockDomains[0].frequencyHz, topology.clockDomains[1].frequencyHz) + << "equal frequencies must not merge independent clock identities"; + EXPECT_NE(topology.clockDomains[0].uuid, topology.clockDomains[1].uuid); + + const TraceRouteIdentity first{TraceRouteId{5U}, 1U}; + const TraceRouteIdentity second{TraceRouteId{70U}, 111U}; + ASSERT_NE(model.streamForRoute(first), nullptr); + ASSERT_NE(model.streamForRoute(second), nullptr); + EXPECT_EQ(model.streamForRoute(first)->clockDomainId, CtfClockDomainId{2U}); + EXPECT_EQ(model.clockDomain(CtfClockDomainId{9U})->name, "clock_nine"); + EXPECT_EQ(model.streamForRoute({TraceRouteId{5U}, 2U}), nullptr); + ASSERT_NE(model.source(first, "dwt", 0U), nullptr); + ASSERT_NE(model.source(second, "dwt", 0U), nullptr); + EXPECT_EQ(model.source(first, "dwt", 0U)->dataType, "unsigned"); + EXPECT_EQ(model.source(second, "dwt", 0U)->dataType, "signed"); + EXPECT_EQ(model.source(first, "dwt", 1U), nullptr); + + model.observeException(CtfStreamClassId{111U}, 54U); + model.observeException(CtfStreamClassId{111U}, 16U); + model.observeException(CtfStreamClassId{111U}, 54U); + EXPECT_EQ(model.observedExceptions(CtfStreamClassId{111U}), (std::vector{16U, 54U})); + EXPECT_TRUE(model.observedExceptions(CtfStreamClassId{1U}).empty()); + EXPECT_THROW(model.observeException(CtfStreamClassId{7U}, 1U), std::runtime_error); + EXPECT_FALSE(model.isLegacySingleStreamLayout()); +} + +TEST(CtraceUnitTests, testCtfMetadataModelAcceptsSharedDomainsAndItmRouteBoundaries) +{ + CtfMetadataTopology shared{ + {modelClock(42U, "shared_clock", 42U, 240000000U)}, + {modelStream(8U, 1U, 42U), modelStream(99U, 111U, 42U)}, + {}, + }; + EXPECT_NO_THROW((void)CtfMetadataModel(CtfTestSupport::testUuid(), std::move(shared))); + + for (const auto traceBusId : {std::uint8_t{1U}, std::uint8_t{111U}}) { + CtfMetadataTopology boundary{ + {modelClock(1U, "boundary_clock", 1U)}, + {modelStream(3U, traceBusId, 1U)}, + {}, + }; + EXPECT_NO_THROW((void)CtfMetadataModel(CtfTestSupport::testUuid(), std::move(boundary))); + } + + CtfMetadataTopology unformatted{ + {modelClock(4U, "unformatted_clock", 4U)}, + {{CtfStreamClassId{0U}, + {TraceRouteId{88U}, std::nullopt}, + CtfSourceKind::Itm, + std::nullopt, + CtfClockDomainId{4U}}}, + {}, + }; + CtfMetadataModel unformattedModel(CtfTestSupport::testUuid(), std::move(unformatted)); + EXPECT_FALSE(unformattedModel.isLegacySingleStreamLayout()); + + CtfMetadataModel legacy(CtfTestSupport::testUuid(), + CtfTestSupport::legacyTopology(1000000U, {TraceRouteId{88U}, std::nullopt})); + EXPECT_TRUE(legacy.isLegacySingleStreamLayout()); + + CtfMetadataModel empty(CtfTestSupport::testUuid(), {}); + EXPECT_TRUE(empty.topology().clockDomains.empty()); + EXPECT_FALSE(empty.isLegacySingleStreamLayout()); +} + +TEST(CtraceUnitTests, testCtfMetadataModelRejectsInvalidClockDomains) +{ + const auto expectRejected = [](CtfMetadataTopology topology, const std::string& message) { + EXPECT_TRUE(throwsWithMessage( + [&] { (void)CtfMetadataModel(CtfTestSupport::testUuid(), std::move(topology)); }, message)); + }; + + auto duplicateId = independentModelTopology(); + duplicateId.clockDomains.push_back(modelClock(2U, "other_clock", 3U)); + expectRejected(std::move(duplicateId), "duplicate clock-domain ID"); + + auto duplicateName = independentModelTopology(); + duplicateName.clockDomains.push_back(modelClock(3U, "clock_two", 3U)); + expectRejected(std::move(duplicateName), "unique valid clock-domain names"); + + auto invalidName = independentModelTopology(); + invalidName.clockDomains.front().name = "9-invalid"; + expectRejected(std::move(invalidName), "unique valid clock-domain names"); + + auto zeroFrequency = independentModelTopology(); + zeroFrequency.clockDomains.front().frequencyHz = 0U; + expectRejected(std::move(zeroFrequency), "non-zero clock-domain frequency"); + + auto traceUuidCollision = independentModelTopology(); + traceUuidCollision.clockDomains.front().uuid = CtfTestSupport::testUuid(); + expectRejected(std::move(traceUuidCollision), "distinct from the trace"); + + auto clockUuidCollision = independentModelTopology(); + clockUuidCollision.clockDomains.front().uuid = clockUuidCollision.clockDomains.back().uuid; + expectRejected(std::move(clockUuidCollision), "distinct from the trace"); + + auto missingClockUuid = independentModelTopology(); + missingClockUuid.clockDomains.front().uuid.reset(); + expectRejected(std::move(missingClockUuid), "require an explicit UUID"); + + auto unknownClock = independentModelTopology(); + unknownClock.streams.front().clockDomainId = CtfClockDomainId{77U}; + expectRejected(std::move(unknownClock), "unknown clock domain"); + + auto orphanClock = independentModelTopology(); + orphanClock.clockDomains.push_back(modelClock(77U, "orphan_clock", 77U)); + expectRejected(std::move(orphanClock), "without a referencing stream class"); +} + +TEST(CtraceUnitTests, testCtfMetadataModelRejectsInvalidStreamAndRouteIdentities) +{ + const auto expectRejected = [](CtfMetadataTopology topology, const std::string& message) { + EXPECT_TRUE(throwsWithMessage( + [&] { (void)CtfMetadataModel(CtfTestSupport::testUuid(), std::move(topology)); }, message)); + }; + + for (const auto invalid : {std::uint8_t{0U}, std::uint8_t{112U}, std::uint8_t{127U}}) { + auto topology = independentModelTopology(); + topology.streams.front().route.traceBusId = invalid; + topology.streams.front().streamClassId = CtfStreamClassId{invalid}; + expectRejected(std::move(topology), "ATB trace ID between 1 and 111"); + } + + auto classMismatch = independentModelTopology(); + classMismatch.streams.front().streamClassId = CtfStreamClassId{17U}; + expectRejected(std::move(classMismatch), "does not match its normalized route identity"); + + auto duplicateClass = independentModelTopology(); + duplicateClass.streams.back().route.traceBusId = duplicateClass.streams.front().route.traceBusId; + duplicateClass.streams.back().streamClassId = duplicateClass.streams.front().streamClassId; + expectRejected(std::move(duplicateClass), "duplicate stream-class ID"); + + auto duplicateRoute = independentModelTopology(); + duplicateRoute.streams.back().route = duplicateRoute.streams.front().route; + duplicateRoute.streams.back().streamClassId = duplicateRoute.streams.front().streamClassId; + expectRejected(std::move(duplicateRoute), "duplicate normalized route"); + + auto inconsistentRoute = independentModelTopology(); + inconsistentRoute.streams.back().route.id = inconsistentRoute.streams.front().route.id; + expectRejected(std::move(inconsistentRoute), "inconsistent normalized route identities"); +} + +TEST(CtraceUnitTests, testCtfMetadataModelKeysSourcesByExactRouteTypeAndNumber) +{ + EXPECT_NO_THROW((void)CtfMetadataModel(CtfTestSupport::testUuid(), independentModelTopology())); + + const auto expectRejected = [](CtfMetadataTopology topology, const std::string& message) { + EXPECT_TRUE(throwsWithMessage( + [&] { (void)CtfMetadataModel(CtfTestSupport::testUuid(), std::move(topology)); }, message)); + }; + + auto duplicate = independentModelTopology(); + duplicate.sources.push_back(duplicate.sources.front()); + expectRejected(std::move(duplicate), "duplicate source metadata"); + + auto conflicting = independentModelTopology(); + auto conflict = conflicting.sources.front(); + conflict.label = "conflicting label"; + conflicting.sources.push_back(std::move(conflict)); + expectRejected(std::move(conflicting), "conflicting source metadata for one route"); + + auto unknownRoute = independentModelTopology(); + unknownRoute.sources.front().route = {TraceRouteId{999U}, 1U}; + expectRejected(std::move(unknownRoute), "unknown normalized route"); + + auto unsupportedType = independentModelTopology(); + unsupportedType.sources.front().type = "future"; + expectRejected(std::move(unsupportedType), "type must be 'itm' or 'dwt'"); + + for (const auto invalidChannel : {0U, 32U}) { + auto invalidItm = independentModelTopology(); + const auto itm = std::find_if(invalidItm.sources.begin(), invalidItm.sources.end(), + [](const CtfSourceDescriptor& source) { return source.type == "itm"; }); + itm->source = invalidChannel; + expectRejected(std::move(invalidItm), "channel between 1 and 31"); + } + + auto invalidComparator = independentModelTopology(); + invalidComparator.sources.front().source = 4U; + expectRejected(std::move(invalidComparator), "comparator between 0 and 3"); + + for (const auto& invalidMetadata : + {std::pair{"future", 4U}, std::pair{"unsigned", 3U}, + std::pair{"float", 2U}}) { + auto invalidValue = independentModelTopology(); + invalidValue.sources.front().dataType = invalidMetadata.first; + invalidValue.sources.front().dataSize = invalidMetadata.second; + expectRejected(std::move(invalidValue), "invalid data-type/size combination"); + } + + auto overflowingAddress = independentModelTopology(); + overflowingAddress.sources.front().address = std::numeric_limits::max(); + overflowingAddress.sources.front().dataSize = 4U; + expectRejected(std::move(overflowingAddress), "address range exceeds"); + + auto boundaryAddress = independentModelTopology(); + boundaryAddress.sources.front().address = std::numeric_limits::max() - 3U; + boundaryAddress.sources.front().dataSize = 4U; + EXPECT_NO_THROW((void)CtfMetadataModel(CtfTestSupport::testUuid(), std::move(boundaryAddress))); + + auto irrelevantItmFields = independentModelTopology(); + const auto itm = std::find_if(irrelevantItmFields.sources.begin(), irrelevantItmFields.sources.end(), + [](const CtfSourceDescriptor& source) { return source.type == "itm"; }); + itm->dataType = "ignored"; + itm->dataSize = 0U; + itm->address = std::numeric_limits::max(); + EXPECT_NO_THROW((void)CtfMetadataModel(CtfTestSupport::testUuid(), std::move(irrelevantItmFields))); +} + +TEST(CtraceUnitTests, testCtfUuidFormattingAndGeneration) +{ + EXPECT_EQ(CtfTestSupport::testUuid(0xabU).toString(), "10ab2233-4455-4677-8899-aabbccddeeff"); + const auto generated = CtfUuid::randomV4(); + EXPECT_EQ(generated.bytes()[6U] & 0xf0U, 0x40U); + EXPECT_EQ(generated.bytes()[8U] & 0xc0U, 0x80U); + EXPECT_EQ(generated.toString().size(), 36U); +} diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp index dc68421aa..89898b5ae 100644 --- a/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp +++ b/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp @@ -5,6 +5,7 @@ * Generated with AI */ +#include "CtfTestSupport.h" #include "TestPath.h" #include "TestPlatform.h" #include "TestSupport.h" @@ -30,20 +31,22 @@ TEST(CtraceUnitTests, testCtfMetadataWriterEscapesAndDeduplicatesSourceLabels) { const TemporaryTestPath path("ctrace-metadata-writer"); path.createDirectory(); - const TraceRouteIdentity route{TraceRouteId{0U}, 1U}; - const std::vector sources{ + const TraceRouteIdentity route{}; + const std::vector sources{ {"itm", 1U, route, std::string("ITM3"), std::nullopt, "unsigned", 4U}, {"itm", 2U, route, std::string("ITM3_1"), std::nullopt, "unsigned", 4U}, {"itm", 3U, route, std::string("ITM3"), std::nullopt, "unsigned", 4U}, {"itm", 4U, route, std::string("line\rbreak"), std::nullopt, "unsigned", 4U}, {"itm", 5U, route, std::nullopt, std::nullopt, "unsigned", 4U}, {"itm", 6U, route, std::string("ITM3"), std::nullopt, "unsigned", 4U}, - {"future", 7U, route, std::string("ignored"), std::nullopt, "unsigned", 4U}, - {"dwt", 0U, route, std::nullopt, std::numeric_limits::max(), "unsigned", 4U}, + {"dwt", 0U, route, std::nullopt, std::numeric_limits::max() - 3U, "unsigned", 4U}, }; - CtfMetadataWriter::write(path.path(), "00000000-0000-4000-8000-000000000000", 1000000U, sources, - {8U, 10U, 13U, 16U, 54U}); + CtfMetadataModel model(CtfTestSupport::testUuid(), CtfTestSupport::legacyTopology(1000000U, route, sources)); + for (const auto number : {8U, 10U, 13U, 16U, 54U}) { + model.observeException(CtfStreamClassId{0U}, number); + } + CtfMetadataWriter::write(path.path(), model); const auto metadata = readTestTextFile(path.path() / "metadata"); EXPECT_NE(metadata.find("ITM3_2"), std::string::npos); EXPECT_NE(metadata.find("\"ITM6\" = 6"), std::string::npos); @@ -65,12 +68,69 @@ TEST(CtraceUnitTests, testCtfMetadataWriterEscapesAndDeduplicatesSourceLabels) EXPECT_NE(metadata.find("variant "), std::string::npos); EXPECT_NE(metadata.find("variant "), std::string::npos); EXPECT_NE(metadata.find("uint32_t u32;"), std::string::npos); + EXPECT_NE(metadata.find("cmsis_dwt0_address_end = \"0xFFFFFFFFFFFFFFFF\""), std::string::npos); } TEST(CtraceUnitTests, testCtfMetadataWriterRejectsMissingOutputDirectory) { const TemporaryTestPath path("ctrace-metadata-writer-missing"); - EXPECT_THROW(CtfMetadataWriter::write(path.path(), "uuid", 1U, {}, {}), std::runtime_error); + const CtfMetadataModel model(CtfTestSupport::testUuid(), CtfTestSupport::legacyTopology(1U)); + EXPECT_THROW(CtfMetadataWriter::write(path.path(), model), std::runtime_error); +} + +TEST(CtraceUnitTests, testCtfMetadataWriterSerializesRouteScopedMultiStreamTopology) +{ + const TemporaryTestPath path("ctrace-multistream-metadata-writer"); + path.createDirectory(); + const TraceRouteIdentity first{TraceRouteId{8U}, 1U}; + const TraceRouteIdentity second{TraceRouteId{91U}, 111U}; + CtfMetadataTopology topology{ + { + {CtfClockDomainId{19U}, "clock_nineteen", CtfTestSupport::testUuid(19U), 240000000U, false}, + {CtfClockDomainId{3U}, "clock_three", CtfTestSupport::testUuid(3U), 240000000U, false}, + }, + { + {CtfStreamClassId{111U}, second, CtfSourceKind::Itm, std::string("second"), CtfClockDomainId{19U}}, + {CtfStreamClassId{1U}, first, CtfSourceKind::Itm, std::string("first"), CtfClockDomainId{3U}}, + }, + { + {"dwt", 0U, first, std::string("First DWT"), 0x1000U, "unsigned", 4U}, + {"dwt", 0U, second, std::string("Second DWT"), 0x2000U, "signed", 2U}, + {"itm", 1U, first, std::string("First console"), std::nullopt, "unsigned", 4U}, + {"itm", 1U, second, std::string("Second console"), std::nullopt, "unsigned", 4U}, + }, + }; + CtfMetadataModel model(CtfTestSupport::testUuid(), std::move(topology)); + model.observeException(CtfStreamClassId{1U}, 54U); + model.observeException(CtfStreamClassId{111U}, 75U); + CtfMetadataWriter::write(path.path(), model); + + const auto metadata = readTestTextFile(path.path() / "metadata"); + EXPECT_NE(metadata.find("uuid = \"" + CtfTestSupport::testUuid().toString() + "\";"), std::string::npos); + EXPECT_NE(metadata.find("name = clock_three;"), std::string::npos); + EXPECT_NE(metadata.find("uuid = \"" + CtfTestSupport::testUuid(3U).toString() + "\";"), std::string::npos); + EXPECT_NE(metadata.find("name = clock_nineteen;"), std::string::npos); + EXPECT_NE(metadata.find("uuid = \"" + CtfTestSupport::testUuid(19U).toString() + "\";"), std::string::npos); + EXPECT_NE(metadata.find("map = clock.clock_three.value; } := clock_three_t;"), std::string::npos); + EXPECT_NE(metadata.find("map = clock.clock_nineteen.value; } := clock_nineteen_t;"), std::string::npos); + EXPECT_NE(metadata.find("stream {\n id = 1;"), std::string::npos); + EXPECT_NE(metadata.find("stream {\n id = 111;"), std::string::npos); + EXPECT_NE(metadata.find("stream_id = 1;"), std::string::npos); + EXPECT_NE(metadata.find("stream_id = 111;"), std::string::npos); + EXPECT_NE(metadata.find("cmsis_stream_1_dwt0_value_type = \"unsigned\";"), std::string::npos); + EXPECT_NE(metadata.find("cmsis_stream_111_dwt0_value_type = \"signed\";"), std::string::npos); + EXPECT_NE(metadata.find("cmsis_stream_1_dwt0_address_start = \"0x1000\";"), std::string::npos); + EXPECT_NE(metadata.find("cmsis_stream_111_dwt0_address_end = \"0x2001\";"), std::string::npos); + EXPECT_NE(metadata.find("\"First DWT\" = 0"), std::string::npos); + EXPECT_NE(metadata.find("\"Second DWT\" = 0"), std::string::npos); + EXPECT_NE(metadata.find("\"First console\" = 1"), std::string::npos); + EXPECT_NE(metadata.find("\"Second console\" = 1"), std::string::npos); + EXPECT_NE(metadata.find("\"External IRQ 38\" = 54"), std::string::npos); + EXPECT_NE(metadata.find("\"External IRQ 59\" = 75"), std::string::npos); + EXPECT_EQ(metadata.find("\n cmsis_dwt0_value_type"), std::string::npos); + EXPECT_EQ(metadata.find("name = swo_clock;"), std::string::npos); + EXPECT_EQ(metadata.find("stream_id = 0;"), std::string::npos); + EXPECT_FALSE(std::filesystem::exists(path.path() / "stream_0")); } TEST(CtraceUnitTests, testTraceCompassXmlWriterRejectsDirectoryTarget) @@ -177,6 +237,7 @@ TEST(CtraceUnitTests, testCtfTextWritersReportDeviceWriteFailures) const TemporaryTestPath path("ctrace-metadata-device-failure"); path.createDirectory(); std::filesystem::create_symlink(TestPlatform::writeFailurePath(), path.path() / "metadata"); - EXPECT_THROW(CtfMetadataWriter::write(path.path(), "uuid", 1U, {}, {}), std::runtime_error); + const CtfMetadataModel model(CtfTestSupport::testUuid(), CtfTestSupport::legacyTopology(1U)); + EXPECT_THROW(CtfMetadataWriter::write(path.path(), model), std::runtime_error); EXPECT_THROW(TraceCompassXmlWriter::writeFile(TestPlatform::writeFailurePath()), std::runtime_error); } diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfStreamWriterTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfStreamWriterTests.cpp index 5eddc528d..ee709c447 100644 --- a/tools/ctrace/test/unit/src/output/ctf/CtfStreamWriterTests.cpp +++ b/tools/ctrace/test/unit/src/output/ctf/CtfStreamWriterTests.cpp @@ -14,6 +14,7 @@ #include "ctf/CtfSchema.h" #include "ctf/CtfStreamWriter.h" +#include #include #include @@ -24,8 +25,7 @@ TEST(CtraceUnitTests, testCtfStreamWriterHandlesInactiveAndEmptyStreams) EXPECT_NO_THROW(writer.writeRecord(1U, 1U, 1U, 0U, [](CtfStreamWriter::Record&) {})); const TemporaryTestPath path("ctrace-empty-stream"); - writer.open(path.path(), 7U); - EXPECT_FALSE(writer.uuidString().empty()); + writer.open(path.path(), CtfStreamClassId{7U}, CtfTestSupport::testUuid()); EXPECT_NO_THROW(writer.close()); } @@ -33,7 +33,7 @@ TEST(CtraceUnitTests, testCtfStreamWriterValidatesDeclaredPayloadSize) { const TemporaryTestPath path("ctrace-invalid-record-stream"); CtfStreamWriter writer; - writer.open(path.path(), 7U); + writer.open(path.path(), CtfStreamClassId{7U}, CtfTestSupport::testUuid()); EXPECT_THROW(writer.writeRecord(1U, 1U, 1U, 65536U, [](CtfStreamWriter::Record&) {}), std::invalid_argument); EXPECT_THROW(writer.writeRecord(1U, 1U, 1U, 1U, [](CtfStreamWriter::Record&) {}), std::logic_error); @@ -46,7 +46,8 @@ TEST(CtraceUnitTests, testCtfStreamWriterHoldsRegressingTimestamps) { const TemporaryTestPath path("ctrace-monotonic-stream"); CtfStreamWriter writer; - writer.open(path.path(), 7U); + const auto traceUuid = CtfTestSupport::testUuid(7U); + writer.open(path.path(), CtfStreamClassId{7U}, traceUuid); const auto eventId = CtfSchema::value(CtfSchema::EventId::TraceStatus); const auto writePayload = [](CtfStreamWriter::Record& record) { record.writeU8(CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError)); @@ -60,6 +61,9 @@ TEST(CtraceUnitTests, testCtfStreamWriterHoldsRegressingTimestamps) ASSERT_EQ(records.size(), 2U); EXPECT_EQ(records[0].timestamp, 100U); EXPECT_EQ(records[1].timestamp, 100U); + const auto bytes = readTestBinaryFile(path.path()); + EXPECT_TRUE(std::equal(traceUuid.bytes().begin(), traceUuid.bytes().end(), bytes.begin() + 4U)); + EXPECT_EQ(CtfTestSupport::readLe32(bytes, 20U), 7U); } TEST(CtraceUnitTests, testCtfStreamWriterReportsDeviceWriteFailures) @@ -68,7 +72,7 @@ TEST(CtraceUnitTests, testCtfStreamWriterReportsDeviceWriteFailures) GTEST_SKIP(); } CtfStreamWriter writer; - writer.open(TestPlatform::writeFailurePath(), 7U); + writer.open(TestPlatform::writeFailurePath(), CtfStreamClassId{7U}, CtfTestSupport::testUuid()); writer.writeRecord(1U, 1U, 1U, 1U, [](CtfStreamWriter::Record& record) { record.writeU8(1U); }); EXPECT_THROW(writer.close(), std::runtime_error); } diff --git a/tools/ctrace/test/unit/support/CtfTestSupport.h b/tools/ctrace/test/unit/support/CtfTestSupport.h index 8443b9f5a..3636d21b8 100644 --- a/tools/ctrace/test/unit/support/CtfTestSupport.h +++ b/tools/ctrace/test/unit/support/CtfTestSupport.h @@ -10,7 +10,9 @@ #include "TestSupport.h" #include "TraceEvent.h" +#include "ctf/CtfMetadataModel.h" #include "ctf/CtfSchema.h" +#include "ctf/CtfUuid.h" #include #include @@ -19,10 +21,29 @@ #include #include #include +#include #include namespace CtfTestSupport { +/** @brief Returns a deterministic RFC 4122 version-4 UUID for CTF tests. */ +inline CtfUuid testUuid(std::uint8_t discriminator = 0U) +{ + return CtfUuid{{0x10U, discriminator, 0x22U, 0x33U, 0x44U, 0x55U, 0x46U, 0x77U, 0x88U, 0x99U, 0xaaU, 0xbbU, 0xccU, + 0xddU, 0xeeU, 0xffU}}; +} + +/** @brief Creates the explicit metadata topology used by the legacy SINGLE runtime. */ +inline CtfMetadataTopology legacyTopology(std::uint64_t clockHz, TraceRouteIdentity route = {}, + std::vector sources = {}) +{ + return { + {{CtfClockDomainId{0U}, "swo_clock", std::nullopt, clockHz, false}}, + {{CtfStreamClassId{0U}, route, CtfSourceKind::Itm, std::nullopt, CtfClockDomainId{0U}}}, + std::move(sources), + }; +} + inline constexpr std::size_t kCtfPacketHeaderSize = 24U; inline constexpr std::size_t kCtfPacketContextSize = 32U; inline constexpr std::size_t kCtfEventHeaderSize = 13U; From 295df49b100fca6b72cfe2c17b10acb8f0a36631 Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Thu, 10 Sep 2026 10:40:04 +0200 Subject: [PATCH 06/31] feat(ctrace): emit multi-stream CTF bundles --- .../ctrace/docs/multicore-multisource-plan.md | 8 +- .../ctrace/src/output/ctf/CtfBundleOutput.cpp | 45 +- tools/ctrace/src/output/ctf/CtfBundleOutput.h | 5 +- tools/ctrace/src/output/ctf/CtfEncoder.cpp | 290 +++++++++---- tools/ctrace/src/output/ctf/CtfEncoder.h | 16 +- .../src/output/ctf/TraceCompassXmlWriter.cpp | 47 ++- .../src/output/ctf/TraceCompassXmlWriter.h | 8 +- .../src/output/TraceOutputLifecycleTests.cpp | 75 ++++ .../src/output/ctf/CtfBundleOutputTests.cpp | 290 ++++++++++++- .../unit/src/output/ctf/CtfEncoderTests.cpp | 393 ++++++++++++++++-- 10 files changed, 1020 insertions(+), 157 deletions(-) diff --git a/tools/ctrace/docs/multicore-multisource-plan.md b/tools/ctrace/docs/multicore-multisource-plan.md index 367d0e351..9e83d2d8d 100644 --- a/tools/ctrace/docs/multicore-multisource-plan.md +++ b/tools/ctrace/docs/multicore-multisource-plan.md @@ -540,8 +540,8 @@ Phase 0 -> Phase 1 -> Phase 2 -> Phase 3 -> Phase 4 | 2 | Raw-input discovery and preflight | Complete | | 3 | Route-aware semantic state, diagnostics, and CSV | Complete | | 4 | CTF descriptors and metadata model | Complete | -| 5 | Multi-stream CTF bundle and Trace Compass policy | Next | -| 6 | DecodeTree `SINGLE` migration | Pending | +| 5 | Multi-stream CTF bundle and Trace Compass policy | Complete | +| 6 | DecodeTree `SINGLE` migration | Next | | 7 | Clean formatted decoding and TB integration | Pending | | 8 | Route-local recovery and error isolation | Pending | | 9 | Consumer validation, documentation, and final hardening | Pending | @@ -1009,7 +1009,9 @@ an Error and does not start. Processor labels are omitted or use the existing ge exactly once and before the event that caused creation. A first overflow, synchronization, or issue-only event follows the same rule; a route whose events are all filtered remains artifact-free. Apply the same `traceEventSelectedForOutput` predicate as CSV before lazy creation; do not duplicate or weaken the - type/stream filter in CTF-specific code. + type/stream filter in CTF-specific code. Synchronization is the narrow compatibility exception: it has no public + CSV row or `--type` value, but an otherwise unfiltered CTF selection may encode it as `trace_start`/`resync` + control context and therefore activate the route. Any explicit type filter keeps a sync-only route artifact-free. 5. Allocate the trace UUID once at bundle level and pass the same UUID to every writer and to the metadata model. Every packet header UUID must equal this metadata trace UUID. `CtfStreamWriter::open` must no longer generate an independent UUID for each file. Clock UUIDs identify time domains and are never reused as the trace UUID. diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp index f0963ca46..006e91575 100644 --- a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp @@ -9,12 +9,15 @@ #include "CtfEncoder.h" #include "CtfUuid.h" +#include "DiagnosticSink.h" #include "TraceCompassXmlWriter.h" #include "TraceEvent.h" #include "TraceOutputConfig.h" #include +#include #include +#include #include #include #include @@ -174,7 +177,8 @@ CtfBundleOutput::CtfBundleOutput(CtfOutputConfig config, DiagnosticSink* diagnos diagnostics, std::move(config.routes), !config.routeCatalogueConfigured, - }) + }), + m_diagnostics(diagnostics) { validateOutputTargets(m_ctfOutputDirectory, m_traceCompassXmlPath); } @@ -209,7 +213,6 @@ void CtfBundleOutput::start() try { m_traceUuid = CtfUuid::randomV4(); m_encoder.start(m_ctfOutputDirectory, m_traceUuid); - TraceCompassXmlWriter::writeFile(m_traceCompassXmlPath); } catch (...) { abort(); throw; @@ -223,6 +226,37 @@ void CtfBundleOutput::stop() } try { m_encoder.stop(); + const auto* metadata = m_encoder.completedMetadata(); + // A successful encoder stop always publishes its completed metadata model. + assert(metadata != nullptr); + + const auto& streams = metadata->topology().streams; + if (streams.empty()) { + removeOutputFile(m_traceCompassXmlPath); + } else { + std::set clocks; + for (const auto& stream : streams) { + clocks.insert(stream.clockDomainId); + } + if (clocks.size() == 1U) { + const auto layout = metadata->isLegacySingleStreamLayout() ? TraceCompassXmlWriter::PathLayout::Legacy + : TraceCompassXmlWriter::PathLayout::RoutePrefixed; + TraceCompassXmlWriter::writeFile(m_traceCompassXmlPath, layout); + } else { + removeOutputFile(m_traceCompassXmlPath); + if (m_diagnostics != nullptr) { + m_diagnostics->report({ + DiagnosticSink::Severity::Warning, + "Trace Compass XML was not generated because emitted CTF streams use multiple clock domains", + { + {"backend", "ctf"}, + {"path", m_traceCompassXmlPath.string()}, + {"clockDomains", std::to_string(clocks.size())}, + }, + }); + } + } + } m_active = false; } catch (...) { abort(); @@ -241,5 +275,10 @@ void CtfBundleOutput::abort() void CtfBundleOutput::writeEvent(const TraceEvent& event) { - m_encoder.writeEvent(event); + try { + m_encoder.writeEvent(event); + } catch (...) { + abort(); + throw; + } } diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.h b/tools/ctrace/src/output/ctf/CtfBundleOutput.h index 3453af35b..9e75ef476 100644 --- a/tools/ctrace/src/output/ctf/CtfBundleOutput.h +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.h @@ -30,9 +30,9 @@ class CtfBundleOutput final : public TraceOutput { /** @brief Aborts an active bundle before destruction. */ ~CtfBundleOutput() override; - /** @brief Prepares empty CTF and XML targets. */ + /** @brief Prepares an empty CTF target and removes stale companion XML. */ void start() override; - /** @brief Completes metadata, stream, and XML output. */ + /** @brief Completes metadata and streams, then writes XML when their clocks permit it. */ void stop() override; /** @brief Removes incomplete CTF and XML targets. */ void abort() override; @@ -50,6 +50,7 @@ class CtfBundleOutput final : public TraceOutput { std::filesystem::path m_ctfOutputDirectory; std::filesystem::path m_traceCompassXmlPath; CtfEncoder m_encoder; + DiagnosticSink* m_diagnostics = nullptr; CtfUuid m_traceUuid; bool m_active = false; }; diff --git a/tools/ctrace/src/output/ctf/CtfEncoder.cpp b/tools/ctrace/src/output/ctf/CtfEncoder.cpp index 4b3a26dc1..0268ef9e0 100644 --- a/tools/ctrace/src/output/ctf/CtfEncoder.cpp +++ b/tools/ctrace/src/output/ctf/CtfEncoder.cpp @@ -18,6 +18,7 @@ #include "TraceSelection.h" #include +#include #include #include #include @@ -143,16 +144,16 @@ void CtfEncoder::start(const std::filesystem::path& outputDirectory, const CtfUu m_outputDirectory = outputDirectory; try { m_metadata.emplace(traceUuid, m_config.metadata); - if (!m_metadata->isLegacySingleStreamLayout()) { - throw std::runtime_error("CTF binary output currently requires exactly one legacy SINGLE stream topology"); + if (m_metadata->topology().streams.empty() && m_config.legacyRouteFallback) { + throw std::runtime_error("CTF encoder requires an explicit metadata topology or normalized route catalogue"); } + m_completedMetadata.reset(); + m_streams.clear(); m_bootstrappedRoutes.clear(); m_streamStates.clear(); + m_emittedExceptionNumbers.clear(); m_reportedDwtSizeMismatches.clear(); m_exceptionLanes.clear(); - const auto& runtimeStream = m_metadata->topology().streams.front(); - m_stream.open(m_outputDirectory / "stream_0", runtimeStream.streamClassId, traceUuid); - m_recording = true; std::map initialRoutes; const auto addInitialRoute = [&](const TraceRouteIdentity& route) { const auto [found, inserted] = initialRoutes.emplace(route.id, route); @@ -175,7 +176,10 @@ void CtfEncoder::start(const std::filesystem::path& outputDirectory, const CtfUu addInitialRoute(source.route); } } - for (const auto& stream : m_metadata->topology().streams) { + m_recording = true; + if (m_metadata->isLegacySingleStreamLayout()) { + const auto& stream = m_metadata->topology().streams.front(); + (void)ensureStreamWriter(stream); if (m_config.selection.includesRoute(stream.route)) { bootstrapRoute(stream.route); } @@ -192,15 +196,28 @@ void CtfEncoder::stop() return; } m_recording = false; - m_stream.close(); + for (auto& [streamClassId, stream] : m_streams) { + (void)streamClassId; + stream.close(); + } writeMetadataFile(); } void CtfEncoder::abort() noexcept { m_recording = false; - m_stream.abort(); + for (auto& [streamClassId, stream] : m_streams) { + (void)streamClassId; + stream.abort(); + } + m_streams.clear(); m_metadata.reset(); + m_completedMetadata.reset(); + m_bootstrappedRoutes.clear(); + m_streamStates.clear(); + m_emittedExceptionNumbers.clear(); + m_reportedDwtSizeMismatches.clear(); + m_exceptionLanes.clear(); m_outputDirectory.clear(); } @@ -214,11 +231,15 @@ void CtfEncoder::writeEvent(const TraceEvent& event) } validateConfiguredRoute(m_config, event.route); const auto* stream = m_metadata->streamForRoute(event.route); - if (stream == nullptr || stream->streamClassId != m_metadata->topology().streams.front().streamClassId) { + if (stream == nullptr) { throw std::runtime_error( "CTF binary output cannot encode an event route without an exact runtime stream descriptor"); } - bootstrapRoute(event.route); + const auto selected = traceEventSelectedForOutput(event, m_config.selection); + if (activatesStream(event, selected)) { + (void)ensureStreamWriter(*stream); + bootstrapRoute(event.route); + } if (!isTraceEvent(event) && event.tcyc.has_value()) { auto& eventTimestamp = streamState(event.route).eventTimestamp; eventTimestamp = std::max(eventTimestamp, *event.tcyc); @@ -227,7 +248,6 @@ void CtfEncoder::writeEvent(const TraceEvent& event) } } - const auto selected = traceEventSelectedForOutput(event, m_config.selection); if (const auto* software = traceEventPayload(event)) { if (selected) { writeSoftwareEvent(event, *software); @@ -291,6 +311,11 @@ void CtfEncoder::writeEvent(const TraceEvent& event) } } +const CtfMetadataModel* CtfEncoder::completedMetadata() const noexcept +{ + return m_completedMetadata ? &*m_completedMetadata : nullptr; +} + void CtfEncoder::writePcSampleEvent(const TraceEvent& event, const PcSampleTraceEvent& sample) { const auto pcSize = sample.sleeping ? 0U : 4U; @@ -300,21 +325,21 @@ void CtfEncoder::writePcSampleEvent(const TraceEvent& event, const PcSampleTrace const auto quality = computeSampleQuality(event); const auto state = CtfSchema::value(sample.sleeping ? CtfSchema::PcSampleState::Sleep : CtfSchema::PcSampleState::Pc); - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::PcSample), eventTimestamp, traceBusId, payloadSize, - [&](CtfStreamWriter::Record& record) { - record.writeU8(state); - if (!sample.sleeping) { - record.writeU32(sample.pc); - } - record.writeU8(quality.first); - record.writeU32(quality.second); - }); + streamWriter(event.route) + .writeRecord(CtfSchema::value(CtfSchema::EventId::PcSample), eventTimestamp, traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(state); + if (!sample.sleeping) { + record.writeU32(sample.pc); + } + record.writeU8(quality.first); + record.writeU32(quality.second); + }); } std::uint64_t CtfEncoder::allocateEventTimestamp(const TraceRouteIdentity& route) { - // CtfStreamWriter applies the final monotonic clamp across the multiplexed - // CTF stream. This value remains local to the normalized route. + // Each route-specific CtfStreamWriter applies its final monotonic clamp. return streamState(route).eventTimestamp; } @@ -323,8 +348,56 @@ CtfEncoder::StreamState& CtfEncoder::streamState(const TraceRouteIdentity& route return m_streamStates[route.id]; } +const CtfStreamDescriptor& CtfEncoder::streamDescriptor(const TraceRouteIdentity& route) const +{ + const auto* stream = m_metadata->streamForRoute(route); + // Public event handling validates the route before any private emission path reaches this lookup. + assert(stream != nullptr); + return *stream; +} + +CtfStreamWriter& CtfEncoder::ensureStreamWriter(const CtfStreamDescriptor& stream) +{ + const auto [writer, inserted] = m_streams.try_emplace(stream.streamClassId); + if (inserted) { + try { + writer->second.open(m_outputDirectory / ("stream_" + std::to_string(stream.streamClassId.value())), + stream.streamClassId, m_metadata->traceUuid()); + } catch (...) { + m_streams.erase(writer); + throw; + } + } + return writer->second; +} + +CtfStreamWriter& CtfEncoder::streamWriter(const TraceRouteIdentity& route) +{ + const auto& stream = streamDescriptor(route); + return m_streams.at(stream.streamClassId); +} + +bool CtfEncoder::activatesStream(const TraceEvent& event, bool selected) const +{ + if (const auto* exception = traceEventPayload(event)) { + return selected && exception->action != ExceptionAction::Unknown; + } + if (const auto* counters = traceEventPayload(event)) { + return selected && std::any_of(kDwtEventCounters.begin(), kDwtEventCounters.end(), [&](const auto counter) { + return (counters->counterMask & dwtEventCounterBit(counter)) != 0U; + }); + } + if (const auto* counters = traceEventPayload(event)) { + return selected && std::any_of(kPmuEventCounters.begin(), kPmuEventCounters.end(), [&](const auto counter) { + return (counters->overflowMask & pmuEventCounterBit(counter)) != 0U; + }); + } + return selected || (isTraceEvent(event) && m_config.selection.types.empty()); +} + void CtfEncoder::bootstrapRoute(const TraceRouteIdentity& route) { + (void)ensureStreamWriter(streamDescriptor(route)); (void)streamState(route); if (!m_bootstrappedRoutes.insert(route.id).second) { return; @@ -344,14 +417,15 @@ void CtfEncoder::writeSoftwareEvent(const TraceEvent& event, const SoftwareTrace const auto eventTimestamp = allocateEventTimestamp(event.route); const auto traceBusId = legacyCtfTraceBusId(event.route); const auto payloadSize = 1U + 1U + variant->byteSize + 1U + 4U; - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::Itm), eventTimestamp, traceBusId, payloadSize, - [&](CtfStreamWriter::Record& record) { - record.writeU8(static_cast(software.channel & 0xffU)); - record.writeU8(CtfSchema::value(variant->tag)); - writeVariantValue(record, software.value, software.size, *variant); - record.writeU8(quality.first); - record.writeU32(quality.second); - }); + streamWriter(event.route) + .writeRecord(CtfSchema::value(CtfSchema::EventId::Itm), eventTimestamp, traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(static_cast(software.channel & 0xffU)); + record.writeU8(CtfSchema::value(variant->tag)); + writeVariantValue(record, software.value, software.size, *variant); + record.writeU8(quality.first); + record.writeU32(quality.second); + }); } void CtfEncoder::writeDwtValueEvent(const TraceEvent& event, const DwtDataTraceEvent& data) @@ -366,19 +440,19 @@ void CtfEncoder::writeDwtValueEvent(const TraceEvent& event, const DwtDataTraceE const auto eventTimestamp = allocateEventTimestamp(event.route); const auto traceBusId = legacyCtfTraceBusId(event.route); const auto quality = computeSampleQuality(event); - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtValue), eventTimestamp, traceBusId, payloadSize, - [&](CtfStreamWriter::Record& record) { - record.writeU8(static_cast(data.comparator & 0xffU)); - record.writeU8(CtfSchema::value(data.access == AccessType::Read - ? CtfSchema::DwtAccess::Read - : CtfSchema::DwtAccess::Write)); - record.writeU8(CtfSchema::value(variant.tag)); - writeVariantValue(record, data.value, data.size, variant); - writeDwtAddress(record, data.pc, pcVariant); - writeDwtAddress(record, data.address, addressVariant); - record.writeU8(quality.first); - record.writeU32(quality.second); - }); + streamWriter(event.route) + .writeRecord(CtfSchema::value(CtfSchema::EventId::DwtValue), eventTimestamp, traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(static_cast(data.comparator & 0xffU)); + record.writeU8(CtfSchema::value(data.access == AccessType::Read ? CtfSchema::DwtAccess::Read + : CtfSchema::DwtAccess::Write)); + record.writeU8(CtfSchema::value(variant.tag)); + writeVariantValue(record, data.value, data.size, variant); + writeDwtAddress(record, data.pc, pcVariant); + writeDwtAddress(record, data.address, addressVariant); + record.writeU8(quality.first); + record.writeU32(quality.second); + }); } void CtfEncoder::reportDwtSizeMismatch(const TraceEvent& event, const DwtDataTraceEvent& data, @@ -413,14 +487,15 @@ void CtfEncoder::writeDwtAddrEvent(const TraceEvent& event, const DwtAddressTrac const auto& pcVariant = dwtAddressVariant(pc); const auto& addressVariant = dwtAddressVariant(address); const auto payloadSize = 1U + 1U + pcVariant.byteSize + 1U + addressVariant.byteSize + 1U + 4U; - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtAddress), eventTimestamp, traceBusId, payloadSize, - [&](CtfStreamWriter::Record& record) { - record.writeU8(static_cast(data.comparator & 0xffU)); - writeDwtAddress(record, pc, pcVariant); - writeDwtAddress(record, address, addressVariant); - record.writeU8(quality.first); - record.writeU32(quality.second); - }); + streamWriter(event.route) + .writeRecord(CtfSchema::value(CtfSchema::EventId::DwtAddress), eventTimestamp, traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(static_cast(data.comparator & 0xffU)); + writeDwtAddress(record, pc, pcVariant); + writeDwtAddress(record, address, addressVariant); + record.writeU8(quality.first); + record.writeU32(quality.second); + }); } void CtfEncoder::writeDwtMatchEvent(const TraceEvent& event, const DwtMatchTraceEvent& match) @@ -429,12 +504,13 @@ void CtfEncoder::writeDwtMatchEvent(const TraceEvent& event, const DwtMatchTrace const auto eventTimestamp = allocateEventTimestamp(event.route); const auto traceBusId = legacyCtfTraceBusId(event.route); const auto quality = computeSampleQuality(event); - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtMatch), eventTimestamp, traceBusId, payloadSize, - [&](CtfStreamWriter::Record& record) { - record.writeU8(static_cast(match.comparator & 0xffU)); - record.writeU8(quality.first); - record.writeU32(quality.second); - }); + streamWriter(event.route) + .writeRecord(CtfSchema::value(CtfSchema::EventId::DwtMatch), eventTimestamp, traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(static_cast(match.comparator & 0xffU)); + record.writeU8(quality.first); + record.writeU32(quality.second); + }); } void CtfEncoder::writeDwtEvent(const TraceEvent& event, const DwtEventTraceEvent& counters) @@ -448,12 +524,13 @@ void CtfEncoder::writeDwtEvent(const TraceEvent& event, const DwtEventTraceEvent if ((counters.counterMask & counterBit) == 0U) { continue; } - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtEvent), eventTimestamp, traceBusId, payloadSize, - [&](CtfStreamWriter::Record& record) { - record.writeU8(CtfSchema::value(counter)); - record.writeU8(quality.first); - record.writeU32(quality.second); - }); + streamWriter(event.route) + .writeRecord(CtfSchema::value(CtfSchema::EventId::DwtEvent), eventTimestamp, traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(CtfSchema::value(counter)); + record.writeU8(quality.first); + record.writeU32(quality.second); + }); } } @@ -467,12 +544,13 @@ void CtfEncoder::writePmuEvent(const TraceEvent& event, const PmuTraceEvent& cou if ((counters.overflowMask & pmuEventCounterBit(counter)) == 0U) { continue; } - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::PmuEvent), eventTimestamp, traceBusId, payloadSize, - [&](CtfStreamWriter::Record& record) { - record.writeU8(CtfSchema::value(counter)); - record.writeU8(quality.first); - record.writeU32(quality.second); - }); + streamWriter(event.route) + .writeRecord(CtfSchema::value(CtfSchema::EventId::PmuEvent), eventTimestamp, traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(CtfSchema::value(counter)); + record.writeU8(quality.first); + record.writeU32(quality.second); + }); } } @@ -481,11 +559,12 @@ void CtfEncoder::writeGlobalTimestampEvent(const TraceEvent& event, const Global constexpr auto payloadSize = 8U + 1U; const auto eventTimestamp = allocateEventTimestamp(event.route); const auto traceBusId = legacyCtfTraceBusId(event.route); - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::GlobalTimestamp), eventTimestamp, traceBusId, payloadSize, - [&](CtfStreamWriter::Record& record) { - record.writeU64(timestamp.value); - record.writeU8(timestamp.clockChange ? 1U : 0U); - }); + streamWriter(event.route) + .writeRecord(CtfSchema::value(CtfSchema::EventId::GlobalTimestamp), eventTimestamp, traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU64(timestamp.value); + record.writeU8(timestamp.clockChange ? 1U : 0U); + }); } void CtfEncoder::writeTraceStatusEvent(std::uint8_t reason, const TraceRouteIdentity& route, bool emitEvent) @@ -506,11 +585,11 @@ void CtfEncoder::writeTraceStatusEvent(std::uint8_t reason, const TraceRouteIden constexpr auto payloadSize = 1U + 4U; const auto eventTimestamp = allocateEventTimestamp(route); const auto traceBusId = legacyCtfTraceBusId(route); - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::TraceStatus), eventTimestamp, traceBusId, payloadSize, - [&](CtfStreamWriter::Record& record) { - record.writeU8(reason); - record.writeU32(ctfOverflowCount(streamState(route).overflowCount)); - }); + streamWriter(route).writeRecord(CtfSchema::value(CtfSchema::EventId::TraceStatus), eventTimestamp, traceBusId, + payloadSize, [&](CtfStreamWriter::Record& record) { + record.writeU8(reason); + record.writeU32(ctfOverflowCount(streamState(route).overflowCount)); + }); } } @@ -550,13 +629,14 @@ void CtfEncoder::emitExceptionRecord(const TraceRouteIdentity& route, ExceptionN const auto encodedOrigin = CtfSchema::value(origin == CtfExceptionLaneTracker::RecordOrigin::Trace ? CtfSchema::ExceptionOrigin::Trace : CtfSchema::ExceptionOrigin::Synthetic); - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::Exception), eventTimestamp, traceBusId, payloadSize, - [&](CtfStreamWriter::Record& record) { - record.writeU16(number); - record.writeU8(encodedAction); - record.writeU16(number); - record.writeU8(encodedOrigin); - }); + streamWriter(route).writeRecord(CtfSchema::value(CtfSchema::EventId::Exception), eventTimestamp, traceBusId, + payloadSize, [&](CtfStreamWriter::Record& record) { + record.writeU16(number); + record.writeU8(encodedAction); + record.writeU16(number); + record.writeU8(encodedOrigin); + }); + m_emittedExceptionNumbers[route.id].insert(number); } CtfExceptionLaneTracker& CtfEncoder::exceptionLane(const TraceRouteIdentity& route) @@ -589,12 +669,38 @@ std::pair CtfEncoder::computeSampleQuality(const Tr void CtfEncoder::writeMetadataFile() { - const auto streamClassId = m_metadata->topology().streams.front().streamClassId; - for (const auto& [routeId, lane] : m_exceptionLanes) { - (void)routeId; - for (const auto number : lane.observedExceptionNumbers()) { - m_metadata->observeException(streamClassId, number); + CtfMetadataTopology emittedTopology; + std::set emittedClockDomains; + std::set emittedRoutes; + for (const auto& stream : m_metadata->topology().streams) { + if (m_streams.find(stream.streamClassId) == m_streams.end()) { + continue; + } + emittedTopology.streams.push_back(stream); + emittedClockDomains.insert(stream.clockDomainId); + emittedRoutes.insert(stream.route.id); + } + for (const auto& clock : m_metadata->topology().clockDomains) { + if (emittedClockDomains.find(clock.id) != emittedClockDomains.end()) { + emittedTopology.clockDomains.push_back(clock); + } + } + for (const auto& source : m_metadata->topology().sources) { + if (emittedRoutes.find(source.route.id) != emittedRoutes.end()) { + emittedTopology.sources.push_back(source); + } + } + + CtfMetadataModel completed(m_metadata->traceUuid(), std::move(emittedTopology)); + for (const auto& stream : completed.topology().streams) { + const auto numbers = m_emittedExceptionNumbers.find(stream.route.id); + if (numbers == m_emittedExceptionNumbers.end()) { + continue; + } + for (const auto number : numbers->second) { + completed.observeException(stream.streamClassId, number); } } - CtfMetadataWriter::write(m_outputDirectory, *m_metadata); + CtfMetadataWriter::write(m_outputDirectory, completed); + m_completedMetadata.emplace(std::move(completed)); } diff --git a/tools/ctrace/src/output/ctf/CtfEncoder.h b/tools/ctrace/src/output/ctf/CtfEncoder.h index c6a9f023d..f5f30d677 100644 --- a/tools/ctrace/src/output/ctf/CtfEncoder.h +++ b/tools/ctrace/src/output/ctf/CtfEncoder.h @@ -37,7 +37,7 @@ struct CtfEncoderConfig { bool legacyRouteFallback = true; }; -/** @brief Encodes semantic trace events into one CTF stream and metadata set. */ +/** @brief Encodes semantic trace events into lazy route-specific CTF streams and one metadata set. */ class CtfEncoder final { public: /** @brief Creates an encoder from validated CTF configuration. */ @@ -58,6 +58,8 @@ class CtfEncoder final { void abort() noexcept; /** @brief Encodes one selected semantic event. */ void writeEvent(const TraceEvent& event); + /** @brief Returns completed emitted metadata after a successful stop. */ + const CtfMetadataModel* completedMetadata() const noexcept; private: /** @brief Tracks timestamp and trace-quality state for one output stream. */ @@ -71,6 +73,14 @@ class CtfEncoder final { std::uint64_t allocateEventTimestamp(const TraceRouteIdentity& route); /** @brief Returns route-local CTF state while rejecting identity mismatches. */ StreamState& streamState(const TraceRouteIdentity& route); + /** @brief Returns the exact configured stream descriptor for one normalized route. */ + const CtfStreamDescriptor& streamDescriptor(const TraceRouteIdentity& route) const; + /** @brief Creates or returns the lazy binary writer for one configured stream. */ + CtfStreamWriter& ensureStreamWriter(const CtfStreamDescriptor& stream); + /** @brief Returns the already-created binary writer for one normalized route. */ + CtfStreamWriter& streamWriter(const TraceRouteIdentity& route); + /** @brief Tests whether one event can create selected CTF output. */ + bool activatesStream(const TraceEvent& event, bool selected) const; /** @brief Emits the legacy stream-local bootstrap exactly once. */ void bootstrapRoute(const TraceRouteIdentity& route); /** @brief Writes metadata that matches the completed binary stream. */ @@ -108,10 +118,12 @@ class CtfEncoder final { CtfEncoderConfig m_config; std::filesystem::path m_outputDirectory; std::optional m_metadata; - CtfStreamWriter m_stream; + std::optional m_completedMetadata; + std::map m_streams; bool m_recording = false; std::set m_bootstrappedRoutes; std::map m_streamStates; + std::map> m_emittedExceptionNumbers; std::set> m_reportedDwtSizeMismatches; std::map m_exceptionLanes; }; diff --git a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp index f1111a2d0..c84422415 100644 --- a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp +++ b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp @@ -10,6 +10,7 @@ #include "CtfSchema.h" #include +#include #include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include constexpr const char* kTraceCompassAnalysisVersionPlaceholder = "__SWO_ANALYSIS_VERSION__"; // Stack depth keeps overlapping visual pulses active until their last scheduled pop. @@ -50,6 +52,39 @@ static std::string withTraceCompassAnalysisVersion(std::string xml) return xml; } +/** @brief Prefixes every state-system path and matching view entry by the event's normalized route. */ +static std::string withRoutePrefixedPaths(std::string xml) +{ + constexpr std::string_view stateChange = ""; + constexpr std::string_view stateChangeEnd = ""; + constexpr std::string_view stateAttribute = "\n"; + + std::size_t searchOffset = 0U; + while ((searchOffset = xml.find(stateChange, searchOffset)) != std::string::npos) { + const auto changeEnd = xml.find(stateChangeEnd, searchOffset); + const auto attribute = xml.find(stateAttribute, searchOffset + stateChange.size()); + // All state changes come from the templates below and contain an output path. + assert(changeEnd != std::string::npos && attribute != std::string::npos && attribute < changeEnd); + const auto lineStart = xml.rfind('\n', attribute); + assert(lineStart != std::string::npos); + const auto indentationStart = lineStart + 1U; + const auto indentation = xml.substr(indentationStart, attribute - indentationStart); + const auto prefix = indentation + std::string(routeAttribute); + xml.insert(indentationStart, prefix); + searchOffset = changeEnd + prefix.size() + stateChangeEnd.size(); + } + + constexpr std::string_view entryPath = " @@ -517,10 +552,14 @@ static std::string traceCompassXml() xml << viewsXml(); xml << R"( )"; - return withTraceCompassAnalysisVersion(xml.str()); + auto result = xml.str(); + if (layout == TraceCompassXmlWriter::PathLayout::RoutePrefixed) { + result = withRoutePrefixedPaths(std::move(result)); + } + return withTraceCompassAnalysisVersion(std::move(result)); } -void TraceCompassXmlWriter::writeFile(const std::filesystem::path& filePath) +void TraceCompassXmlWriter::writeFile(const std::filesystem::path& filePath, PathLayout layout) { if (!filePath.parent_path().empty()) { std::filesystem::create_directories(filePath.parent_path()); @@ -529,7 +568,7 @@ void TraceCompassXmlWriter::writeFile(const std::filesystem::path& filePath) if (!out) { throw std::runtime_error("Failed to write Trace Compass XML " + filePath.string()); } - out << traceCompassXml(); + out << traceCompassXml(layout); out.close(); if (!out) { throw std::runtime_error("Failed to write Trace Compass XML " + filePath.string()); diff --git a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.h b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.h index f168d91a2..44611ed43 100644 --- a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.h +++ b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.h @@ -13,8 +13,14 @@ /** @brief Writes the Trace Compass analysis definition accompanying CTF output. */ class TraceCompassXmlWriter final { public: + /** @brief Selects the state-system hierarchy generated for one trace. */ + enum class PathLayout { + Legacy, + RoutePrefixed, + }; + /** @brief Writes the complete analysis definition to a file. */ - static void writeFile(const std::filesystem::path& path); + static void writeFile(const std::filesystem::path& path, PathLayout layout = PathLayout::Legacy); private: /** @brief Prevents construction of this stateless XML utility. */ diff --git a/tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp b/tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp index c946e2d02..ebe9934a0 100644 --- a/tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp +++ b/tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp @@ -5,15 +5,19 @@ * Generated with AI */ +#include "CtfTestSupport.h" #include "TestPath.h" #include "TestSupport.h" #include "TraceOutputTestSupport.h" #include #include "csv/CsvFileOutput.h" +#include "ctf/CtfBundleOutput.h" #include "DiagnosticSink.h" #include "TraceEvent.h" #include "TraceOutput.h" +#include "TraceOutputConfig.h" #include "TraceOutputLifecycle.h" +#include #include #include #include @@ -58,6 +62,77 @@ TEST(CtraceUnitTests, testTraceOutputLifecycleCompletesIndependentOutputs) << "output lifecycle should complete successful outputs despite another output failure"; } +TEST(CtraceUnitTests, testTraceOutputLifecycleCompletesCsvAfterLaterCtfStreamFailure) +{ + const TemporaryTestPath temporaryPath("ctrace-output-lifecycle-real-ctf-failure-test"); + const auto& root = temporaryPath.createDirectory(); + const auto ctfDirectory = root / "output.ctf"; + const auto xmlPath = root / "output.SWO.traceanalysis.xml"; + const auto csvPath = root / "output.csv"; + const TraceRouteIdentity firstRoute{TraceRouteId{10U}, 1U}; + const TraceRouteIdentity lastRoute{TraceRouteId{20U}, 111U}; + CtfMetadataTopology topology{ + {{CtfClockDomainId{1U}, "shared_clock", CtfTestSupport::testUuid(1U), 1000000U, false}}, + { + {CtfStreamClassId{1U}, firstRoute, CtfSourceKind::Itm, "core-one", CtfClockDomainId{1U}}, + {CtfStreamClassId{111U}, lastRoute, CtfSourceKind::Itm, "core-last", CtfClockDomainId{1U}}, + }, + {}, + }; + + CollectingDiagnosticSink diagnostics; + std::vector> outputs; + outputs.push_back( + std::make_unique(CtfOutputConfig(ctfDirectory, xmlPath, {}, std::move(topology), + std::vector{firstRoute, lastRoute}, true), + &diagnostics)); + outputs.push_back(std::make_unique(csvPath)); + TraceOutputLifecycle lifecycle(std::move(outputs), diagnostics); + + std::filesystem::create_directory(ctfDirectory / "stream_111"); + lifecycle.append(onRoute(softwarePacket(1U, 1U, 'A'), firstRoute)); + lifecycle.append(onRoute(softwarePacket(2U, 1U, 'B'), lastRoute)); + lifecycle.append(onRoute(softwarePacket(3U, 1U, 'C'), firstRoute)); + lifecycle.finish(); + + EXPECT_FALSE(std::filesystem::exists(ctfDirectory)); + EXPECT_FALSE(std::filesystem::exists(xmlPath)); + EXPECT_EQ(diagnostics.failureCount(), 1U); + EXPECT_TRUE(diagnostics.containsContext("backend", "ctf")); + EXPECT_TRUE(diagnostics.containsContext("phase", "write")); + const auto lines = readTestLines(csvPath); + ASSERT_EQ(lines.size(), 4U); + EXPECT_EQ(lines[1], ",1,itm,1,0x41,,,"); + EXPECT_EQ(lines[2], ",111,itm,2,0x42,,,"); + EXPECT_EQ(lines[3], ",1,itm,3,0x43,,,"); +} + +TEST(CtraceUnitTests, testTraceOutputLifecycleCompletesCtfAfterRealCsvStartFailure) +{ + const TemporaryTestPath temporaryPath("ctrace-output-lifecycle-real-csv-failure-test"); + const auto& root = temporaryPath.createDirectory(); + const auto ctfDirectory = root / "output.ctf"; + const auto xmlPath = root / "output.SWO.traceanalysis.xml"; + const auto csvPath = root / "blocked.csv"; + std::filesystem::create_directory(csvPath); + + CollectingDiagnosticSink diagnostics; + std::vector> outputs; + outputs.push_back(std::make_unique( + CtfOutputConfig(ctfDirectory, xmlPath, {}, CtfTestSupport::legacyTopology(1000000U)))); + outputs.push_back(std::make_unique(csvPath)); + TraceOutputLifecycle lifecycle(std::move(outputs), diagnostics); + lifecycle.append(softwarePacket(1U, 1U, 'A')); + lifecycle.finish(); + + EXPECT_TRUE(std::filesystem::is_regular_file(ctfDirectory / "metadata")); + EXPECT_TRUE(std::filesystem::is_regular_file(ctfDirectory / "stream_0")); + EXPECT_TRUE(std::filesystem::is_regular_file(xmlPath)); + EXPECT_EQ(diagnostics.failureCount(), 1U); + EXPECT_TRUE(diagnostics.containsContext("backend", "csv")); + EXPECT_TRUE(diagnostics.containsContext("phase", "start")); +} + TEST(CtraceUnitTests, testTraceOutputLifecycleReportsAbortFailures) { std::vector> outputs; diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp index f0d5d4fee..0944958b8 100644 --- a/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp +++ b/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp @@ -16,6 +16,7 @@ #include "ctf/CtfBundleOutput.h" #include "ctf/CtfMetadataWriter.h" #include "ctf/CtfSchema.h" +#include "ctf/TraceCompassXmlWriter.h" #include "CtraceRunMeta.h" #include "OutputRequirements.h" #include "TestPath.h" @@ -66,6 +67,18 @@ static CtfOutputConfig makeCtfBundleConfig(const std::filesystem::path& outputDi return CtfOutputConfig(outputDirectory, traceCompassXmlPath, {}, CtfTestSupport::legacyTopology(coreClockHz)); } +/** @brief Creates a formatted CTF bundle configuration from an explicit topology. */ +static CtfOutputConfig makeFormattedCtfBundleConfig(const std::filesystem::path& outputDirectory, + CtfMetadataTopology topology, TraceSelection selection = {}) +{ + std::vector routes; + for (const auto& stream : topology.streams) { + routes.push_back(stream.route); + } + return CtfOutputConfig(outputDirectory, testTraceCompassXmlPath(outputDirectory), std::move(selection), + std::move(topology), std::move(routes), true); +} + /** @brief Requires all files of a completed CTF bundle. */ static void requireCompleteCtfBundle(const std::filesystem::path& ctfDirectory, const std::filesystem::path& xmlPath, const std::string& message) @@ -131,6 +144,43 @@ static std::uint8_t readFirstCtfDwtValueTag(const std::filesystem::path& streamP return record.payload[2U]; } +/** @brief Requires every generated state change to begin its output path with the normalized route. */ +static void requireRoutePrefixedStateChanges(const std::string& xml) +{ + const std::string stateChange = ""; + const std::string stateChangeEnd = ""; + const std::string stateAttribute = ""; + std::size_t offset = 0U; + std::size_t stateChangeCount = 0U; + while ((offset = xml.find(stateChange, offset)) != std::string::npos) { + const auto end = xml.find(stateChangeEnd, offset); + const auto firstAttribute = xml.find(stateAttribute, offset + stateChange.size()); + ASSERT_NE(end, std::string::npos); + ASSERT_NE(firstAttribute, std::string::npos); + ASSERT_LT(firstAttribute, end); + EXPECT_EQ(xml.compare(firstAttribute, routeAttribute.size(), routeAttribute), 0); + ++stateChangeCount; + offset = end + stateChangeEnd.size(); + } + EXPECT_GT(stateChangeCount, 0U); +} + +/** @brief Requires every generated view entry to select all normalized trace routes. */ +static void requireRoutePrefixedViewEntries(const std::string& xml) +{ + const std::string entryPath = ""), std::string::npos); + EXPECT_NE(xml.find(" #include +using CtfTestSupport::CtfExceptionRecord; using CtfTestSupport::CtfRecord; -using CtfTestSupport::TimestampedCtfExceptionRecord; using CtfTestSupport::kCtfEventOffset; using CtfTestSupport::kCtfPacketContextSize; using CtfTestSupport::kCtfPacketHeaderSize; using CtfTestSupport::kCtfPacketSize; +using CtfTestSupport::readCtfExceptionRecords; using CtfTestSupport::readCtfRecords; using CtfTestSupport::readLe16; using CtfTestSupport::readLe32; using CtfTestSupport::requireFirstCtfRecord; using CtfTestSupport::requireSingleItmEvent; +using CtfTestSupport::TimestampedCtfExceptionRecord; using CtfTestSupport::timestampedCtfExceptionRecords; /** @brief Formats an encoded CTF UUID for comparison. */ @@ -88,6 +90,46 @@ static CtfEncoderConfig legacyEncoderConfig(std::uint64_t clockHz, TraceSelectio }; } +/** @brief Creates a two-route formatted topology with boundary stream IDs. */ +static CtfMetadataTopology formattedEncoderTopology(bool sharedClock = false) +{ + const TraceRouteIdentity first{TraceRouteId{4U}, 1U}; + const TraceRouteIdentity second{TraceRouteId{90U}, 111U}; + CtfMetadataTopology topology{ + { + {CtfClockDomainId{3U}, "first_clock", CtfTestSupport::testUuid(3U), 240000000U, false}, + {CtfClockDomainId{9U}, "second_clock", CtfTestSupport::testUuid(9U), 480000000U, false}, + }, + { + {CtfStreamClassId{1U}, first, CtfSourceKind::Itm, std::string("first"), CtfClockDomainId{3U}}, + {CtfStreamClassId{111U}, second, CtfSourceKind::Itm, std::string("second"), + sharedClock ? CtfClockDomainId{3U} : CtfClockDomainId{9U}}, + }, + { + {"dwt", 0U, first, std::string("First DWT"), 0x1000U, "unsigned", 4U}, + {"dwt", 0U, second, std::string("Second DWT"), 0x2000U, "signed", 2U}, + {"itm", 1U, first, std::string("First console"), std::nullopt, "unsigned", 4U}, + {"itm", 1U, second, std::string("Second console"), std::nullopt, "unsigned", 4U}, + }, + }; + if (sharedClock) { + topology.clockDomains.pop_back(); + } + return topology; +} + +/** @brief Creates an encoder configuration for the formatted test topology. */ +static CtfEncoderConfig formattedEncoderConfig(TraceSelection selection = {}, bool sharedClock = false) +{ + return { + formattedEncoderTopology(sharedClock), + std::move(selection), + nullptr, + {{TraceRouteId{4U}, 1U}, {TraceRouteId{90U}, 111U}}, + false, + }; +} + /** @brief Starts an encoder with a deterministic bundle UUID. */ static void startEncoder(CtfEncoder& encoder, const std::filesystem::path& outputDirectory) { @@ -362,6 +404,9 @@ TEST(CtraceUnitTests, testCtfEncoderRejectsInvalidClockAndPayloadMetadata) CtfEncoder missingTopology(CtfEncoderConfig{}); EXPECT_THROW(startEncoder(missingTopology, temporaryPath.path()), std::runtime_error); + CtfEncoder fallbackWithoutTopology(CtfEncoderConfig{{}, {}, nullptr, {{TraceRouteId{4U}, 1U}}, true}); + EXPECT_THROW(startEncoder(fallbackWithoutTopology, temporaryPath.path()), std::runtime_error); + CtfEncoder zeroClock(legacyEncoderConfig(0U)); EXPECT_THROW(startEncoder(zeroClock, temporaryPath.path()), std::invalid_argument); @@ -383,47 +428,315 @@ TEST(CtraceUnitTests, testCtfEncoderRejectsInvalidClockAndPayloadMetadata) invalidAddress.abort(); } -TEST(CtraceUnitTests, testCtfEncoderRejectsFormattedTopologiesBeforeOpeningAStream) +TEST(CtraceUnitTests, testCtfEncoderKeepsFormattedStreamsLazyAndProjectsCompletedMetadata) { - const TemporaryTestPath temporaryPath("ctrace-ctf-formatted-runtime-boundary-test"); - temporaryPath.createDirectory(); - const auto singleDirectory = temporaryPath.path() / "single"; - const auto multipleDirectory = temporaryPath.path() / "multiple"; - std::filesystem::create_directories(singleDirectory); - std::filesystem::create_directories(multipleDirectory); - const auto hardStop = "CTF binary output currently requires exactly one legacy SINGLE stream topology"; - - CtfMetadataTopology singleTopology{ - {{CtfClockDomainId{1U}, "formatted_clock", CtfTestSupport::testUuid(1U), 1000000U, false}}, - {{CtfStreamClassId{1U}, {TraceRouteId{7U}, 1U}, CtfSourceKind::Itm, std::string("core"), CtfClockDomainId{1U}}}, - {}, - }; - CtfEncoder single(CtfEncoderConfig{std::move(singleTopology), {}, nullptr, {}, false}); - EXPECT_TRUE(throwsWithMessage([&] { startEncoder(single, singleDirectory); }, hardStop)); - EXPECT_FALSE(std::filesystem::exists(singleDirectory / "stream_0")); + const TemporaryTestPath temporaryPath("ctrace-ctf-formatted-lazy-test"); + const auto& outputDirectory = temporaryPath.createDirectory(); + CtfEncoder encoder(formattedEncoderConfig()); + startEncoder(encoder, outputDirectory); - CtfMetadataTopology multipleTopology{ - { - {CtfClockDomainId{3U}, "first_clock", CtfTestSupport::testUuid(3U), 1000000U, false}, - {CtfClockDomainId{9U}, "second_clock", CtfTestSupport::testUuid(9U), 2000000U, false}, - }, - { - {CtfStreamClassId{1U}, - {TraceRouteId{4U}, 1U}, - CtfSourceKind::Itm, - std::string("first"), - CtfClockDomainId{3U}}, - {CtfStreamClassId{111U}, - {TraceRouteId{90U}, 111U}, - CtfSourceKind::Itm, - std::string("second"), - CtfClockDomainId{9U}}, - }, - {}, - }; - CtfEncoder multiple(CtfEncoderConfig{std::move(multipleTopology), {}, nullptr, {}, false}); - EXPECT_TRUE(throwsWithMessage([&] { startEncoder(multiple, multipleDirectory); }, hardStop)); - EXPECT_FALSE(std::filesystem::exists(multipleDirectory / "stream_0")); + EXPECT_EQ(encoder.completedMetadata(), nullptr); + EXPECT_FALSE(std::filesystem::exists(outputDirectory / "stream_1")); + EXPECT_FALSE(std::filesystem::exists(outputDirectory / "stream_111")); + const TraceRouteIdentity firstRoute{TraceRouteId{4U}, 1U}; + encoder.writeEvent(atCycle(onRoute(TraceEvent{LocalTimestampTraceEvent{}}, firstRoute), 30U)); + EXPECT_FALSE(std::filesystem::exists(outputDirectory / "stream_1")) + << "a control packet without CTF output must not create a formatted stream"; + + encoder.writeEvent(atCycle(onRoute(softwarePacket(1U, 1U, 'A'), firstRoute), 40U)); + EXPECT_TRUE(std::filesystem::is_regular_file(outputDirectory / "stream_1")); + EXPECT_FALSE(std::filesystem::exists(outputDirectory / "stream_111")); + encoder.stop(); + + const auto records = readCtfRecords(outputDirectory / "stream_1"); + ASSERT_EQ(records.size(), 3U); + EXPECT_EQ(records[0].id, CtfSchema::value(CtfSchema::EventId::TraceStatus)); + EXPECT_EQ(records[0].payload[0U], CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart)); + EXPECT_EQ(records[0].timestamp, 30U); + EXPECT_EQ(records[1].id, CtfSchema::value(CtfSchema::EventId::Exception)); + EXPECT_EQ(records[2].id, CtfSchema::value(CtfSchema::EventId::Itm)); + EXPECT_EQ(records[2].timestamp, 40U); + for (const auto& record : records) { + EXPECT_EQ(record.traceBusId, 1U); + } + + const auto* completed = encoder.completedMetadata(); + ASSERT_NE(completed, nullptr); + ASSERT_EQ(completed->topology().streams.size(), 1U); + ASSERT_EQ(completed->topology().clockDomains.size(), 1U); + ASSERT_EQ(completed->topology().sources.size(), 2U); + EXPECT_EQ(completed->topology().streams.front().streamClassId, CtfStreamClassId{1U}); + EXPECT_EQ(completed->topology().clockDomains.front().id, CtfClockDomainId{3U}); + for (const auto& source : completed->topology().sources) { + EXPECT_EQ(source.route.traceBusId, 1U); + } + const auto metadata = readTestTextFile(outputDirectory / "metadata"); + EXPECT_NE(metadata.find("stream {\n id = 1;"), std::string::npos); + EXPECT_NE(metadata.find("name = first_clock;"), std::string::npos); + EXPECT_EQ(metadata.find("stream {\n id = 111;"), std::string::npos); + EXPECT_EQ(metadata.find("name = second_clock;"), std::string::npos); + EXPECT_EQ(metadata.find("Second DWT"), std::string::npos); + + encoder.abort(); + EXPECT_EQ(encoder.completedMetadata(), nullptr); + EXPECT_TRUE(std::filesystem::is_regular_file(outputDirectory / "metadata")) + << "the non-owning encoder must not delete its caller's completed output"; +} + +TEST(CtraceUnitTests, testCtfEncoderWritesInterleavedNonContiguousStreamsWithIndependentState) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-formatted-multistream-test"); + const auto& outputDirectory = temporaryPath.createDirectory(); + CtfEncoder encoder(formattedEncoderConfig()); + startEncoder(encoder, outputDirectory); + + const TraceRouteIdentity firstRoute{TraceRouteId{4U}, 1U}; + const TraceRouteIdentity secondRoute{TraceRouteId{90U}, 111U}; + encoder.writeEvent(atCycle(onRoute(softwarePacket(1U, 1U, 'A'), firstRoute), 100U)); + encoder.writeEvent(atCycle(onRoute(softwarePacket(1U, 1U, 'B'), secondRoute), 10U)); + encoder.writeEvent(atCycle(onRoute(softwarePacket(1U, 1U, 'C'), firstRoute), 50U)); + encoder.writeEvent(atCycle(onRoute(softwarePacket(1U, 1U, 'D'), secondRoute), 20U)); + encoder.stop(); + + const auto firstRecords = readCtfRecords(outputDirectory / "stream_1"); + const auto secondRecords = readCtfRecords(outputDirectory / "stream_111"); + ASSERT_EQ(firstRecords.size(), 4U); + ASSERT_EQ(secondRecords.size(), 4U); + EXPECT_EQ(firstRecords[0].id, CtfSchema::value(CtfSchema::EventId::TraceStatus)); + EXPECT_EQ(secondRecords[0].id, CtfSchema::value(CtfSchema::EventId::TraceStatus)); + EXPECT_EQ(firstRecords[1].id, CtfSchema::value(CtfSchema::EventId::Exception)); + EXPECT_EQ(secondRecords[1].id, CtfSchema::value(CtfSchema::EventId::Exception)); + EXPECT_EQ(firstRecords[2].timestamp, 100U); + EXPECT_EQ(firstRecords[3].timestamp, 100U); + EXPECT_EQ(secondRecords[2].timestamp, 10U); + EXPECT_EQ(secondRecords[3].timestamp, 20U); + for (const auto& record : firstRecords) { + EXPECT_EQ(record.traceBusId, 1U); + } + for (const auto& record : secondRecords) { + EXPECT_EQ(record.traceBusId, 111U); + } + + const auto firstBytes = readTestBinaryFile(outputDirectory / "stream_1"); + const auto secondBytes = readTestBinaryFile(outputDirectory / "stream_111"); + EXPECT_EQ(readLe32(firstBytes, 20U), 1U); + EXPECT_EQ(readLe32(secondBytes, 20U), 111U); + EXPECT_EQ(formatCtfUuid(firstBytes, 4U), CtfTestSupport::testUuid().toString()); + EXPECT_EQ(formatCtfUuid(secondBytes, 4U), CtfTestSupport::testUuid().toString()); + EXPECT_EQ(readLe32(firstBytes, kCtfPacketHeaderSize + 28U), 0U); + EXPECT_EQ(readLe32(secondBytes, kCtfPacketHeaderSize + 28U), 0U); + + const auto* completed = encoder.completedMetadata(); + ASSERT_NE(completed, nullptr); + EXPECT_EQ(completed->topology().streams.size(), 2U); + EXPECT_EQ(completed->topology().clockDomains.size(), 2U); + EXPECT_EQ(completed->topology().sources.size(), 4U); + const auto metadata = readTestTextFile(outputDirectory / "metadata"); + EXPECT_NE(metadata.find("stream {\n id = 1;"), std::string::npos); + EXPECT_NE(metadata.find("stream {\n id = 111;"), std::string::npos); + EXPECT_NE(metadata.find("cmsis_stream_1_dwt0_value_type = \"unsigned\";"), std::string::npos); + EXPECT_NE(metadata.find("cmsis_stream_111_dwt0_value_type = \"signed\";"), std::string::npos); +} + +TEST(CtraceUnitTests, testCtfEncoderAppliesFormattedStreamFilterBeforeLazyCreation) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-formatted-stream-filter-test"); + const auto& outputDirectory = temporaryPath.createDirectory(); + CtfEncoder encoder(formattedEncoderConfig(TraceSelection{{}, {111U}})); + startEncoder(encoder, outputDirectory); + const TraceRouteIdentity firstRoute{TraceRouteId{4U}, 1U}; + const TraceRouteIdentity secondRoute{TraceRouteId{90U}, 111U}; + + encoder.writeEvent(onRoute(softwarePacket(1U, 1U, 'A'), firstRoute)); + encoder.writeEvent(onRoute(TraceEvent{OverflowTraceEvent{}}, firstRoute)); + encoder.writeEvent(onRoute(TraceEvent{SyncTraceEvent{}}, firstRoute)); + EXPECT_FALSE(std::filesystem::exists(outputDirectory / "stream_1")); + + encoder.writeEvent(onRoute(softwarePacket(1U, 1U, 'B'), secondRoute)); + encoder.stop(); + EXPECT_FALSE(std::filesystem::exists(outputDirectory / "stream_1")); + EXPECT_TRUE(std::filesystem::is_regular_file(outputDirectory / "stream_111")); + ASSERT_NE(encoder.completedMetadata(), nullptr); + ASSERT_EQ(encoder.completedMetadata()->topology().streams.size(), 1U); + EXPECT_EQ(encoder.completedMetadata()->topology().streams.front().streamClassId, CtfStreamClassId{111U}); + ASSERT_EQ(encoder.completedMetadata()->topology().clockDomains.size(), 1U); + EXPECT_EQ(encoder.completedMetadata()->topology().clockDomains.front().id, CtfClockDomainId{9U}); +} + +TEST(CtraceUnitTests, testCtfEncoderKeepsOverflowQualityIndependentAcrossFormattedRoutes) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-formatted-overflow-isolation-test"); + const auto& outputDirectory = temporaryPath.createDirectory(); + CtfEncoder encoder(formattedEncoderConfig()); + startEncoder(encoder, outputDirectory); + const TraceRouteIdentity firstRoute{TraceRouteId{4U}, 1U}; + const TraceRouteIdentity secondRoute{TraceRouteId{90U}, 111U}; + + auto firstOverflow = onRoute(TraceEvent{OverflowTraceEvent{}}, firstRoute); + firstOverflow.quality = TraceQuality{true, false, 7U}; + encoder.writeEvent(firstOverflow); + auto secondSample = onRoute(softwarePacket(1U, 1U, 'B'), secondRoute); + secondSample.quality = TraceQuality{false, true, 0U}; + encoder.writeEvent(secondSample); + encoder.writeEvent(onRoute(softwarePacket(1U, 1U, 'A'), firstRoute)); + encoder.stop(); + + const auto firstRecords = readCtfRecords(outputDirectory / "stream_1"); + const auto secondRecords = readCtfRecords(outputDirectory / "stream_111"); + const auto& firstSample = + requireFirstCtfRecord(firstRecords, CtfSchema::EventId::Itm, "first route's CTF ITM sample is missing"); + const auto& secondRouteSample = + requireFirstCtfRecord(secondRecords, CtfSchema::EventId::Itm, "second route's CTF ITM sample is missing"); + EXPECT_EQ(readLe32(firstSample.payload, 4U), 7U); + EXPECT_EQ(readLe32(secondRouteSample.payload, 4U), 0U); + EXPECT_EQ(secondRouteSample.payload[3U] & CtfSchema::SampleFlagOverflow, 0U); + const auto firstOverflowStatus = std::find_if(firstRecords.begin(), firstRecords.end(), [](const CtfRecord& record) { + return record.id == CtfSchema::value(CtfSchema::EventId::TraceStatus) && + record.payload[0U] == CtfSchema::value(CtfSchema::TraceStatusReason::Overflow); + }); + ASSERT_NE(firstOverflowStatus, firstRecords.end()); + EXPECT_EQ(readLe32(firstOverflowStatus->payload, 1U), 7U); + EXPECT_EQ(std::count_if(secondRecords.begin(), secondRecords.end(), + [](const CtfRecord& record) { + return record.id == CtfSchema::value(CtfSchema::EventId::TraceStatus) && + record.payload[0U] == CtfSchema::value(CtfSchema::TraceStatusReason::Overflow); + }), + 0); +} + +TEST(CtraceUnitTests, testCtfEncoderKeepsExceptionLanesIndependentAcrossFormattedRoutes) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-formatted-exception-isolation-test"); + const auto& outputDirectory = temporaryPath.createDirectory(); + CtfEncoder encoder(formattedEncoderConfig()); + startEncoder(encoder, outputDirectory); + const TraceRouteIdentity firstRoute{TraceRouteId{4U}, 1U}; + const TraceRouteIdentity secondRoute{TraceRouteId{90U}, 111U}; + + encoder.writeEvent(onRoute(exceptionPacket(15U, ExceptionAction::Entered, 10U), firstRoute)); + encoder.writeEvent(onRoute(exceptionPacket(54U, ExceptionAction::Entered, 20U), secondRoute)); + encoder.writeEvent(onRoute(exceptionPacket(16U, ExceptionAction::Entered, 30U), firstRoute)); + encoder.writeEvent(onRoute(exceptionPacket(0U, ExceptionAction::Returned, 40U), secondRoute)); + encoder.stop(); + + EXPECT_EQ(readCtfExceptionRecords(outputDirectory / "stream_1"), (std::vector({ + {0U, 0U, 1U}, + {0U, 1U, 1U}, + {15U, 0U, 0U}, + {15U, 1U, 1U}, + {16U, 0U, 0U}, + }))); + EXPECT_EQ(readCtfExceptionRecords(outputDirectory / "stream_111"), (std::vector({ + {0U, 0U, 1U}, + {0U, 1U, 1U}, + {54U, 0U, 0U}, + {54U, 1U, 1U}, + {0U, 2U, 0U}, + }))); + ASSERT_NE(encoder.completedMetadata(), nullptr); + EXPECT_EQ(encoder.completedMetadata()->observedExceptions(CtfStreamClassId{1U}), + (std::vector{0U, 15U, 16U})); + EXPECT_EQ(encoder.completedMetadata()->observedExceptions(CtfStreamClassId{111U}), + (std::vector{0U, 54U})); +} + +TEST(CtraceUnitTests, testCtfEncoderWritesMetadataOnlyForEmptyFormattedTopology) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-formatted-empty-test"); + const auto& outputDirectory = temporaryPath.createDirectory(); + CtfEncoder encoder(CtfEncoderConfig{{}, {}, nullptr, {}, false}); + startEncoder(encoder, outputDirectory); + encoder.stop(); + + ASSERT_NE(encoder.completedMetadata(), nullptr); + EXPECT_TRUE(encoder.completedMetadata()->topology().streams.empty()); + EXPECT_TRUE(encoder.completedMetadata()->topology().clockDomains.empty()); + EXPECT_TRUE(std::filesystem::is_regular_file(outputDirectory / "metadata")); + EXPECT_FALSE(std::filesystem::exists(outputDirectory / "stream_0")); + EXPECT_FALSE(std::filesystem::exists(outputDirectory / "stream_1")); +} + +TEST(CtraceUnitTests, testCtfEncoderDoesNotCreateFormattedArtifactsForEventsWithoutSelectedRecords) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-formatted-filter-test"); + const auto& outputDirectory = temporaryPath.createDirectory(); + CtfEncoder encoder(formattedEncoderConfig(TraceSelection{{"itm"}, {}})); + startEncoder(encoder, outputDirectory); + const TraceRouteIdentity firstRoute{TraceRouteId{4U}, 1U}; + const TraceRouteIdentity secondRoute{TraceRouteId{90U}, 111U}; + + encoder.writeEvent(onRoute(exceptionPacket(15U, ExceptionAction::Entered, 1U), firstRoute)); + encoder.writeEvent(onRoute(exceptionPacket(15U, ExceptionAction::Unknown, 2U), firstRoute)); + encoder.writeEvent(onRoute(TraceEvent{OverflowTraceEvent{}}, firstRoute)); + encoder.writeEvent(onRoute(TraceEvent{SyncTraceEvent{}}, firstRoute)); + encoder.writeEvent(onRoute(TraceEvent{DwtDataTraceEvent{0U, 1U, 1U, AccessType::Read}}, firstRoute)); + encoder.writeEvent(onRoute(TraceEvent{DwtEventTraceEvent{0U}}, firstRoute)); + encoder.writeEvent(onRoute(TraceEvent{PmuTraceEvent{0U}}, firstRoute)); + EXPECT_FALSE(std::filesystem::exists(outputDirectory / "stream_1")); + EXPECT_FALSE(std::filesystem::exists(outputDirectory / "stream_111")); + + encoder.writeEvent(onRoute(exceptionPacket(75U, ExceptionAction::Entered, 3U), secondRoute)); + encoder.writeEvent(onRoute(softwarePacket(1U, 1U, 'A'), secondRoute)); + encoder.stop(); + EXPECT_FALSE(std::filesystem::exists(outputDirectory / "stream_1")); + EXPECT_TRUE(std::filesystem::is_regular_file(outputDirectory / "stream_111")); + ASSERT_NE(encoder.completedMetadata(), nullptr); + ASSERT_EQ(encoder.completedMetadata()->topology().streams.size(), 1U); + EXPECT_EQ(encoder.completedMetadata()->topology().streams.front().streamClassId, CtfStreamClassId{111U}); + EXPECT_EQ(encoder.completedMetadata()->topology().clockDomains.front().id, CtfClockDomainId{9U}); + EXPECT_TRUE(encoder.completedMetadata()->observedExceptions(CtfStreamClassId{111U}).empty()); + EXPECT_EQ(readTestTextFile(outputDirectory / "metadata").find("\"External IRQ 59\" = 75"), std::string::npos) + << "a filtered exception must not leak into metadata when another event later emits the stream"; + + const auto zeroCounterDirectory = outputDirectory / "zero-counters"; + std::filesystem::create_directory(zeroCounterDirectory); + CtfEncoder zeroCounters(formattedEncoderConfig()); + startEncoder(zeroCounters, zeroCounterDirectory); + zeroCounters.writeEvent(onRoute(TraceEvent{DwtEventTraceEvent{0U}}, firstRoute)); + zeroCounters.writeEvent(onRoute(TraceEvent{PmuTraceEvent{0U}}, firstRoute)); + zeroCounters.stop(); + EXPECT_FALSE(std::filesystem::exists(zeroCounterDirectory / "stream_1")); + ASSERT_NE(zeroCounters.completedMetadata(), nullptr); + EXPECT_TRUE(zeroCounters.completedMetadata()->topology().streams.empty()); +} + +TEST(CtraceUnitTests, testCtfEncoderCreatesFormattedWritersForStatusOnlyOutput) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-formatted-status-only-test"); + const auto& root = temporaryPath.createDirectory(); + const TraceRouteIdentity route{TraceRouteId{4U}, 1U}; + + const auto syncDirectory = root / "sync"; + std::filesystem::create_directory(syncDirectory); + CtfEncoder syncEncoder(formattedEncoderConfig()); + startEncoder(syncEncoder, syncDirectory); + syncEncoder.writeEvent(onRoute(TraceEvent{SyncTraceEvent{}}, route)); + syncEncoder.stop(); + const auto syncRecords = readCtfRecords(syncDirectory / "stream_1"); + ASSERT_EQ(syncRecords.size(), 3U); + EXPECT_EQ(syncRecords[0].payload[0U], CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart)); + EXPECT_EQ(syncRecords[1].id, CtfSchema::value(CtfSchema::EventId::Exception)); + EXPECT_EQ(syncRecords[2].payload[0U], CtfSchema::value(CtfSchema::TraceStatusReason::Resync)); + + const auto overflowDirectory = root / "overflow"; + std::filesystem::create_directory(overflowDirectory); + CtfEncoder overflowEncoder(formattedEncoderConfig(TraceSelection{{"overflow"}, {}})); + startEncoder(overflowEncoder, overflowDirectory); + overflowEncoder.writeEvent(onRoute(TraceEvent{OverflowTraceEvent{}}, route)); + overflowEncoder.stop(); + const auto overflowRecords = readCtfRecords(overflowDirectory / "stream_1"); + ASSERT_EQ(overflowRecords.size(), 1U); + EXPECT_EQ(overflowRecords.front().payload[0U], CtfSchema::value(CtfSchema::TraceStatusReason::Overflow)); + + const auto issueDirectory = root / "issue"; + std::filesystem::create_directory(issueDirectory); + CtfEncoder issueEncoder(formattedEncoderConfig(TraceSelection{{"error"}, {}})); + startEncoder(issueEncoder, issueDirectory); + issueEncoder.writeEvent(onRoute(issuePacket(TraceIssueCode::OpenCsdDecodeError), route)); + issueEncoder.stop(); + const auto issueRecords = readCtfRecords(issueDirectory / "stream_1"); + ASSERT_EQ(issueRecords.size(), 1U); + EXPECT_EQ(issueRecords.front().payload[0U], CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError)); } TEST(CtraceUnitTests, testCtfEncoderWritesConfiguredDwtValueVariantsAndDefault) From 329e6aec628a01dafd0fbdc0cb901bf04991d66a Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Thu, 10 Sep 2026 11:06:25 +0200 Subject: [PATCH 07/31] feat(ctrace): route single input through DecodeTree --- .../ctrace/docs/multicore-multisource-plan.md | 4 +- tools/ctrace/src/CMakeLists.txt | 2 + tools/ctrace/src/decode/OpenCsdItmSession.cpp | 89 ++------ tools/ctrace/src/decode/OpenCsdItmSession.h | 69 +----- .../ctrace/src/decode/OpenCsdTreeSession.cpp | 146 +++++++++++++ tools/ctrace/src/decode/OpenCsdTreeSession.h | 134 ++++++++++++ tools/ctrace/test/unit/CMakeLists.txt | 1 + .../src/decode/OpenCsdItmDecoderTests.cpp | 35 ++- .../src/decode/OpenCsdTreeSessionTests.cpp | 204 ++++++++++++++++++ 9 files changed, 531 insertions(+), 153 deletions(-) create mode 100644 tools/ctrace/src/decode/OpenCsdTreeSession.cpp create mode 100644 tools/ctrace/src/decode/OpenCsdTreeSession.h create mode 100644 tools/ctrace/test/unit/src/decode/OpenCsdTreeSessionTests.cpp diff --git a/tools/ctrace/docs/multicore-multisource-plan.md b/tools/ctrace/docs/multicore-multisource-plan.md index 9e83d2d8d..99e764d1c 100644 --- a/tools/ctrace/docs/multicore-multisource-plan.md +++ b/tools/ctrace/docs/multicore-multisource-plan.md @@ -541,8 +541,8 @@ Phase 0 -> Phase 1 -> Phase 2 -> Phase 3 -> Phase 4 | 3 | Route-aware semantic state, diagnostics, and CSV | Complete | | 4 | CTF descriptors and metadata model | Complete | | 5 | Multi-stream CTF bundle and Trace Compass policy | Complete | -| 6 | DecodeTree `SINGLE` migration | Next | -| 7 | Clean formatted decoding and TB integration | Pending | +| 6 | DecodeTree `SINGLE` migration | Complete | +| 7 | Clean formatted decoding and TB integration | Next | | 8 | Route-local recovery and error isolation | Pending | | 9 | Consumer validation, documentation, and final hardening | Pending | diff --git a/tools/ctrace/src/CMakeLists.txt b/tools/ctrace/src/CMakeLists.txt index 9bde3fd35..5043f4872 100644 --- a/tools/ctrace/src/CMakeLists.txt +++ b/tools/ctrace/src/CMakeLists.txt @@ -33,6 +33,7 @@ set(CTRACE_DECODE_HEADER_FILES decode/OpenCsdItmSession.h decode/OpenCsdPacketCollector.h decode/OpenCsdTraceElement.h + decode/OpenCsdTreeSession.h decode/SaturatingArithmetic.h ) set(CTRACE_OUTPUT_HEADER_FILES @@ -133,6 +134,7 @@ add_library(ctrace-decode STATIC decode/OpenCsdItmDecoder.cpp decode/OpenCsdItmSession.cpp decode/OpenCsdPacketCollector.cpp + decode/OpenCsdTreeSession.cpp ${CTRACE_DECODE_HEADER_FILES} ) add_library(ctrace::decode ALIAS ctrace-decode) diff --git a/tools/ctrace/src/decode/OpenCsdItmSession.cpp b/tools/ctrace/src/decode/OpenCsdItmSession.cpp index 775ffb252..c62f44fe1 100644 --- a/tools/ctrace/src/decode/OpenCsdItmSession.cpp +++ b/tools/ctrace/src/decode/OpenCsdItmSession.cpp @@ -9,10 +9,7 @@ #include "OpenCsdErrorController.h" #include "OpenCsdPacketCollector.h" -#include "common/ocsd_dcd_mngr_i.h" -#include "common/ocsd_lib_dcd_register.h" -#include "common/trc_component.h" -#include "interfaces/trc_data_raw_in_i.h" +#include "OpenCsdTreeSession.h" #include "opencsd/itm/trc_pkt_types_itm.h" #include "opencsd/ocsd_if_types.h" @@ -22,99 +19,37 @@ constexpr std::uint32_t kItmTcrSwoEnable = 1U << 4U; constexpr ocsd_itm_cfg kItmConfig{kItmTcrSwoEnable}; OpenCsdItmSession::OpenCsdItmSession(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController) - : OpenCsdItmSession(collector, errorController, &OcsdLibDcdRegister::getDecoderRegister) -{ -} - -OpenCsdItmSession::OpenCsdItmSession(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController, - DecoderRegistryProvider registryProvider) - : m_config(&kItmConfig) + : m_config(&kItmConfig), + m_treeSession(OCSD_TRC_SRC_SINGLE, 0U, errorController, collector) { // Keep OpenCSD timestamps in raw ITM ticks. They are scaled after decode, // where the originating CoreSight stream and processor are known. - createDecoder(collector, errorController, registryProvider); + // Channel 0 is OpenCSD's internal SINGLE transport channel. It is not an + // architectural Trace Bus ID and the bound collector retains the synthetic route. + m_config.setTraceID(0U); + m_treeSession.createDecoder(OCSD_BUILTIN_DCD_ITM, OCSD_CREATE_FLG_FULL_DECODER, m_config); + m_treeSession.attachDecoderCallbacks(0U, collector); } OpenCsdItmSession::~OpenCsdItmSession() noexcept = default; -void OpenCsdSessionValidation::requireObject(const void* object, const char* message) -{ - if (object == nullptr) { - throw OpenCsdItmSessionError(message); - } -} - -void OpenCsdSessionValidation::requireSuccess(ocsd_err_t error, const char* message) -{ - if (error != OCSD_OK) { - throw OpenCsdItmSessionError(OpenCsdErrorController::describeApiError(error, message)); - } -} - -void OpenCsdItmSession::DecoderDeleter::operator()(TraceComponent* component) const noexcept -{ - if (manager != nullptr) { - manager->destroyDecoder(component); - } -} - ocsd_datapath_resp_t OpenCsdItmSession::pushData(ocsd_trc_index_t index, std::uint32_t size, const std::uint8_t* data, std::uint32_t& processed) { - return m_input->TraceDataIn(OCSD_OP_DATA, index, size, data, &processed); + return m_treeSession.traceDataIn(OCSD_OP_DATA, index, size, data, &processed); } ocsd_datapath_resp_t OpenCsdItmSession::flush() { - return m_input->TraceDataIn(OCSD_OP_FLUSH, 0, 0, nullptr, nullptr); + return m_treeSession.traceDataIn(OCSD_OP_FLUSH, 0, 0, nullptr, nullptr); } ocsd_datapath_resp_t OpenCsdItmSession::reset() { - return m_input->TraceDataIn(OCSD_OP_RESET, 0, 0, nullptr, nullptr); + return m_treeSession.traceDataIn(OCSD_OP_RESET, 0, 0, nullptr, nullptr); } ocsd_datapath_resp_t OpenCsdItmSession::endOfTrace() { - return m_input->TraceDataIn(OCSD_OP_EOT, 0, 0, nullptr, nullptr); -} - -void OpenCsdItmSession::createDecoder(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController, - DecoderRegistryProvider registryProvider) -{ - if (registryProvider == nullptr) { - throw OpenCsdItmSessionError("OpenCSD decoder registry provider is not configured"); - } - auto* registry = registryProvider(); - if (registry == nullptr) { - throw OpenCsdItmSessionError("OpenCSD decoder registry is not initialized"); - } - - auto error = registry->getDecoderMngrByName(OCSD_BUILTIN_DCD_ITM, &m_manager); - OpenCsdSessionValidation::requireSuccess(error, "failed to get OpenCSD ITM decoder manager"); - OpenCsdSessionValidation::requireObject(m_manager, "OpenCSD ITM decoder manager is not initialized"); - - TraceComponent* component = nullptr; - error = m_manager->createDecoder(OCSD_CREATE_FLG_FULL_DECODER, 0, &m_config, &component); - m_component.get_deleter().manager = m_manager; - m_component.reset(component); - OpenCsdSessionValidation::requireSuccess(error, "failed to create OpenCSD ITM decoder"); - OpenCsdSessionValidation::requireObject(m_component.get(), "OpenCSD ITM decoder component is not initialized"); - - error = m_manager->attachErrorLogger(m_component.get(), &errorController); - OpenCsdSessionValidation::requireSuccess(error, "failed to attach OpenCSD packet-decoder error logger"); - if (m_component->getAssocComponent() != nullptr) { - error = m_manager->attachErrorLogger(m_component->getAssocComponent(), &errorController); - OpenCsdSessionValidation::requireSuccess(error, "failed to attach OpenCSD packet-processor error logger"); - } - - error = m_manager->attachOutputSink(m_component.get(), &collector); - OpenCsdSessionValidation::requireSuccess(error, "failed to attach OpenCSD ITM output sink"); - - error = m_manager->getDataInputI(m_component.get(), &m_input); - OpenCsdSessionValidation::requireSuccess(error, "failed to get OpenCSD ITM input interface"); - OpenCsdSessionValidation::requireObject(m_input, "OpenCSD ITM input interface is not initialized"); - - error = m_manager->attachPktMonitor(m_component.get(), &collector); - OpenCsdSessionValidation::requireSuccess(error, "failed to attach OpenCSD ITM packet monitor"); + return m_treeSession.traceDataIn(OCSD_OP_EOT, 0, 0, nullptr, nullptr); } diff --git a/tools/ctrace/src/decode/OpenCsdItmSession.h b/tools/ctrace/src/decode/OpenCsdItmSession.h index b6268f636..eebb361f3 100644 --- a/tools/ctrace/src/decode/OpenCsdItmSession.h +++ b/tools/ctrace/src/decode/OpenCsdItmSession.h @@ -8,19 +8,14 @@ #ifndef CTRACE_SRC_DECODE_OPENCSDITMSESSION_H #define CTRACE_SRC_DECODE_OPENCSDITMSESSION_H +#include "OpenCsdTreeSession.h" #include "opencsd/itm/trc_cmp_cfg_itm.h" #include "opencsd/ocsd_if_types.h" #include -#include -#include -class IDecoderMngr; -class ITrcDataIn; class OpenCsdErrorController; class OpenCsdPacketCollector; -class OcsdLibDcdRegister; -class TraceComponent; /** @brief Abstracts one OpenCSD ITM session for production use and tests. */ class OpenCsdItmSessionInterface { @@ -46,35 +41,8 @@ class OpenCsdItmSessionInterface { virtual ocsd_datapath_resp_t endOfTrace() = 0; }; -/** @brief Reports an OpenCSD session creation or API failure. */ -class OpenCsdItmSessionError final : public std::runtime_error { -public: - /** @brief Inherits standard runtime-error construction. */ - using std::runtime_error::runtime_error; -}; - -/** @brief Validates pointers and results returned by OpenCSD session setup APIs. */ -class OpenCsdSessionValidation final { -public: - /** - * @brief Rejects a null OpenCSD API object with a session error. - * @param object Required external API object. - * @param message Failure text used when object is null. - * @throws OpenCsdItmSessionError If object is null. - */ - static void requireObject(const void* object, const char* message); - /** - * @brief Rejects an unsuccessful OpenCSD API result with a session error. - * @param error OpenCSD result to validate. - * @param message Failure text used for an error result. - * @throws OpenCsdItmSessionError If error does not report success. - */ - static void requireSuccess(ocsd_err_t error, const char* message); - -private: - /** @brief Prevents construction of this stateless validation utility. */ - OpenCsdSessionValidation() = delete; -}; +/** @brief Backward-compatible name for an OpenCSD tree-session setup failure. */ +using OpenCsdItmSessionError = OpenCsdTreeSessionError; /** * @brief Owns one fully wired OpenCSD ITM callback decoder. @@ -84,25 +52,13 @@ class OpenCsdSessionValidation final { */ class OpenCsdItmSession final : public OpenCsdItmSessionInterface { public: - /** @brief Supplies the OpenCSD decoder registry to a session. */ - using DecoderRegistryProvider = OcsdLibDcdRegister* (*)(); - /** - * @brief Creates and connects an OpenCSD ITM decoder session. + * @brief Creates and connects an OpenCSD SINGLE-tree ITM decoder session. * @param collector Callback target for decoded packets and elements. * @param errorController Callback target for OpenCSD errors. * @throws OpenCsdItmSessionError If external session setup fails. */ OpenCsdItmSession(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController); - /** - * @brief Creates a session with an injectable decoder-registry provider. - * @param collector Callback target for decoded packets and elements. - * @param errorController Callback target for OpenCSD errors. - * @param registryProvider Provider used to retrieve the decoder registry. - * @throws OpenCsdItmSessionError If external session setup fails. - */ - OpenCsdItmSession(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController, - DecoderRegistryProvider registryProvider); /** @brief Disconnects and destroys the OpenCSD session without throwing. */ ~OpenCsdItmSession() noexcept; @@ -122,21 +78,8 @@ class OpenCsdItmSession final : public OpenCsdItmSessionInterface { ocsd_datapath_resp_t endOfTrace() override; private: - /** @brief Destroys an OpenCSD decoder component through its owning manager. */ - struct DecoderDeleter { - IDecoderMngr* manager = nullptr; - /** @brief Destroys a component through the manager that created it. */ - void operator()(TraceComponent* component) const noexcept; - }; - - /** @brief Creates the ITM decoder and attaches callbacks and input interfaces. */ - void createDecoder(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController, - DecoderRegistryProvider registryProvider); - ITMConfig m_config; - IDecoderMngr* m_manager = nullptr; - std::unique_ptr m_component{nullptr, DecoderDeleter{}}; - ITrcDataIn* m_input = nullptr; + OpenCsdTreeSession m_treeSession; }; -#endif // CTRACE_SRC_DECODE_OPENCSDITMSESSION_H +#endif // CTRACE_SRC_DECODE_OPENCSDITMSESSION_H diff --git a/tools/ctrace/src/decode/OpenCsdTreeSession.cpp b/tools/ctrace/src/decode/OpenCsdTreeSession.cpp new file mode 100644 index 000000000..d99286bf9 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdTreeSession.cpp @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdTreeSession.h" + +#include "OpenCsdErrorController.h" +#include "common/ocsd_dcd_mngr_i.h" +#include "common/ocsd_dcd_tree.h" +#include "common/ocsd_dcd_tree_elem.h" +#include "common/trc_component.h" +#include "interfaces/trc_abs_typed_base_i.h" +#include "interfaces/trc_error_log_i.h" +#include "interfaces/trc_gen_elem_in_i.h" + +#include +#include + +namespace { + +// DecodeTree keeps both the alternate logger and its live-tree registry in +// process-global state. This lease serializes every tree owned by ctrace. +std::atomic_bool ctraceOwnsProcessGlobalDecodeTreeState{false}; + +} // namespace + +/** @brief Holds exclusive use of OpenCSD's process-global tree and logger state. */ +class OpenCsdTreeSession::LoggerLease final { +public: + /** @brief Acquires exclusive tree use and installs the supplied logger. */ + explicit LoggerLease(ITraceErrorLog& errorLogger) + { + bool expected = false; + if (!ctraceOwnsProcessGlobalDecodeTreeState.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) { + throw OpenCsdTreeSessionError("another OpenCSD DecodeTree session is already active"); + } + m_previousLogger = DecodeTree::getCurrentErrorLogI(); + DecodeTree::setAlternateErrorLogger(&errorLogger); + } + + /** @brief Restores the previous logger and releases exclusive tree use. */ + ~LoggerLease() noexcept + { + DecodeTree::setAlternateErrorLogger(m_previousLogger); + ctraceOwnsProcessGlobalDecodeTreeState.store(false, std::memory_order_release); + } + + /** @brief Disables copying because the lease has unique process ownership. */ + LoggerLease(const LoggerLease&) = delete; + /** @brief Disables copy assignment because the lease has unique process ownership. */ + LoggerLease& operator=(const LoggerLease&) = delete; + +private: + ITraceErrorLog* m_previousLogger = nullptr; +}; + +void OpenCsdSessionValidation::requireObject(const void* object, const char* message) +{ + if (object == nullptr) { + throw OpenCsdTreeSessionError(message); + } +} + +void OpenCsdSessionValidation::requireSuccess(ocsd_err_t error, const char* message) +{ + if (error != OCSD_OK) { + throw OpenCsdTreeSessionError(OpenCsdErrorController::describeApiError(error, message)); + } +} + +void OpenCsdTreeSession::TreeDeleter::operator()(DecodeTree* tree) const noexcept +{ + destroy(tree); +} + +OpenCsdTreeSession::TreeLifecycle OpenCsdTreeSession::defaultLifecycle() +{ + return { + [](ocsd_dcd_tree_src_t sourceType, std::uint32_t formatterFlags) { + return DecodeTree::CreateDecodeTree(sourceType, formatterFlags); + }, + [](DecodeTree* tree) { DecodeTree::DestroyDecodeTree(tree); }, + }; +} + +OpenCsdTreeSession::OpenCsdTreeSession(ocsd_dcd_tree_src_t sourceType, std::uint32_t formatterFlags, + ITraceErrorLog& errorLogger, ITrcGenElemIn& elementOutput) + : OpenCsdTreeSession(sourceType, formatterFlags, errorLogger, elementOutput, defaultLifecycle()) +{ +} + +OpenCsdTreeSession::OpenCsdTreeSession(ocsd_dcd_tree_src_t sourceType, std::uint32_t formatterFlags, + ITraceErrorLog& errorLogger, ITrcGenElemIn& elementOutput, + const TreeLifecycle& lifecycle) + : m_errorLogger(errorLogger), + m_loggerLease(std::make_unique(errorLogger)), + m_tree(nullptr, TreeDeleter{lifecycle.destroy}) +{ + if (!lifecycle.create) { + throw OpenCsdTreeSessionError("OpenCSD DecodeTree creator is not configured"); + } + if (!lifecycle.destroy) { + throw OpenCsdTreeSessionError("OpenCSD DecodeTree destroyer is not configured"); + } + m_tree.reset(lifecycle.create(sourceType, formatterFlags)); + OpenCsdSessionValidation::requireObject(m_tree.get(), "failed to create OpenCSD DecodeTree"); + m_tree->setGenTraceElemOutI(&elementOutput); +} + +OpenCsdTreeSession::~OpenCsdTreeSession() noexcept = default; + +void OpenCsdTreeSession::createDecoder(const std::string& decoderName, int createFlags, const CSConfig& config) +{ + OpenCsdSessionValidation::requireSuccess(m_tree->createDecoder(decoderName, createFlags, &config), + "failed to create OpenCSD decoder"); +} + +void OpenCsdTreeSession::attachDecoderCallbacks(std::uint8_t channel, ITrcTypedBase& packetMonitor) +{ + auto* element = m_tree->getDecoderElement(channel); + OpenCsdSessionValidation::requireObject(element, "OpenCSD decoder element is not initialized"); + auto* manager = element->getDecoderMngr(); + OpenCsdSessionValidation::requireObject(manager, "OpenCSD decoder manager is not initialized"); + auto* component = element->getDecoderHandle(); + OpenCsdSessionValidation::requireObject(component, "OpenCSD full decoder component is not initialized"); + auto* packetProcessor = component->getAssocComponent(); + OpenCsdSessionValidation::requireObject(packetProcessor, "OpenCSD associated packet processor is not initialized"); + + // DecodeTree::createDecoder already attaches the active alternate logger to + // the full decoder. OpenCSD does not propagate it to the associated packet + // processor, where ITM protocol errors originate. + OpenCsdSessionValidation::requireSuccess(manager->attachErrorLogger(packetProcessor, &m_errorLogger), + "failed to attach OpenCSD packet-processor error logger"); + OpenCsdSessionValidation::requireSuccess(manager->attachPktMonitor(component, &packetMonitor), + "failed to attach OpenCSD packet monitor"); +} + +ocsd_datapath_resp_t OpenCsdTreeSession::traceDataIn(ocsd_datapath_op_t operation, ocsd_trc_index_t index, + std::uint32_t size, const std::uint8_t* data, + std::uint32_t* processed) +{ + return m_tree->TraceDataIn(operation, index, size, data, processed); +} diff --git a/tools/ctrace/src/decode/OpenCsdTreeSession.h b/tools/ctrace/src/decode/OpenCsdTreeSession.h new file mode 100644 index 000000000..de99ac2c0 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdTreeSession.h @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_DECODE_OPENCSDTREESESSION_H +#define CTRACE_SRC_DECODE_OPENCSDTREESESSION_H + +#include "opencsd/ocsd_if_types.h" + +#include +#include +#include +#include +#include + +class CSConfig; +class DecodeTree; +class ITraceErrorLog; +class ITrcGenElemIn; +class ITrcTypedBase; + +/** @brief Reports an OpenCSD tree-session creation or API failure. */ +class OpenCsdTreeSessionError final : public std::runtime_error { +public: + /** @brief Inherits standard runtime-error construction. */ + using std::runtime_error::runtime_error; +}; + +/** @brief Validates pointers and results returned by OpenCSD session setup APIs. */ +class OpenCsdSessionValidation final { +public: + /** + * @brief Rejects a null OpenCSD API object with a session error. + * @param object Required external API object. + * @param message Failure text used when object is null. + * @throws OpenCsdTreeSessionError If object is null. + */ + static void requireObject(const void* object, const char* message); + /** + * @brief Rejects an unsuccessful OpenCSD API result with a session error. + * @param error OpenCSD result to validate. + * @param message Failure text used for an error result. + * @throws OpenCsdTreeSessionError If error does not report success. + */ + static void requireSuccess(ocsd_err_t error, const char* message); + +private: + /** @brief Prevents construction of this stateless validation utility. */ + OpenCsdSessionValidation() = delete; +}; + +/** + * @brief Owns one exclusively active OpenCSD DecodeTree and its global logger lease. + * + * Decoder feed and recovery policy remains outside this infrastructure wrapper. + */ +class OpenCsdTreeSession final { +public: + /** @brief Creates an OpenCSD tree. */ + using TreeCreator = std::function; + /** @brief Destroys a tree created by TreeCreator without throwing. */ + using TreeDestroyer = std::function; + + /** @brief Supplies DecodeTree construction operations for lifecycle tests. */ + struct TreeLifecycle { + TreeCreator create; + TreeDestroyer destroy; + }; + + /** + * @brief Creates a tree and installs its logger and generic-element output. + * @param sourceType OpenCSD root input type. + * @param formatterFlags OpenCSD frame-deformatter configuration flags. + * @param errorLogger Logger kept active for the complete tree lifetime. + * @param elementOutput Generic trace-element output shared by configured decoders. + * @throws OpenCsdTreeSessionError If the process already owns a live tree or setup fails. + */ + OpenCsdTreeSession(ocsd_dcd_tree_src_t sourceType, std::uint32_t formatterFlags, ITraceErrorLog& errorLogger, + ITrcGenElemIn& elementOutput); + /** + * @brief Creates a tree through injectable lifecycle operations. + * @param sourceType OpenCSD root input type. + * @param formatterFlags OpenCSD frame-deformatter configuration flags. + * @param errorLogger Logger kept active for the complete tree lifetime. + * @param elementOutput Generic trace-element output shared by configured decoders. + * @param lifecycle Tree creation and destruction operations. + * @throws OpenCsdTreeSessionError If the process already owns a live tree or setup fails. + */ + OpenCsdTreeSession(ocsd_dcd_tree_src_t sourceType, std::uint32_t formatterFlags, ITraceErrorLog& errorLogger, + ITrcGenElemIn& elementOutput, const TreeLifecycle& lifecycle); + /** @brief Destroys the tree before restoring the previous process-global logger. */ + ~OpenCsdTreeSession() noexcept; + + /** @brief Disables copying because a session owns process-global and external state. */ + OpenCsdTreeSession(const OpenCsdTreeSession&) = delete; + /** @brief Disables copy assignment because a session owns process-global and external state. */ + OpenCsdTreeSession& operator=(const OpenCsdTreeSession&) = delete; + + /** @brief Creates one protocol decoder in the tree. */ + void createDecoder(const std::string& decoderName, int createFlags, const CSConfig& config); + /** + * @brief Attaches explicit error and packet callbacks to one full decoder pair. + * @param channel OpenCSD transport channel used to resolve the decoder element. + * @param packetMonitor Raw protocol-packet monitor attached to the packet processor. + */ + void attachDecoderCallbacks(std::uint8_t channel, ITrcTypedBase& packetMonitor); + /** @brief Routes one data-path operation through the DecodeTree root. */ + ocsd_datapath_resp_t traceDataIn(ocsd_datapath_op_t operation, ocsd_trc_index_t index, std::uint32_t size, + const std::uint8_t* data, std::uint32_t* processed); + +private: + class LoggerLease; + + /** @brief Destroys a DecodeTree through its configured lifecycle operation. */ + struct TreeDeleter { + TreeDestroyer destroy; + /** @brief Destroys a non-null tree through the no-throw lifecycle contract. */ + void operator()(DecodeTree* tree) const noexcept; + }; + + /** @brief Returns production DecodeTree construction operations. */ + static TreeLifecycle defaultLifecycle(); + + // Declaration order is intentional: reverse destruction removes the tree + // before the global logger lease can restore its predecessor. + ITraceErrorLog& m_errorLogger; + std::unique_ptr m_loggerLease; + std::unique_ptr m_tree; +}; + +#endif // CTRACE_SRC_DECODE_OPENCSDTREESESSION_H diff --git a/tools/ctrace/test/unit/CMakeLists.txt b/tools/ctrace/test/unit/CMakeLists.txt index b2b44da38..c08ff6d76 100644 --- a/tools/ctrace/test/unit/CMakeLists.txt +++ b/tools/ctrace/test/unit/CMakeLists.txt @@ -35,6 +35,7 @@ add_executable(CtraceUnitTests src/decode/OpenCsdErrorControllerTests.cpp src/decode/OpenCsdItmDecoderTests.cpp src/decode/OpenCsdPacketCollectorTests.cpp + src/decode/OpenCsdTreeSessionTests.cpp src/diagnostics/DiagnosticsTests.cpp src/model/TraceSelectionTests.cpp src/output/OutputRequirementsTests.cpp diff --git a/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp b/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp index c45248640..ee9844a7d 100644 --- a/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp +++ b/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp @@ -255,6 +255,30 @@ TEST(CtraceUnitTests, testOpenCsdItmSessionAcceptsEmptyDataPathOperations) EXPECT_NE(errors.decide(session.endOfTrace()).action, OpenCsdErrorController::Action::Abort); } +TEST(CtraceUnitTests, testOpenCsdItmSessionUsesSingleChannelAndAssociatedErrorLogger) +{ + CollectingOpenCsdElementSink sink; + OpenCsdItmDecoder decoder({}, sink); + const std::uint8_t trace[]{ + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), 0x04U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('B'), + }; + + decoder.push(trace, sizeof(trace)); + EXPECT_EQ(decoder.finish().bytesIn, sizeof(trace)); + EXPECT_TRUE(sink.hasIssue(TraceIssueCode::OpenCsdInvalidPacketHeader)); + + bool foundSoftware = false; + for (const auto& element : sink.elements()) { + if (element.kind == OpenCsdTraceElement::Kind::Software) { + foundSoftware = true; + EXPECT_EQ(element.route, TraceRouteIdentity{}) + << "OpenCSD SINGLE channel 0 must retain the synthetic ctrace route"; + } + } + EXPECT_TRUE(foundSoftware); +} + TEST(CtraceUnitTests, testOpenCsdSessionValidationRejectsInvalidApiResults) { const std::uint32_t object = 1U; @@ -268,14 +292,3 @@ TEST(CtraceUnitTests, testOpenCsdSessionValidationRejectsInvalidApiResults) EXPECT_NE(message->find("OCSD_ERR_MEM"), std::string::npos); EXPECT_NE(message->find("decoder setup failed"), std::string::npos); } - -TEST(CtraceUnitTests, testOpenCsdItmSessionRejectsMissingDecoderRegistry) -{ - CollectingOpenCsdElementSink sink; - OpenCsdPacketCollector collector({}, sink); - OpenCsdErrorController errors; - const auto missingRegistry = []() -> OcsdLibDcdRegister* { return nullptr; }; - - EXPECT_THROW((void)OpenCsdItmSession(collector, errors, nullptr), OpenCsdItmSessionError); - EXPECT_THROW((void)OpenCsdItmSession(collector, errors, missingRegistry), OpenCsdItmSessionError); -} diff --git a/tools/ctrace/test/unit/src/decode/OpenCsdTreeSessionTests.cpp b/tools/ctrace/test/unit/src/decode/OpenCsdTreeSessionTests.cpp new file mode 100644 index 000000000..5f4b40eef --- /dev/null +++ b/tools/ctrace/test/unit/src/decode/OpenCsdTreeSessionTests.cpp @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdTestSupport.h" +#include "TestSupport.h" + +#include + +#include "OpenCsdErrorController.h" +#include "OpenCsdPacketCollector.h" +#include "OpenCsdTreeSession.h" +#include "common/ocsd_dcd_tree.h" +#include "common/ocsd_dcd_tree_elem.h" +#include "common/trc_component.h" +#include "opencsd/itm/trc_cmp_cfg_itm.h" +#include "opencsd/ocsd_if_types.h" + +#include +#include + +using OpenCsdTestSupport::CollectingOpenCsdElementSink; + +namespace { + +/** @brief Installs one foreign OpenCSD logger and restores its predecessor. */ +class ScopedAlternateLogger final { +public: + /** @brief Saves the current logger and installs the supplied replacement. */ + explicit ScopedAlternateLogger(ITraceErrorLog& logger) + : m_previous(DecodeTree::getCurrentErrorLogI()) + { + DecodeTree::setAlternateErrorLogger(&logger); + } + + /** @brief Restores the logger that preceded this scope. */ + ~ScopedAlternateLogger() + { + DecodeTree::setAlternateErrorLogger(m_previous); + } + + /** @brief Disables copying because this object owns a process-global scope. */ + ScopedAlternateLogger(const ScopedAlternateLogger&) = delete; + /** @brief Disables copy assignment because this object owns a process-global scope. */ + ScopedAlternateLogger& operator=(const ScopedAlternateLogger&) = delete; + +private: + ITraceErrorLog* m_previous; +}; + +/** @brief Creates a collector bound to a collecting sink for one tree test. */ +struct TreeTestContext { + CollectingOpenCsdElementSink sink; + OpenCsdPacketCollector collector{{}, sink}; + OpenCsdErrorController errors; +}; + +} // namespace + +TEST(CtraceUnitTests, testOpenCsdTreeSessionRestoresForeignLoggerAfterDestroyingTree) +{ + OpenCsdErrorController foreignLogger; + ScopedAlternateLogger foreignLoggerScope(foreignLogger); + TreeTestContext context; + bool createdWithSessionLogger = false; + bool destroyedWithSessionLogger = false; + bool fullDecoderUsesSessionLogger = false; + bool packetProcessorUsesSessionLogger = false; + bool destroyed = false; + ocsd_dcd_tree_src_t observedSourceType = OCSD_TRC_SRC_FRAME_FORMATTED; + std::uint32_t observedFormatterFlags = 1U; + + const OpenCsdTreeSession::TreeLifecycle lifecycle{ + [&](ocsd_dcd_tree_src_t sourceType, std::uint32_t formatterFlags) { + observedSourceType = sourceType; + observedFormatterFlags = formatterFlags; + createdWithSessionLogger = DecodeTree::getCurrentErrorLogI() == &context.errors; + return DecodeTree::CreateDecodeTree(sourceType, formatterFlags); + }, + [&](DecodeTree* tree) { + destroyedWithSessionLogger = DecodeTree::getCurrentErrorLogI() == &context.errors; + auto* element = tree->getDecoderElement(0U); + auto* fullDecoder = element == nullptr ? nullptr : element->getDecoderHandle(); + auto* packetProcessor = fullDecoder == nullptr ? nullptr : fullDecoder->getAssocComponent(); + fullDecoderUsesSessionLogger = + fullDecoder != nullptr && fullDecoder->getErrorLogAttachPt()->first() == &context.errors; + packetProcessorUsesSessionLogger = + packetProcessor != nullptr && packetProcessor->getErrorLogAttachPt()->first() == &context.errors; + DecodeTree::DestroyDecodeTree(tree); + destroyed = true; + }, + }; + + ITMConfig config; + config.setTraceID(0U); + { + OpenCsdTreeSession session(OCSD_TRC_SRC_SINGLE, 0U, context.errors, context.collector, lifecycle); + session.createDecoder(OCSD_BUILTIN_DCD_ITM, OCSD_CREATE_FLG_FULL_DECODER, config); + session.attachDecoderCallbacks(0U, context.collector); + EXPECT_EQ(DecodeTree::getCurrentErrorLogI(), &context.errors); + } + + EXPECT_TRUE(createdWithSessionLogger); + EXPECT_TRUE(destroyedWithSessionLogger); + EXPECT_TRUE(fullDecoderUsesSessionLogger); + EXPECT_TRUE(packetProcessorUsesSessionLogger); + EXPECT_TRUE(destroyed); + EXPECT_EQ(observedSourceType, OCSD_TRC_SRC_SINGLE); + EXPECT_EQ(observedFormatterFlags, 0U); + EXPECT_EQ(DecodeTree::getCurrentErrorLogI(), &foreignLogger); +} + +TEST(CtraceUnitTests, testOpenCsdTreeSessionRejectsOverlapAndReleasesLease) +{ + OpenCsdErrorController foreignLogger; + ScopedAlternateLogger foreignLoggerScope(foreignLogger); + TreeTestContext first; + TreeTestContext second; + + { + OpenCsdTreeSession active(OCSD_TRC_SRC_SINGLE, 0U, first.errors, first.collector); + EXPECT_THROW((void)OpenCsdTreeSession(OCSD_TRC_SRC_SINGLE, 0U, second.errors, second.collector), + OpenCsdTreeSessionError); + EXPECT_EQ(DecodeTree::getCurrentErrorLogI(), &first.errors); + } + + EXPECT_EQ(DecodeTree::getCurrentErrorLogI(), &foreignLogger); + EXPECT_NO_THROW((void)OpenCsdTreeSession(OCSD_TRC_SRC_SINGLE, 0U, second.errors, second.collector)); + EXPECT_EQ(DecodeTree::getCurrentErrorLogI(), &foreignLogger); +} + +TEST(CtraceUnitTests, testOpenCsdTreeSessionCleansUpConstructionFailures) +{ + OpenCsdErrorController foreignLogger; + ScopedAlternateLogger foreignLoggerScope(foreignLogger); + TreeTestContext context; + const auto destroyTree = [](DecodeTree* tree) { DecodeTree::DestroyDecodeTree(tree); }; + + const OpenCsdTreeSession::TreeLifecycle missingCreator{{}, destroyTree}; + EXPECT_THROW((void)OpenCsdTreeSession(OCSD_TRC_SRC_SINGLE, 0U, context.errors, context.collector, missingCreator), + OpenCsdTreeSessionError); + EXPECT_EQ(DecodeTree::getCurrentErrorLogI(), &foreignLogger); + + std::uint32_t missingDestroyerCreateCalls = 0U; + const auto createWithoutDestroyer = [&](ocsd_dcd_tree_src_t, std::uint32_t) -> DecodeTree* { + ++missingDestroyerCreateCalls; + return nullptr; + }; + const OpenCsdTreeSession::TreeLifecycle missingDestroyer{createWithoutDestroyer, {}}; + EXPECT_THROW((void)OpenCsdTreeSession(OCSD_TRC_SRC_SINGLE, 0U, context.errors, context.collector, missingDestroyer), + OpenCsdTreeSessionError); + EXPECT_EQ(missingDestroyerCreateCalls, 0U); + EXPECT_EQ(DecodeTree::getCurrentErrorLogI(), &foreignLogger); + + const OpenCsdTreeSession::TreeLifecycle failedCreate{ + [](ocsd_dcd_tree_src_t, std::uint32_t) -> DecodeTree* { return nullptr; }, destroyTree}; + EXPECT_THROW((void)OpenCsdTreeSession(OCSD_TRC_SRC_SINGLE, 0U, context.errors, context.collector, failedCreate), + OpenCsdTreeSessionError); + EXPECT_EQ(DecodeTree::getCurrentErrorLogI(), &foreignLogger); + + const OpenCsdTreeSession::TreeLifecycle throwingCreate{ + [](ocsd_dcd_tree_src_t, std::uint32_t) -> DecodeTree* { + throw OpenCsdTreeSessionError("synthetic DecodeTree creation failure"); + }, + destroyTree, + }; + EXPECT_THROW((void)OpenCsdTreeSession(OCSD_TRC_SRC_SINGLE, 0U, context.errors, context.collector, throwingCreate), + OpenCsdTreeSessionError); + EXPECT_EQ(DecodeTree::getCurrentErrorLogI(), &foreignLogger); + + EXPECT_NO_THROW((void)OpenCsdTreeSession(OCSD_TRC_SRC_SINGLE, 0U, context.errors, context.collector)); + EXPECT_EQ(DecodeTree::getCurrentErrorLogI(), &foreignLogger); +} + +TEST(CtraceUnitTests, testOpenCsdTreeSessionCleansUpPartialDecoderSetupFailure) +{ + OpenCsdErrorController foreignLogger; + ScopedAlternateLogger foreignLoggerScope(foreignLogger); + TreeTestContext context; + ITMConfig config; + std::uint32_t destroyCalls = 0U; + const auto createTree = [](ocsd_dcd_tree_src_t sourceType, std::uint32_t formatterFlags) { + return DecodeTree::CreateDecodeTree(sourceType, formatterFlags); + }; + const auto destroyTree = [&](DecodeTree* tree) { + ++destroyCalls; + DecodeTree::DestroyDecodeTree(tree); + }; + const OpenCsdTreeSession::TreeLifecycle lifecycle{createTree, destroyTree}; + + const auto setupInvalidDecoder = [&] { + OpenCsdTreeSession session(OCSD_TRC_SRC_SINGLE, 0U, context.errors, context.collector, lifecycle); + session.createDecoder("missing-decoder", OCSD_CREATE_FLG_FULL_DECODER, config); + }; + EXPECT_THROW(setupInvalidDecoder(), OpenCsdTreeSessionError); + EXPECT_EQ(destroyCalls, 1U); + EXPECT_EQ(DecodeTree::getCurrentErrorLogI(), &foreignLogger); + + EXPECT_NO_THROW((void)OpenCsdTreeSession(OCSD_TRC_SRC_SINGLE, 0U, context.errors, context.collector)); + EXPECT_EQ(DecodeTree::getCurrentErrorLogI(), &foreignLogger); +} From c52074324dc218b47d118a42005974fb9c6d6ad3 Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Thu, 10 Sep 2026 12:06:00 +0200 Subject: [PATCH 08/31] feat(ctrace): decode formatted CoreSight inputs --- .../ctrace/docs/multicore-multisource-plan.md | 10 +- tools/ctrace/src/CMakeLists.txt | 2 + tools/ctrace/src/control/FileDecodeJob.cpp | 60 ++- tools/ctrace/src/decode/DecodePipeline.cpp | 33 +- tools/ctrace/src/decode/DecodePipeline.h | 21 +- .../src/decode/OpenCsdFormattedItmSession.cpp | 243 +++++++++ .../src/decode/OpenCsdFormattedItmSession.h | 132 +++++ tools/ctrace/src/decode/OpenCsdItmDecoder.cpp | 196 ++++++- tools/ctrace/src/decode/OpenCsdItmDecoder.h | 31 +- .../src/decode/OpenCsdPacketCollector.cpp | 247 ++++++--- .../src/decode/OpenCsdPacketCollector.h | 63 ++- .../ctrace/src/decode/OpenCsdTreeSession.cpp | 44 +- tools/ctrace/src/decode/OpenCsdTreeSession.h | 12 + .../src/diagnostics/TraceIssueReporter.cpp | 2 + tools/ctrace/src/model/TraceEvent.h | 1 + .../test/integration/src/CtraceIntegTests.cpp | 184 ++++++- tools/ctrace/test/unit/CMakeLists.txt | 2 + .../src/control/TraceDirectoryJobTests.cpp | 44 +- .../unit/src/decode/DecodePipelineTests.cpp | 26 +- .../OpenCsdFormattedItmSessionTests.cpp | 494 ++++++++++++++++++ .../src/decode/OpenCsdItmDecoderTests.cpp | 306 ++++++++++- .../decode/OpenCsdPacketCollectorTests.cpp | 142 ++++- .../src/decode/OpenCsdTreeSessionTests.cpp | 2 +- .../unit/src/diagnostics/DiagnosticsTests.cpp | 16 +- .../unit/support/FormattedTraceTestSupport.h | 166 ++++++ 25 files changed, 2300 insertions(+), 179 deletions(-) create mode 100644 tools/ctrace/src/decode/OpenCsdFormattedItmSession.cpp create mode 100644 tools/ctrace/src/decode/OpenCsdFormattedItmSession.h create mode 100644 tools/ctrace/test/unit/src/decode/OpenCsdFormattedItmSessionTests.cpp create mode 100644 tools/ctrace/test/unit/support/FormattedTraceTestSupport.h diff --git a/tools/ctrace/docs/multicore-multisource-plan.md b/tools/ctrace/docs/multicore-multisource-plan.md index 99e764d1c..a920641fe 100644 --- a/tools/ctrace/docs/multicore-multisource-plan.md +++ b/tools/ctrace/docs/multicore-multisource-plan.md @@ -542,8 +542,8 @@ Phase 0 -> Phase 1 -> Phase 2 -> Phase 3 -> Phase 4 | 4 | CTF descriptors and metadata model | Complete | | 5 | Multi-stream CTF bundle and Trace Compass policy | Complete | | 6 | DecodeTree `SINGLE` migration | Complete | -| 7 | Clean formatted decoding and TB integration | Next | -| 8 | Route-local recovery and error isolation | Pending | +| 7 | Clean formatted decoding and TB integration | Complete | +| 8 | Route-local recovery and error isolation | Next | | 9 | Consumer validation, documentation, and final hardening | Pending | Update this table only after the corresponding exit criterion and common gate pass. @@ -742,8 +742,10 @@ Purpose: add the memory-aligned formatted path after the semantic and output lay formatter IDs. 3. Keep ID `0` silent as NULL/padding. Diagnose each unsupported normal source ID once and skip it without guessing a protocol; preserve supported routes and outputs. -4. Preserve the architectural Trace Bus ID through semantic events, CSV filtering/stream values, CTF stream-class - mapping, diagnostics, and Trace Compass when XML is valid. +4. Preserve the architectural Trace Bus ID through successfully decoded semantic events, CSV filtering/stream + values, CTF stream-class mapping, unsupported-source diagnostics, and Trace Compass when XML is valid. Keep + OpenCSD protocol/deformatter diagnostics input-wide with deterministic provisional attribution until Phase 8 + retains and resolves the OpenCSD error channel. 5. Enable the reconstructed TB fixture end to end and add focused clean formatted fixtures for single-source, boundary-ID, unsupported-ID, and empty-input behavior. 6. Treat any formatted protocol or deformatter error as input-fatal in this phase. Route-local recovery is enabled diff --git a/tools/ctrace/src/CMakeLists.txt b/tools/ctrace/src/CMakeLists.txt index 5043f4872..abb26af63 100644 --- a/tools/ctrace/src/CMakeLists.txt +++ b/tools/ctrace/src/CMakeLists.txt @@ -29,6 +29,7 @@ set(CTRACE_DECODE_HEADER_FILES decode/DecodePipeline.h decode/DwtPacketDecoder.h decode/OpenCsdErrorController.h + decode/OpenCsdFormattedItmSession.h decode/OpenCsdItmDecoder.h decode/OpenCsdItmSession.h decode/OpenCsdPacketCollector.h @@ -131,6 +132,7 @@ add_library(ctrace-decode STATIC decode/DecodePipeline.cpp decode/DwtPacketDecoder.cpp decode/OpenCsdErrorController.cpp + decode/OpenCsdFormattedItmSession.cpp decode/OpenCsdItmDecoder.cpp decode/OpenCsdItmSession.cpp decode/OpenCsdPacketCollector.cpp diff --git a/tools/ctrace/src/control/FileDecodeJob.cpp b/tools/ctrace/src/control/FileDecodeJob.cpp index e5772e092..128dc6762 100644 --- a/tools/ctrace/src/control/FileDecodeJob.cpp +++ b/tools/ctrace/src/control/FileDecodeJob.cpp @@ -94,11 +94,22 @@ static std::string decodeSummary(const DecodeResult& decode, std::chrono::steady return out.str(); } -/** @brief Resolves the one semantic route used by the current SINGLE frontend. */ -static CortexMDecodeRoute decodeRoute(const CtraceRunMeta& ctraceRunMeta) +/** @brief Converts normalized trace-run routes into semantic decoder routes. */ +static std::vector decodeRoutes(const CtraceRunMeta& ctraceRunMeta) { - const auto& route = ctraceRunMeta.routes().front(); - return {route.identity, route.timestampPrescaler}; + std::vector result; + result.reserve(ctraceRunMeta.routes().size()); + for (const auto& route : ctraceRunMeta.routes()) { + result.push_back({route.identity, route.timestampPrescaler}); + } + return result; +} + +/** @brief Maps the preflighted raw-input contract to the decode frontend. */ +static OpenCsdItmInputMode decodeInputMode(const TraceRunInputDescriptor& input) +{ + return input.format() == TraceRunFormat::Formatted ? OpenCsdItmInputMode::CoreSightFormatted + : OpenCsdItmInputMode::Single; } /** @brief Indexes route-local ITM enable masks without using a transport sentinel. */ @@ -160,12 +171,9 @@ FileDecodeJob::FileDecodeJob(CliOptions options, TraceRunInputDescriptor input, void FileDecodeJob::run() { - if (m_input.format() == TraceRunFormat::Formatted) { - throw std::runtime_error("formatted trace input is not enabled yet"); - } - const auto& ctraceRunMeta = m_input.metadata(); - const auto route = decodeRoute(ctraceRunMeta); + const auto routes = decodeRoutes(ctraceRunMeta); + const auto inputMode = decodeInputMode(m_input); auto outputPlan = planTraceOutputs(outputRequest(m_options), m_input.path(), ctraceRunMeta, m_diagnostics); if (outputPlan.hasRequestedOutputs() && !outputPlan.hasEnabledOutputs()) { return; @@ -183,11 +191,21 @@ void FileDecodeJob::run() DecodeConsumers consumers(std::move(outputs), m_diagnostics, ctraceRunMeta.itmEnableMask(), itmEnableMasks(ctraceRunMeta)); - m_diagnostics.report({ - DiagnosticSink::Severity::Info, - "using timestamp prescaler", - {{"value", std::to_string(route.timestampPrescaler)}}, - }); + for (const auto& route : ctraceRunMeta.routes()) { + std::vector> context; + context.emplace_back("value", std::to_string(route.timestampPrescaler)); + if (route.identity.traceBusId.has_value()) { + context.emplace_back("stream", std::to_string(*route.identity.traceBusId)); + } + if (route.processorName.has_value()) { + context.emplace_back("pname", *route.processorName); + } + m_diagnostics.report({ + DiagnosticSink::Severity::Info, + "using timestamp prescaler", + std::move(context), + }); + } const auto decodeStart = std::chrono::steady_clock::now(); DecodeResult decode; bool decoderFatal = false; @@ -195,9 +213,19 @@ void FileDecodeJob::run() RawFileReader input(m_input.path(), m_input.stream()); std::unique_ptr pipeline; if (m_sessionFactory) { - pipeline = std::make_unique(route, consumers, m_sessionFactory); + pipeline = std::make_unique(routes, inputMode, consumers, m_sessionFactory); } else { - pipeline = std::make_unique(route, consumers); + pipeline = std::make_unique(routes, inputMode, consumers, + [&](std::uint8_t traceBusId, std::uint64_t sourceOffset) { + m_diagnostics.report({ + DiagnosticSink::Severity::Warning, + "skipping unsupported formatted CoreSight trace source", + { + {"stream", std::to_string(traceBusId)}, + {"rawOffset", std::to_string(sourceOffset)}, + }, + }); + }); } while (true) { const auto read = input.read(); diff --git a/tools/ctrace/src/decode/DecodePipeline.cpp b/tools/ctrace/src/decode/DecodePipeline.cpp index 076ce34f4..588e093c7 100644 --- a/tools/ctrace/src/decode/DecodePipeline.cpp +++ b/tools/ctrace/src/decode/DecodePipeline.cpp @@ -15,17 +15,42 @@ #include #include #include +#include DecodePipeline::DecodePipeline(CortexMDecodeRoute route, TraceEventSink& eventSink) - : m_streamDecoder({route}, eventSink), - m_decoder(std::move(route.identity), m_streamDecoder) + : DecodePipeline(std::vector{std::move(route)}, OpenCsdItmInputMode::Single, eventSink) { } DecodePipeline::DecodePipeline(CortexMDecodeRoute route, TraceEventSink& eventSink, const OpenCsdItmSessionFactory& sessionFactory) - : m_streamDecoder({route}, eventSink), - m_decoder(std::move(route.identity), m_streamDecoder, sessionFactory) + : DecodePipeline(std::vector{std::move(route)}, OpenCsdItmInputMode::Single, eventSink, + sessionFactory) +{ +} + +/** @brief Extracts decoder identities while retaining post-decoder route configuration. */ +static std::vector routeIdentities(const std::vector& routes) +{ + std::vector identities; + identities.reserve(routes.size()); + for (const auto& route : routes) { + identities.push_back(route.identity); + } + return identities; +} + +DecodePipeline::DecodePipeline(std::vector routes, OpenCsdItmInputMode inputMode, + TraceEventSink& eventSink, OpenCsdUnsupportedTraceIdObserver unsupportedTraceIdSink) + : m_streamDecoder(routes, eventSink), + m_decoder(routeIdentities(routes), inputMode, m_streamDecoder, std::move(unsupportedTraceIdSink)) +{ +} + +DecodePipeline::DecodePipeline(std::vector routes, OpenCsdItmInputMode inputMode, + TraceEventSink& eventSink, const OpenCsdItmSessionFactory& sessionFactory) + : m_streamDecoder(routes, eventSink), + m_decoder(routeIdentities(routes), inputMode, m_streamDecoder, sessionFactory) { } diff --git a/tools/ctrace/src/decode/DecodePipeline.h b/tools/ctrace/src/decode/DecodePipeline.h index abb6575f3..1276b73b0 100644 --- a/tools/ctrace/src/decode/DecodePipeline.h +++ b/tools/ctrace/src/decode/DecodePipeline.h @@ -14,6 +14,7 @@ #include #include +#include /** @brief Provides a non-owning view of one raw trace byte chunk. */ struct RawByteView { @@ -49,6 +50,24 @@ class DecodePipeline final { * @param sessionFactory Factory used to create the OpenCSD session. */ DecodePipeline(CortexMDecodeRoute route, TraceEventSink& eventSink, const OpenCsdItmSessionFactory& sessionFactory); + /** + * @brief Creates a pipeline for normalized SINGLE or formatted routes. + * @param routes Route identities and timestamp prescalers used by the decoders. + * @param inputMode Raw transport presented to OpenCSD. + * @param eventSink Sink receiving decoded events synchronously. + * @param unsupportedTraceIdSink Observer for unconfigured normal formatted IDs. + */ + DecodePipeline(std::vector routes, OpenCsdItmInputMode inputMode, TraceEventSink& eventSink, + OpenCsdUnsupportedTraceIdObserver unsupportedTraceIdSink = {}); + /** + * @brief Creates a configured pipeline with an injected OpenCSD session. + * @param routes Route identities and timestamp prescalers used by the decoders. + * @param inputMode Decoder policy mode applied around the injected session. + * @param eventSink Sink receiving decoded events synchronously. + * @param sessionFactory Factory used to create the OpenCSD session. + */ + DecodePipeline(std::vector routes, OpenCsdItmInputMode inputMode, TraceEventSink& eventSink, + const OpenCsdItmSessionFactory& sessionFactory); /** * @brief Pushes the next contiguous chunk of raw trace bytes. @@ -68,4 +87,4 @@ class DecodePipeline final { OpenCsdItmDecoder m_decoder; }; -#endif // CTRACE_SRC_DECODE_DECODEPIPELINE_H +#endif // CTRACE_SRC_DECODE_DECODEPIPELINE_H diff --git a/tools/ctrace/src/decode/OpenCsdFormattedItmSession.cpp b/tools/ctrace/src/decode/OpenCsdFormattedItmSession.cpp new file mode 100644 index 000000000..070066e9c --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdFormattedItmSession.cpp @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdFormattedItmSession.h" + +#include "OpenCsdTreeSession.h" +#include "TraceStreamId.h" +#include "common/trc_gen_elem.h" +#include "interfaces/trc_data_rawframe_in_i.h" +#include "interfaces/trc_error_log_i.h" +#include "interfaces/trc_gen_elem_in_i.h" +#include "interfaces/trc_pkt_raw_in_i.h" +#include "opencsd/itm/trc_cmp_cfg_itm.h" +#include "opencsd/itm/trc_pkt_elem_itm.h" +#include "opencsd/itm/trc_pkt_types_itm.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint32_t kItmTcrSwoEnable = 1U << 4U; +constexpr ocsd_itm_cfg kItmConfig{kItmTcrSwoEnable}; +constexpr std::uint32_t kFormattedTreeFlags = OCSD_DFRMTR_FRAME_MEM_ALIGN | OCSD_DFRMTR_UNPACKED_RAW_OUT; + +} // namespace + +/** @brief Retains the first exception raised while OpenCSD owns the call stack. */ +class OpenCsdFormattedItmSession::CallbackErrorState final { +public: + /** @brief Captures the active exception without allowing another exception to escape. */ + void captureCurrent() noexcept + { + if (!m_error) { + m_error = std::current_exception(); + } + } + + /** @brief Rethrows and clears the first captured callback exception. */ + void rethrow() + { + if (!m_error) { + return; + } + auto error = std::exchange(m_error, nullptr); + std::rethrow_exception(error); + } + +private: + std::exception_ptr m_error; +}; + +/** @brief Prevents generic-output exceptions from unwinding through OpenCSD. */ +class OpenCsdFormattedItmSession::GenericElementAdapter final : public ITrcGenElemIn { +public: + /** @brief Binds the external generic-element output and shared error state. */ + GenericElementAdapter(ITrcGenElemIn& output, CallbackErrorState& errors) + : m_output(output), + m_errors(errors) + { + } + + /** @brief Forwards one element while preserving its OpenCSD channel ID. */ + ocsd_datapath_resp_t TraceElemIn(ocsd_trc_index_t index, std::uint8_t channel, + const OcsdTraceElement& element) noexcept override + { + try { + return m_output.TraceElemIn(index, channel, element); + } catch (...) { + m_errors.captureCurrent(); + return OCSD_RESP_FATAL_SYS_ERR; + } + } + +private: + ITrcGenElemIn& m_output; + CallbackErrorState& m_errors; +}; + +/** @brief Restores route identity omitted by OpenCSD's raw-packet monitor interface. */ +class OpenCsdFormattedItmSession::RoutePacketMonitor final : public IPktRawDataMon { +public: + /** @brief Binds one decoder route to the shared packet sink. */ + RoutePacketMonitor(TraceRouteIdentity route, OpenCsdFormattedItmPacketSink& sink, CallbackErrorState& errors) + : m_route(std::move(route)), + m_sink(sink), + m_errors(errors) + { + } + + /** @brief Forwards one packet with its bound route without throwing through OpenCSD. */ + void RawPacketDataMon(ocsd_datapath_op_t operation, ocsd_trc_index_t index, const ItmTrcPacket* packet, + std::uint32_t size, const std::uint8_t* data) noexcept override + { + try { + m_sink.rawPacketForRoute(m_route, operation, index, packet, size, data); + } catch (...) { + m_errors.captureCurrent(); + } + } + +private: + TraceRouteIdentity m_route; + OpenCsdFormattedItmPacketSink& m_sink; + CallbackErrorState& m_errors; +}; + +/** @brief Observes deformatter IDs that have no configured protocol decoder. */ +class OpenCsdFormattedItmSession::RawFrameMonitor final : public ITrcRawFrameIn { +public: + /** @brief Indexes all configured normal source IDs. */ + explicit RawFrameMonitor(const std::vector& routes) + { + for (const auto& route : routes) { + m_configured[*route.traceBusId] = true; + } + } + + /** @brief Records unsupported or unassigned deformatter output without external calls. */ + ocsd_err_t TraceRawFrameIn(ocsd_datapath_op_t operation, ocsd_trc_index_t index, ocsd_rawframe_elem_t frameElement, + int dataSize, const std::uint8_t*, std::uint8_t traceId) noexcept override + { + if (operation != OCSD_OP_DATA || frameElement != OCSD_FRM_ID_DATA || dataSize <= 0) { + return OCSD_OK; + } + if (traceId == OCSD_BAD_CS_SRC_ID) { + if (!m_unassignedIndex.has_value()) { + m_unassignedIndex = index; + } + return OCSD_OK; + } + if (!OCSD_IS_VALID_CS_SRC_ID(traceId) || m_configured[traceId] || m_reported[traceId] || + m_pendingIndex[traceId].has_value()) { + return OCSD_OK; + } + m_pendingIndex[traceId] = index; + return OCSD_OK; + } + + /** @brief Publishes new unsupported IDs and rejects data with no preceding source ID. */ + void completeOperation(const OpenCsdUnsupportedTraceIdSink& unsupportedTraceIdSink) + { + if (m_unassignedIndex.has_value()) { + const auto index = *std::exchange(m_unassignedIndex, std::nullopt); + throw OpenCsdFormattedInputError("formatted trace data has no source ID", static_cast(index)); + } + for (std::uint8_t traceId = CoreSight::kMinAtbTraceId; traceId <= CoreSight::kMaxAtbTraceId; ++traceId) { + if (!m_pendingIndex[traceId].has_value()) { + continue; + } + const auto index = *std::exchange(m_pendingIndex[traceId], std::nullopt); + m_reported[traceId] = true; + if (unsupportedTraceIdSink) { + unsupportedTraceIdSink(traceId, index); + } + } + } + +private: + std::array m_configured{}; + std::array m_reported{}; + std::array, 128U> m_pendingIndex{}; + std::optional m_unassignedIndex; +}; + +std::vector OpenCsdFormattedItmSession::validateRoutes(std::vector routes) +{ + if (routes.empty()) { + throw OpenCsdItmSessionError("formatted OpenCSD ITM session requires at least one route"); + } + std::array configured{}; + for (const auto& route : routes) { + if (!route.traceBusId.has_value() || !CoreSight::isAtbTraceId(*route.traceBusId)) { + throw OpenCsdItmSessionError("formatted OpenCSD ITM route requires a Trace Bus ID between 1 and 111"); + } + if (configured[*route.traceBusId]) { + throw OpenCsdItmSessionError("formatted OpenCSD ITM routes require unique Trace Bus IDs"); + } + configured[*route.traceBusId] = true; + } + return routes; +} + +OpenCsdFormattedItmSession::OpenCsdFormattedItmSession(std::vector routes, + ITrcGenElemIn& elementOutput, ITraceErrorLog& errorLogger, + OpenCsdFormattedItmPacketSink& packetSink, + OpenCsdUnsupportedTraceIdSink unsupportedTraceIdSink) + : m_routes(validateRoutes(std::move(routes))), + m_unsupportedTraceIdSink(std::move(unsupportedTraceIdSink)), + m_callbackErrors(std::make_unique()), + m_elementAdapter(std::make_unique(elementOutput, *m_callbackErrors)), + m_rawFrameMonitor(std::make_unique(m_routes)), + m_treeSession(OCSD_TRC_SRC_FRAME_FORMATTED, kFormattedTreeFlags, errorLogger, *m_elementAdapter) +{ + m_treeSession.attachRawFrameMonitor(*m_rawFrameMonitor); + m_packetMonitors.reserve(m_routes.size()); + for (const auto& route : m_routes) { + m_packetMonitors.push_back(std::make_unique(route, packetSink, *m_callbackErrors)); + ITMConfig config(&kItmConfig); + config.setTraceID(*route.traceBusId); + m_treeSession.createDecoder(OCSD_BUILTIN_DCD_ITM, OCSD_CREATE_FLG_FULL_DECODER, config); + m_treeSession.attachDecoderCallbacks(*route.traceBusId, *m_packetMonitors.back()); + } +} + +OpenCsdFormattedItmSession::~OpenCsdFormattedItmSession() noexcept = default; + +ocsd_datapath_resp_t OpenCsdFormattedItmSession::pushData(ocsd_trc_index_t index, std::uint32_t size, + const std::uint8_t* data, std::uint32_t& processed) +{ + return completeOperation(m_treeSession.traceDataIn(OCSD_OP_DATA, index, size, data, &processed)); +} + +ocsd_datapath_resp_t OpenCsdFormattedItmSession::flush() +{ + return completeOperation(m_treeSession.traceDataIn(OCSD_OP_FLUSH, 0, 0, nullptr, nullptr)); +} + +ocsd_datapath_resp_t OpenCsdFormattedItmSession::reset() +{ + return completeOperation(m_treeSession.traceDataIn(OCSD_OP_RESET, 0, 0, nullptr, nullptr)); +} + +ocsd_datapath_resp_t OpenCsdFormattedItmSession::endOfTrace() +{ + return completeOperation(m_treeSession.traceDataIn(OCSD_OP_EOT, 0, 0, nullptr, nullptr)); +} + +ocsd_datapath_resp_t OpenCsdFormattedItmSession::completeOperation(ocsd_datapath_resp_t response) +{ + m_callbackErrors->rethrow(); + m_rawFrameMonitor->completeOperation(m_unsupportedTraceIdSink); + return response; +} diff --git a/tools/ctrace/src/decode/OpenCsdFormattedItmSession.h b/tools/ctrace/src/decode/OpenCsdFormattedItmSession.h new file mode 100644 index 000000000..0d894c3c9 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdFormattedItmSession.h @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_DECODE_OPENCSDFORMATTEDITMSESSION_H +#define CTRACE_SRC_DECODE_OPENCSDFORMATTEDITMSESSION_H + +#include "OpenCsdItmSession.h" +#include "TraceRoute.h" +#include "opencsd/ocsd_if_types.h" + +#include +#include +#include +#include +#include +#include + +class ITraceErrorLog; +class ITrcGenElemIn; +class ItmTrcPacket; + +/** @brief Receives raw ITM packets with the normalized route omitted by OpenCSD's monitor API. */ +class OpenCsdFormattedItmPacketSink { +public: + /** @brief Destroys a routed packet sink through its interface. */ + virtual ~OpenCsdFormattedItmPacketSink() = default; + + /** + * @brief Receives one raw ITM packet callback for its configured route. + * @param route Normalized route bound to the decoder producing the callback. + * @param operation OpenCSD data-path operation. + * @param index Raw formatted-input offset associated with the packet. + * @param packet Expanded ITM packet, or null for a control operation. + * @param size Number of raw protocol bytes associated with the packet. + * @param data Raw protocol bytes, or null when no bytes are supplied. + */ + virtual void rawPacketForRoute(const TraceRouteIdentity& route, ocsd_datapath_op_t operation, ocsd_trc_index_t index, + const ItmTrcPacket* packet, std::uint32_t size, const std::uint8_t* data) = 0; +}; + +/** @brief Reports one normal formatter source ID for which no protocol route is configured. */ +using OpenCsdUnsupportedTraceIdSink = std::function; + +/** @brief Reports malformed formatted input detected outside OpenCSD's error callback API. */ +class OpenCsdFormattedInputError final : public std::runtime_error { +public: + /** + * @brief Creates an input error at an exact raw formatter offset. + * @param message Human-readable failure description without an offset suffix. + * @param sourceOffset Raw formatted-input offset at which the failure was detected. + */ + OpenCsdFormattedInputError(const std::string& message, std::uint64_t sourceOffset) + : std::runtime_error(message + " at raw input offset " + std::to_string(sourceOffset)), + m_sourceOffset(sourceOffset) + { + } + + /** @brief Returns the raw formatted-input offset associated with the failure. */ + std::uint64_t sourceOffset() const noexcept + { + return m_sourceOffset; + } + +private: + std::uint64_t m_sourceOffset = 0U; +}; + +/** + * @brief Owns one memory-aligned formatted OpenCSD tree with routed ITM decoders. + * + * Feed, response, transaction, and recovery policy remains outside this low-level + * session. Callback exceptions are never allowed to unwind through OpenCSD. + */ +class OpenCsdFormattedItmSession final : public OpenCsdItmSessionInterface { +public: + /** + * @brief Creates one full ITM decoder for every supplied formatted route. + * @param routes Normalized routes, each with one unique Trace Bus ID in 1..111. + * @param elementOutput Tree-wide generic-element output receiving OpenCSD channel IDs. + * @param errorLogger Error logger kept active for the complete tree lifetime. + * @param packetSink Routed raw-packet callback target shared by all decoder adapters. + * @param unsupportedTraceIdSink Optional callback invoked once per observed unconfigured normal ID. + * @throws OpenCsdItmSessionError If route validation or external session setup fails. + */ + OpenCsdFormattedItmSession(std::vector routes, ITrcGenElemIn& elementOutput, + ITraceErrorLog& errorLogger, OpenCsdFormattedItmPacketSink& packetSink, + OpenCsdUnsupportedTraceIdSink unsupportedTraceIdSink = {}); + /** @brief Disconnects callbacks and destroys the formatted DecodeTree without throwing. */ + ~OpenCsdFormattedItmSession() noexcept; + + /** @brief Disables copying because a session owns external decoder state. */ + OpenCsdFormattedItmSession(const OpenCsdFormattedItmSession&) = delete; + /** @brief Disables copy assignment because a session owns external decoder state. */ + OpenCsdFormattedItmSession& operator=(const OpenCsdFormattedItmSession&) = delete; + + /** @brief Pushes complete memory-aligned formatter frames into OpenCSD. */ + ocsd_datapath_resp_t pushData(ocsd_trc_index_t index, std::uint32_t size, const std::uint8_t* data, + std::uint32_t& processed) override; + /** @brief Flushes pending decoder and deformatter work. */ + ocsd_datapath_resp_t flush() override; + /** @brief Resets the complete formatted tree. */ + ocsd_datapath_resp_t reset() override; + /** @brief Signals end of trace to every configured decoder. */ + ocsd_datapath_resp_t endOfTrace() override; + +private: + class CallbackErrorState; + class GenericElementAdapter; + class RoutePacketMonitor; + class RawFrameMonitor; + + /** @brief Validates route IDs before any OpenCSD process-global state is acquired. */ + static std::vector validateRoutes(std::vector routes); + /** @brief Rethrows callback failures and publishes observations after one tree operation. */ + ocsd_datapath_resp_t completeOperation(ocsd_datapath_resp_t response); + + // Declaration order is intentional: the tree is destroyed before every + // callback object whose address was installed in it. + std::vector m_routes; + OpenCsdUnsupportedTraceIdSink m_unsupportedTraceIdSink; + std::unique_ptr m_callbackErrors; + std::unique_ptr m_elementAdapter; + std::vector> m_packetMonitors; + std::unique_ptr m_rawFrameMonitor; + OpenCsdTreeSession m_treeSession; +}; + +#endif // CTRACE_SRC_DECODE_OPENCSDFORMATTEDITMSESSION_H diff --git a/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp b/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp index 8bdb01679..2a1f4e624 100644 --- a/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp +++ b/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp @@ -9,6 +9,7 @@ #include "TraceEvent.h" #include "OpenCsdErrorController.h" +#include "OpenCsdFormattedItmSession.h" #include "OpenCsdPacketCollector.h" #include "OpenCsdItmSession.h" #include "OpenCsdTraceElement.h" @@ -16,32 +17,43 @@ #include "opencsd/ocsd_if_types.h" #include +#include #include #include #include #include #include #include +#include static_assert(sizeof(ocsd_trc_index_t) == sizeof(std::uint64_t), "ctrace requires 64-bit OpenCSD trace indices"); -/** @brief Creates the production OpenCSD ITM session. */ -static std::unique_ptr -createDefaultOpenCsdItmSession(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController) -{ - return std::make_unique(collector, errorController); -} - /** @brief Implements OpenCSD feeding, bounded retry, and hardware-sync recovery. */ class OpenCsdItmDecoderImpl { public: - /** @brief Creates a decoder implementation around one session factory. */ - OpenCsdItmDecoderImpl(TraceRouteIdentity route, OpenCsdTraceElementSink& elementSink, - const OpenCsdItmSessionFactory& sessionFactory) - : m_collector(std::move(route), elementSink) + /** @brief Creates a decoder implementation around the selected frontend and optional session factory. */ + OpenCsdItmDecoderImpl(std::vector routes, OpenCsdItmInputMode inputMode, + OpenCsdTraceElementSink& elementSink, const OpenCsdItmSessionFactory& sessionFactory, + OpenCsdUnsupportedTraceIdObserver unsupportedTraceIdObserver) + : m_inputMode(inputMode), + m_collector(createCollector(routes, inputMode, elementSink)) { try { - m_session = sessionFactory(m_collector, m_errorController); + if (sessionFactory) { + m_session = sessionFactory(m_collector, m_errorController); + } else if (isFormatted()) { + OpenCsdUnsupportedTraceIdSink unsupportedTraceIdSink; + if (unsupportedTraceIdObserver) { + unsupportedTraceIdSink = [observer = std::move(unsupportedTraceIdObserver)](std::uint8_t traceBusId, + ocsd_trc_index_t sourceOffset) { + observer(traceBusId, static_cast(sourceOffset)); + }; + } + m_session = std::make_unique(std::move(routes), m_collector, m_errorController, + m_collector, std::move(unsupportedTraceIdSink)); + } else { + m_session = std::make_unique(m_collector, m_errorController); + } if (m_session == nullptr) { failInitialization("OpenCSD ITM session factory returned no session"); } @@ -56,6 +68,15 @@ class OpenCsdItmDecoderImpl { if (m_finished) { throw std::runtime_error("OpenCSD ITM decoder already finished"); } + if (data == nullptr && size != 0U) { + throw std::invalid_argument("raw trace data pointer is null while bytes are present"); + } + if (isFormatted() && size % kFormattedFrameSize != 0U) { + m_collector.appendDecodeError(m_traceIndex, "formatted raw trace chunk is not a multiple of 16 bytes", + TraceIssueCode::OpenCsdDecodeError, false); + throw OpenCsdFatalError("formatted raw trace chunk is not a multiple of 16 bytes", + static_cast(m_traceIndex)); + } std::uint32_t offset = 0; while (offset < size) { const auto span = std::min(kMaxTraceDataInBytes, size - offset); @@ -74,11 +95,11 @@ class OpenCsdItmDecoderImpl { completeConsumedDataLoss(m_traceIndex); m_collector.beginTransaction(); m_errorController.beginDataPathCall(); - const auto response = m_session->endOfTrace(); - m_collector.rethrowOutputError(); + const auto response = invokeSessionOperation([&] { return m_session->endOfTrace(); }, m_traceIndex, 0U, nullptr, + "OpenCSD aborted end-of-trace processing: ", true); const auto decision = m_errorController.decide(response); - if (decision.action == OpenCsdErrorController::Action::Abort) { - abortDecode(decision, 0U, m_traceIndex, 0U, "OpenCSD aborted end-of-trace processing: "); + if (decision.action == OpenCsdErrorController::Action::Abort || formattedOperationFailed(decision)) { + abortDecode(decision, 0U, m_traceIndex, 0U, "OpenCSD aborted end-of-trace processing: ", isFormatted()); } if (decision.action == OpenCsdErrorController::Action::RecoverStream) { const auto sourceOffset = OpenCsdErrorController::errorOffset(decision, m_traceIndex); @@ -98,6 +119,91 @@ class OpenCsdItmDecoderImpl { private: static constexpr std::uint32_t kMaxTraceDataInBytes = 4U * 1024U; + static constexpr std::uint32_t kFormattedFrameSize = 16U; + + /** @brief Creates a fixed-route or channel-routed collector for the selected transport. */ + static OpenCsdPacketCollector createCollector(const std::vector& routes, + OpenCsdItmInputMode inputMode, OpenCsdTraceElementSink& elementSink) + { + if (routes.empty()) { + throw std::invalid_argument("OpenCSD ITM decoding requires at least one normalized route"); + } + if (inputMode == OpenCsdItmInputMode::Single) { + if (routes.size() != 1U) { + throw std::invalid_argument("OpenCSD SINGLE decoding requires exactly one normalized route"); + } + return OpenCsdPacketCollector(routes.front(), elementSink); + } + return OpenCsdPacketCollector(routes, elementSink); + } + + /** @brief Reports whether the frontend is a CoreSight frame deformatter. */ + bool isFormatted() const noexcept + { + return m_inputMode == OpenCsdItmInputMode::CoreSightFormatted; + } + + /** @brief Makes every formatted protocol or deformatter error input-fatal in Phase 7. */ + bool formattedOperationFailed(const OpenCsdErrorController::Decision& decision) const + { + if (!isFormatted()) { + return false; + } + const auto reportedError = std::any_of(decision.errors.begin(), decision.errors.end(), + [](const auto& error) { return error.severity == OCSD_ERR_SEV_ERROR; }); + return decision.action == OpenCsdErrorController::Action::RecoverStream || reportedError || + OpenCsdErrorController::responseReportsError(decision.response) || m_collector.transactionHasError(); + } + + /** @brief Runs one session operation while keeping legacy SINGLE exception behavior unchanged. */ + template + ocsd_datapath_resp_t invokeSessionOperation(Operation&& operation, std::uint64_t baseOffset, std::uint32_t size, + const std::uint32_t* bytesConsumed, const std::string& fatalPrefix, + bool preserveIncompleteTail = false) + { + if (!isFormatted()) { + const auto response = operation(); + m_collector.rethrowOutputError(); + return response; + } + + try { + const auto response = operation(); + m_collector.rethrowOutputError(); + return response; + } catch (const OpenCsdFormattedInputError& error) { + abortFormattedException(error.what(), error.sourceOffset(), processedOffset(baseOffset, size, bytesConsumed), + fatalPrefix, TraceIssueCode::OpenCsdFormattedInputError, preserveIncompleteTail); + } catch (const std::exception& error) { + const auto sourceOffset = m_collector.transactionFirstSourceOffset().value_or(baseOffset); + abortFormattedException(std::string("formatted OpenCSD session operation failed: ") + error.what() + + " at raw input offset " + std::to_string(sourceOffset), + sourceOffset, processedOffset(baseOffset, size, bytesConsumed), fatalPrefix, + TraceIssueCode::OpenCsdDecodeError, preserveIncompleteTail); + } + } + + /** @brief Computes the raw byte boundary reached by a session operation. */ + static std::uint64_t processedOffset(std::uint64_t baseOffset, std::uint32_t size, + const std::uint32_t* bytesConsumed) noexcept + { + return baseOffset + + (bytesConsumed == nullptr ? 0U : std::min(*bytesConsumed, static_cast(size))); + } + + /** @brief Normalizes one formatted-session exception into the input-fatal decoder contract. */ + [[noreturn]] void abortFormattedException(const std::string& message, std::uint64_t sourceOffset, + std::uint64_t bytesProcessed, const std::string& fatalPrefix, + TraceIssueCode issueCode, bool preserveIncompleteTail) + { + if (preserveIncompleteTail) { + static_cast(m_collector.commitTransactionErrors(TraceIssueCode::OpenCsdIncompleteTail)); + } else { + m_collector.rollbackTransaction(); + } + m_collector.appendDecodeError(static_cast(sourceOffset), message, issueCode, true); + throw OpenCsdFatalError(fatalPrefix + message, bytesProcessed); + } void appendReportedErrors(const OpenCsdErrorController::Decision& decision, std::uint64_t baseOffset, bool discontinuity, bool force = false) @@ -130,11 +236,17 @@ class OpenCsdItmDecoderImpl { } [[noreturn]] void abortDecode(const OpenCsdErrorController::Decision& decision, std::uint32_t size, - std::uint64_t baseOffset, std::uint32_t bytesConsumed, const std::string& prefix) + std::uint64_t baseOffset, std::uint32_t bytesConsumed, const std::string& prefix, + bool preserveIncompleteTail = false) { - m_collector.rollbackTransaction(); + std::size_t retainedErrors = 0U; + if (preserveIncompleteTail) { + retainedErrors = m_collector.commitTransactionErrors(TraceIssueCode::OpenCsdIncompleteTail); + } else { + m_collector.rollbackTransaction(); + } completeConsumedDataLoss(OpenCsdErrorController::errorOffset(decision, baseOffset)); - appendReportedErrors(decision, baseOffset, true); + appendReportedErrors(decision, baseOffset, true, retainedErrors == 0U); const auto processed = baseOffset + std::min(bytesConsumed, size); throw OpenCsdFatalError(prefix + OpenCsdErrorController::describeSummary(decision), processed); } @@ -174,10 +286,11 @@ class OpenCsdItmDecoderImpl { std::uint32_t processedThisPass = 0; m_collector.beginTransaction(); m_errorController.beginDataPathCall(); - const auto response = m_session->pushData(m_traceIndex, callSize, data + processed, processedThisPass); - m_collector.rethrowOutputError(); + const auto response = invokeSessionOperation( + [&] { return m_session->pushData(m_traceIndex, callSize, data + processed, processedThisPass); }, callIndex, + callSize, &processedThisPass, "OpenCSD aborted decode: "); const auto decision = m_errorController.decide(response); - if (decision.action == OpenCsdErrorController::Action::Abort) { + if (decision.action == OpenCsdErrorController::Action::Abort || formattedOperationFailed(decision)) { abortDecode(decision, callSize, callIndex, processedThisPass, "OpenCSD aborted decode: "); } @@ -226,6 +339,12 @@ class OpenCsdItmDecoderImpl { } if (consumed == 0U) { m_collector.rollbackTransaction(); + if (isFormatted()) { + m_collector.appendDecodeError(m_traceIndex, "OpenCSD made no progress on formatted trace input", + TraceIssueCode::OpenCsdNoProgress, false); + throw OpenCsdFatalError("OpenCSD made no progress on formatted trace input", + static_cast(m_traceIndex)); + } m_collector.appendDecodeError( m_traceIndex, "OpenCSD made no progress while raw data was present; decoder reset and searching " @@ -240,6 +359,11 @@ class OpenCsdItmDecoderImpl { if (m_collector.transactionElementCount() == 0U) { m_collector.rollbackTransaction(); appendReportedErrors(decision, callIndex, false); + if (isFormatted()) { + processed += consumed; + m_traceIndex += consumed; + continue; + } if (!m_dataLossActive) { m_consumedDataLossStart = static_cast(m_traceIndex); m_consumedDataLossBoundaryMarked = false; @@ -265,10 +389,10 @@ class OpenCsdItmDecoderImpl { for (std::uint32_t call = 0; call < kMaxFlushCalls; ++call) { m_collector.beginTransaction(); m_errorController.beginDataPathCall(); - const auto response = m_session->flush(); - m_collector.rethrowOutputError(); + const auto response = invokeSessionOperation([&] { return m_session->flush(); }, m_traceIndex, 0U, nullptr, + "OpenCSD aborted while flushing a WAIT response: "); const auto decision = m_errorController.decide(response); - if (decision.action == OpenCsdErrorController::Action::Abort) { + if (decision.action == OpenCsdErrorController::Action::Abort || formattedOperationFailed(decision)) { abortDecode(decision, 0U, m_traceIndex, 0U, "OpenCSD aborted while flushing a WAIT response: "); } if (decision.action == OpenCsdErrorController::Action::RecoverStream) { @@ -316,6 +440,7 @@ class OpenCsdItmDecoderImpl { throw OpenCsdFatalError(message, static_cast(m_traceIndex)); } + OpenCsdItmInputMode m_inputMode = OpenCsdItmInputMode::Single; OpenCsdPacketCollector m_collector; OpenCsdErrorController m_errorController; std::unique_ptr m_session; @@ -328,13 +453,30 @@ class OpenCsdItmDecoderImpl { }; OpenCsdItmDecoder::OpenCsdItmDecoder(TraceRouteIdentity route, OpenCsdTraceElementSink& elementSink) - : m_impl(std::make_unique(std::move(route), elementSink, createDefaultOpenCsdItmSession)) + : OpenCsdItmDecoder(std::vector{std::move(route)}, OpenCsdItmInputMode::Single, elementSink) { } OpenCsdItmDecoder::OpenCsdItmDecoder(TraceRouteIdentity route, OpenCsdTraceElementSink& elementSink, const OpenCsdItmSessionFactory& sessionFactory) - : m_impl(std::make_unique(std::move(route), elementSink, sessionFactory)) + : OpenCsdItmDecoder(std::vector{std::move(route)}, OpenCsdItmInputMode::Single, elementSink, + sessionFactory) +{ +} + +OpenCsdItmDecoder::OpenCsdItmDecoder(std::vector routes, OpenCsdItmInputMode inputMode, + OpenCsdTraceElementSink& elementSink, + OpenCsdUnsupportedTraceIdObserver unsupportedTraceIdObserver) + : m_impl(std::make_unique(std::move(routes), inputMode, elementSink, + OpenCsdItmSessionFactory{}, std::move(unsupportedTraceIdObserver))) +{ +} + +OpenCsdItmDecoder::OpenCsdItmDecoder(std::vector routes, OpenCsdItmInputMode inputMode, + OpenCsdTraceElementSink& elementSink, + const OpenCsdItmSessionFactory& sessionFactory) + : m_impl(std::make_unique(std::move(routes), inputMode, elementSink, sessionFactory, + OpenCsdUnsupportedTraceIdObserver{})) { } diff --git a/tools/ctrace/src/decode/OpenCsdItmDecoder.h b/tools/ctrace/src/decode/OpenCsdItmDecoder.h index c0c91a668..70e7abb34 100644 --- a/tools/ctrace/src/decode/OpenCsdItmDecoder.h +++ b/tools/ctrace/src/decode/OpenCsdItmDecoder.h @@ -16,6 +16,16 @@ #include #include #include +#include + +/** @brief Selects the root transport presented to the OpenCSD ITM decoders. */ +enum class OpenCsdItmInputMode { + Single, + CoreSightFormatted, +}; + +/** @brief Receives one observed but unconfigured normal CoreSight Trace Bus ID. */ +using OpenCsdUnsupportedTraceIdObserver = std::function; /** @brief Summarizes raw input consumed by an OpenCSD ITM decoder. */ struct OpenCsdItmDecodeResult { @@ -74,6 +84,25 @@ class OpenCsdItmDecoder { */ OpenCsdItmDecoder(TraceRouteIdentity route, OpenCsdTraceElementSink& elementSink, const OpenCsdItmSessionFactory& sessionFactory); + /** + * @brief Creates a decoder for one SINGLE route or several formatted routes. + * @param routes Normalized routes accepted by the input frontend. + * @param inputMode OpenCSD root transport used for the raw bytes. + * @param elementSink Sink receiving decoded and recovery elements. + * @param unsupportedTraceIdSink Observer for unconfigured normal formatted IDs. + */ + OpenCsdItmDecoder(std::vector routes, OpenCsdItmInputMode inputMode, + OpenCsdTraceElementSink& elementSink, + OpenCsdUnsupportedTraceIdObserver unsupportedTraceIdSink = {}); + /** + * @brief Creates a configured decoder with an injected external session. + * @param routes Normalized routes accepted by the input frontend. + * @param inputMode Policy mode applied around the injected session. + * @param elementSink Sink receiving decoded and recovery elements. + * @param sessionFactory Factory used to construct the external session. + */ + OpenCsdItmDecoder(std::vector routes, OpenCsdItmInputMode inputMode, + OpenCsdTraceElementSink& elementSink, const OpenCsdItmSessionFactory& sessionFactory); /** @brief Destroys the decoder implementation and external session. */ ~OpenCsdItmDecoder(); @@ -100,4 +129,4 @@ class OpenCsdItmDecoder { std::unique_ptr m_impl; }; -#endif // CTRACE_SRC_DECODE_OPENCSDITMDECODER_H +#endif // CTRACE_SRC_DECODE_OPENCSDITMDECODER_H diff --git a/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp b/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp index 201dc7e39..209dcf435 100644 --- a/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp +++ b/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp @@ -10,6 +10,7 @@ #include "TraceEvent.h" #include "OpenCsdTraceElement.h" #include "TraceRoute.h" +#include "TraceStreamId.h" #include "common/trc_gen_elem.h" #include "opencsd/itm/trc_pkt_elem_itm.h" #include "opencsd/itm/trc_pkt_types_itm.h" @@ -19,17 +20,37 @@ #include #include #include +#include #include +#include #include #include #include OpenCsdPacketCollector::OpenCsdPacketCollector(TraceRouteIdentity route, OpenCsdTraceElementSink& elementSink) - : m_route(std::move(route)), + : m_singleRoute(std::move(route)), m_elementSink(elementSink) { } +OpenCsdPacketCollector::OpenCsdPacketCollector(std::vector routes, + OpenCsdTraceElementSink& elementSink) + : m_elementSink(elementSink) +{ + if (routes.empty()) { + throw std::invalid_argument("formatted OpenCSD packet collection requires at least one normalized route"); + } + for (auto& route : routes) { + if (!route.traceBusId.has_value() || !CoreSight::isAtbTraceId(*route.traceBusId)) { + throw std::invalid_argument("formatted OpenCSD packet route requires a Trace Bus ID between 1 and 111"); + } + const auto channel = *route.traceBusId; + if (!m_routesByChannel.emplace(channel, std::move(route)).second) { + throw std::invalid_argument("duplicate Trace Bus ID in formatted OpenCSD packet routes"); + } + } +} + void OpenCsdPacketCollector::beginTransaction() { m_transactionActive = true; @@ -45,6 +66,23 @@ void OpenCsdPacketCollector::commitTransaction() m_transactionActive = false; } +std::size_t OpenCsdPacketCollector::commitTransactionErrors(TraceIssueCode issueCode) +{ + std::vector retained; + for (auto& element : m_transactionElements) { + if (element.kind == OpenCsdTraceElement::Kind::Error && element.issueSeverity == TraceIssueSeverity::Error && + element.issueCode == issueCode) { + retained.push_back(std::move(element)); + } + } + m_transactionElements.clear(); + m_transactionActive = false; + for (auto& element : retained) { + appendCommitted(std::move(element)); + } + return retained.size(); +} + void OpenCsdPacketCollector::commitTransactionBefore(std::uint64_t sourceOffset) { for (auto& element : m_transactionElements) { @@ -78,6 +116,16 @@ std::size_t OpenCsdPacketCollector::transactionElementCount() const return m_transactionElements.size(); } +bool OpenCsdPacketCollector::transactionHasError() const +{ + for (const auto& element : m_transactionElements) { + if (element.kind == OpenCsdTraceElement::Kind::Error && element.issueSeverity == TraceIssueSeverity::Error) { + return true; + } + } + return false; +} + std::optional OpenCsdPacketCollector::transactionFirstSourceOffset() const { std::optional firstOffset; @@ -94,6 +142,7 @@ void OpenCsdPacketCollector::appendDecodeError(ocsd_trc_index_t index, const std TraceIssueCode issueCode, bool discontinuity, TraceIssueSeverity severity) { + const auto& route = defaultRoute(); OpenCsdTraceElement element; element.kind = OpenCsdTraceElement::Kind::Error; element.sourceIndex = static_cast(index); @@ -101,13 +150,14 @@ void OpenCsdPacketCollector::appendDecodeError(ocsd_trc_index_t index, const std element.issueCode = issueCode; element.issueSeverity = severity; element.errorMessage = message; - appendElement(std::move(element)); + appendElement(std::move(element), route); } void OpenCsdPacketCollector::prependDiscontinuity(ocsd_trc_index_t index, const std::string& message, TraceIssueCode issueCode, std::optional rawBytesConsumed) { + const auto& route = defaultRoute(); OpenCsdTraceElement element; element.kind = OpenCsdTraceElement::Kind::Discontinuity; element.sourceIndex = static_cast(index); @@ -115,17 +165,18 @@ void OpenCsdPacketCollector::prependDiscontinuity(ocsd_trc_index_t index, const element.issueCode = issueCode; element.errorMessage = message; element.rawBytesConsumed = rawBytesConsumed; - element.route = m_route; + element.route = route; if (m_transactionActive) { m_transactionElements.insert(m_transactionElements.begin(), std::move(element)); return; } - appendElement(std::move(element)); + appendElement(std::move(element), route); } void OpenCsdPacketCollector::prependDataLossError(ocsd_trc_index_t index, const std::string& message, std::uint64_t rawBytesConsumed) { + const auto& route = defaultRoute(); OpenCsdTraceElement element; element.kind = OpenCsdTraceElement::Kind::Error; element.sourceIndex = static_cast(index); @@ -133,18 +184,22 @@ void OpenCsdPacketCollector::prependDataLossError(ocsd_trc_index_t index, const element.errorMessage = message; element.rawBytesConsumed = rawBytesConsumed; element.awaitingResumeTimestamp = true; - element.route = m_route; + element.route = route; if (m_transactionActive) { m_transactionElements.insert(m_transactionElements.begin(), std::move(element)); return; } - appendElement(std::move(element)); + appendElement(std::move(element), route); } -ocsd_datapath_resp_t OpenCsdPacketCollector::TraceElemIn(const ocsd_trc_index_t index_sop, const std::uint8_t, - const OcsdTraceElement& elem) +ocsd_datapath_resp_t OpenCsdPacketCollector::TraceElemIn(const ocsd_trc_index_t index_sop, + const std::uint8_t trc_chan_id, const OcsdTraceElement& elem) { try { + const auto* route = routeForChannel(trc_chan_id); + if (route == nullptr) { + return OCSD_RESP_CONT; + } if (elem.getType() != OCSD_GEN_TRC_ELEM_ITMTRACE) { return OCSD_RESP_CONT; } @@ -152,19 +207,19 @@ ocsd_datapath_resp_t OpenCsdPacketCollector::TraceElemIn(const ocsd_trc_index_t const auto& info = elem.swt_itm; switch (info.pkt_type) { case SWIT_PAYLOAD: - appendSoftware(index_sop, elem); + appendSoftware(index_sop, elem, *route); break; case DWT_PAYLOAD: - appendDwt(index_sop, elem); + appendDwt(index_sop, elem, *route); break; case TS_SYNC: case TS_DELAY: case TS_PKT_DELAY: case TS_PKT_TS_DELAY: - appendTimestamp(index_sop, elem); + appendTimestamp(index_sop, elem, *route); break; case TS_GLOBAL: - appendGlobalTimestamp(index_sop, elem); + appendGlobalTimestamp(index_sop, elem, *route); break; } } catch (...) { @@ -177,44 +232,25 @@ ocsd_datapath_resp_t OpenCsdPacketCollector::TraceElemIn(const ocsd_trc_index_t } void OpenCsdPacketCollector::RawPacketDataMon(const ocsd_datapath_op_t op, const ocsd_trc_index_t index_sop, - const ItmTrcPacket* pkt, const std::uint32_t, const std::uint8_t*) + const ItmTrcPacket* pkt, const std::uint32_t size, + const std::uint8_t* data) { - try { - if (pkt == nullptr) { - return; - } - - // OpenCSD publishes the incomplete packet through the raw monitor as DATA - // while processing EOT; its following EOT monitor notification has no packet. - if (pkt->getPktType() == ITM_PKT_INCOMPLETE_EOT) { - appendDecodeError(index_sop, "incomplete ITM packet at end of input", TraceIssueCode::OpenCsdIncompleteTail, true, - TraceIssueSeverity::Error); - return; - } - if (op != OCSD_OP_DATA) { - return; - } + const auto* route = singleRoute(); + if (route == nullptr) { + return; + } + rawPacketForRoute(*route, op, index_sop, pkt, size, data); +} - switch (pkt->getPktType()) { - case ITM_PKT_ASYNC: - appendSync(index_sop); - break; - case ITM_PKT_OVERFLOW: - appendOverflow(index_sop); - break; - case ITM_PKT_TS_GLOBAL_1: - case ITM_PKT_TS_GLOBAL_2: - // The ITM decoder combines GTS1/GTS2 and publishes the complete - // 64-bit value as a generic TS_GLOBAL element. Raw fragments are - // intentionally not forwarded as independent timestamps. - break; - case ITM_PKT_BAD_SEQUENCE: - case ITM_PKT_RESERVED: - appendError(index_sop, *pkt); - break; - default: - break; +void OpenCsdPacketCollector::rawPacketForRoute(const TraceRouteIdentity& route, const ocsd_datapath_op_t op, + const ocsd_trc_index_t index_sop, const ItmTrcPacket* pkt, + const std::uint32_t, const std::uint8_t*) +{ + try { + if (!containsRoute(route)) { + throw std::invalid_argument("raw OpenCSD packet references an unknown normalized route"); } + appendRawPacket(route, op, index_sop, pkt); } catch (...) { if (!m_outputError) { m_outputError = std::current_exception(); @@ -222,43 +258,126 @@ void OpenCsdPacketCollector::RawPacketDataMon(const ocsd_datapath_op_t op, const } } -void OpenCsdPacketCollector::appendSync(ocsd_trc_index_t index) +const TraceRouteIdentity* OpenCsdPacketCollector::singleRoute() const noexcept +{ + return m_singleRoute.has_value() ? &*m_singleRoute : nullptr; +} + +const TraceRouteIdentity& OpenCsdPacketCollector::defaultRoute() const noexcept +{ + if (const auto* route = singleRoute()) { + return *route; + } + return m_routesByChannel.begin()->second; +} + +const TraceRouteIdentity* OpenCsdPacketCollector::routeForChannel(std::uint8_t channel) const noexcept +{ + if (const auto* route = singleRoute()) { + return route; + } + const auto found = m_routesByChannel.find(channel); + return found != m_routesByChannel.end() ? &found->second : nullptr; +} + +bool OpenCsdPacketCollector::containsRoute(const TraceRouteIdentity& route) const noexcept +{ + if (const auto* boundRoute = singleRoute()) { + return *boundRoute == route; + } + if (!route.traceBusId.has_value()) { + return false; + } + const auto found = m_routesByChannel.find(*route.traceBusId); + return found != m_routesByChannel.end() && found->second == route; +} + +void OpenCsdPacketCollector::appendRawPacket(const TraceRouteIdentity& route, const ocsd_datapath_op_t op, + const ocsd_trc_index_t index_sop, const ItmTrcPacket* pkt) +{ + if (pkt == nullptr) { + return; + } + + // OpenCSD publishes the incomplete packet through the raw monitor as DATA + // while processing EOT; its following EOT monitor notification has no packet. + if (pkt->getPktType() == ITM_PKT_INCOMPLETE_EOT) { + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Error; + element.sourceIndex = static_cast(index_sop); + element.discontinuity = true; + element.issueCode = TraceIssueCode::OpenCsdIncompleteTail; + element.issueSeverity = TraceIssueSeverity::Error; + element.errorMessage = "incomplete ITM packet at end of input"; + appendElement(std::move(element), route); + return; + } + if (op != OCSD_OP_DATA) { + return; + } + + switch (pkt->getPktType()) { + case ITM_PKT_ASYNC: + appendSync(index_sop, route); + break; + case ITM_PKT_OVERFLOW: + appendOverflow(index_sop, route); + break; + case ITM_PKT_TS_GLOBAL_1: + case ITM_PKT_TS_GLOBAL_2: + // The ITM decoder combines GTS1/GTS2 and publishes the complete + // 64-bit value as a generic TS_GLOBAL element. Raw fragments are + // intentionally not forwarded as independent timestamps. + break; + case ITM_PKT_BAD_SEQUENCE: + case ITM_PKT_RESERVED: + appendError(index_sop, *pkt, route); + break; + default: + break; + } +} + +void OpenCsdPacketCollector::appendSync(ocsd_trc_index_t index, const TraceRouteIdentity& route) { OpenCsdTraceElement element; element.kind = OpenCsdTraceElement::Kind::Sync; element.sourceIndex = static_cast(index); - appendElement(std::move(element)); + appendElement(std::move(element), route); } -void OpenCsdPacketCollector::appendOverflow(ocsd_trc_index_t index) +void OpenCsdPacketCollector::appendOverflow(ocsd_trc_index_t index, const TraceRouteIdentity& route) { OpenCsdTraceElement element; element.kind = OpenCsdTraceElement::Kind::Overflow; element.sourceIndex = static_cast(index); - appendElement(std::move(element)); + appendElement(std::move(element), route); } -void OpenCsdPacketCollector::appendGlobalTimestamp(ocsd_trc_index_t index, const OcsdTraceElement& elem) +void OpenCsdPacketCollector::appendGlobalTimestamp(ocsd_trc_index_t index, const OcsdTraceElement& elem, + const TraceRouteIdentity& route) { OpenCsdTraceElement element; element.kind = OpenCsdTraceElement::Kind::GlobalTimestamp; element.sourceIndex = static_cast(index); element.timestampValue = elem.timestamp; element.clockChange = elem.cpu_freq_change != 0U; - appendElement(std::move(element)); + appendElement(std::move(element), route); } -void OpenCsdPacketCollector::appendError(ocsd_trc_index_t index, const ItmTrcPacket& pkt) +void OpenCsdPacketCollector::appendError(ocsd_trc_index_t index, const ItmTrcPacket& pkt, + const TraceRouteIdentity& route) { OpenCsdTraceElement element; element.kind = OpenCsdTraceElement::Kind::Error; element.sourceIndex = static_cast(index); element.issueCode = TraceIssueCode::OpenCsdDecodeError; element.errorMessage = pkt.getPktType() == ITM_PKT_RESERVED ? "Reserved ITM packet" : "Bad ITM packet sequence"; - appendElement(std::move(element)); + appendElement(std::move(element), route); } -void OpenCsdPacketCollector::appendSoftware(ocsd_trc_index_t index, const OcsdTraceElement& elem) +void OpenCsdPacketCollector::appendSoftware(ocsd_trc_index_t index, const OcsdTraceElement& elem, + const TraceRouteIdentity& route) { const auto& info = elem.swt_itm; OpenCsdTraceElement element; @@ -268,10 +387,11 @@ void OpenCsdPacketCollector::appendSoftware(ocsd_trc_index_t index, const OcsdTr element.size = info.payload_size; element.value = info.value; element.overflow = info.overflow; - appendElement(std::move(element)); + appendElement(std::move(element), route); } -void OpenCsdPacketCollector::appendDwt(ocsd_trc_index_t index, const OcsdTraceElement& elem) +void OpenCsdPacketCollector::appendDwt(ocsd_trc_index_t index, const OcsdTraceElement& elem, + const TraceRouteIdentity& route) { const auto& info = elem.swt_itm; OpenCsdTraceElement element; @@ -281,10 +401,11 @@ void OpenCsdPacketCollector::appendDwt(ocsd_trc_index_t index, const OcsdTraceEl element.size = info.payload_size; element.value = info.value; element.overflow = info.overflow; - appendElement(std::move(element)); + appendElement(std::move(element), route); } -void OpenCsdPacketCollector::appendTimestamp(ocsd_trc_index_t index, const OcsdTraceElement& elem) +void OpenCsdPacketCollector::appendTimestamp(ocsd_trc_index_t index, const OcsdTraceElement& elem, + const TraceRouteIdentity& route) { const auto& info = elem.swt_itm; OpenCsdTraceElement element; @@ -293,7 +414,7 @@ void OpenCsdPacketCollector::appendTimestamp(ocsd_trc_index_t index, const OcsdT element.timestampRelation = timestampRelation(info.pkt_type); element.tcyc = elem.timestamp; element.overflow = info.overflow; - appendElement(std::move(element)); + appendElement(std::move(element), route); } LocalTimestampRelation OpenCsdPacketCollector::timestampRelation(swt_itm_type type) @@ -310,9 +431,9 @@ LocalTimestampRelation OpenCsdPacketCollector::timestampRelation(swt_itm_type ty return LocalTimestampRelation::Synchronous; } -void OpenCsdPacketCollector::appendElement(OpenCsdTraceElement element) +void OpenCsdPacketCollector::appendElement(OpenCsdTraceElement element, const TraceRouteIdentity& route) { - element.route = m_route; + element.route = route; if (m_transactionActive) { m_transactionElements.push_back(std::move(element)); return; diff --git a/tools/ctrace/src/decode/OpenCsdPacketCollector.h b/tools/ctrace/src/decode/OpenCsdPacketCollector.h index 936161072..f7662f55a 100644 --- a/tools/ctrace/src/decode/OpenCsdPacketCollector.h +++ b/tools/ctrace/src/decode/OpenCsdPacketCollector.h @@ -8,6 +8,7 @@ #ifndef CTRACE_SRC_DECODE_OPENCSDPACKETCOLLECTOR_H #define CTRACE_SRC_DECODE_OPENCSDPACKETCOLLECTOR_H +#include "OpenCsdFormattedItmSession.h" #include "TraceEvent.h" #include "OpenCsdTraceElement.h" #include "TraceRoute.h" @@ -21,11 +22,15 @@ #include #include #include +#include +#include #include #include /** @brief Collects OpenCSD callbacks into transactional ctrace elements. */ -class OpenCsdPacketCollector : public ITrcGenElemIn, public IPktRawDataMon { +class OpenCsdPacketCollector : public ITrcGenElemIn, + public IPktRawDataMon, + public OpenCsdFormattedItmPacketSink { public: /** * @brief Creates a collector that emits committed elements to a sink. @@ -33,6 +38,13 @@ class OpenCsdPacketCollector : public ITrcGenElemIn, public IPktRawDataMon routes, OpenCsdTraceElementSink& elementSink); /** * @brief Starts buffering elements for one recoverable decoder operation. @@ -43,6 +55,12 @@ class OpenCsdPacketCollector : public ITrcGenElemIn, public IPktRawDataMon transactionFirstSourceOffset() const; /** @@ -89,30 +109,53 @@ class OpenCsdPacketCollector : public ITrcGenElemIn, public IPktRawDataMon m_singleRoute; + std::map m_routesByChannel; OpenCsdTraceElementSink& m_elementSink; bool m_transactionActive = false; std::vector m_transactionElements; diff --git a/tools/ctrace/src/decode/OpenCsdTreeSession.cpp b/tools/ctrace/src/decode/OpenCsdTreeSession.cpp index d99286bf9..753ad6d65 100644 --- a/tools/ctrace/src/decode/OpenCsdTreeSession.cpp +++ b/tools/ctrace/src/decode/OpenCsdTreeSession.cpp @@ -11,8 +11,11 @@ #include "common/ocsd_dcd_mngr_i.h" #include "common/ocsd_dcd_tree.h" #include "common/ocsd_dcd_tree_elem.h" +#include "common/trc_cs_config.h" #include "common/trc_component.h" +#include "common/trc_frame_deformatter.h" #include "interfaces/trc_abs_typed_base_i.h" +#include "interfaces/trc_data_rawframe_in_i.h" #include "interfaces/trc_error_log_i.h" #include "interfaces/trc_gen_elem_in_i.h" @@ -86,6 +89,14 @@ OpenCsdTreeSession::TreeLifecycle OpenCsdTreeSession::defaultLifecycle() }; } +ocsd_dcd_tree_src_t OpenCsdTreeSession::validateSourceType(ocsd_dcd_tree_src_t sourceType) +{ + if (sourceType != OCSD_TRC_SRC_SINGLE && sourceType != OCSD_TRC_SRC_FRAME_FORMATTED) { + throw OpenCsdTreeSessionError("unsupported OpenCSD DecodeTree source type"); + } + return sourceType; +} + OpenCsdTreeSession::OpenCsdTreeSession(ocsd_dcd_tree_src_t sourceType, std::uint32_t formatterFlags, ITraceErrorLog& errorLogger, ITrcGenElemIn& elementOutput) : OpenCsdTreeSession(sourceType, formatterFlags, errorLogger, elementOutput, defaultLifecycle()) @@ -95,7 +106,8 @@ OpenCsdTreeSession::OpenCsdTreeSession(ocsd_dcd_tree_src_t sourceType, std::uint OpenCsdTreeSession::OpenCsdTreeSession(ocsd_dcd_tree_src_t sourceType, std::uint32_t formatterFlags, ITraceErrorLog& errorLogger, ITrcGenElemIn& elementOutput, const TreeLifecycle& lifecycle) - : m_errorLogger(errorLogger), + : m_sourceType(validateSourceType(sourceType)), + m_errorLogger(errorLogger), m_loggerLease(std::make_unique(errorLogger)), m_tree(nullptr, TreeDeleter{lifecycle.destroy}) { @@ -105,7 +117,7 @@ OpenCsdTreeSession::OpenCsdTreeSession(ocsd_dcd_tree_src_t sourceType, std::uint if (!lifecycle.destroy) { throw OpenCsdTreeSessionError("OpenCSD DecodeTree destroyer is not configured"); } - m_tree.reset(lifecycle.create(sourceType, formatterFlags)); + m_tree.reset(lifecycle.create(m_sourceType, formatterFlags)); OpenCsdSessionValidation::requireObject(m_tree.get(), "failed to create OpenCSD DecodeTree"); m_tree->setGenTraceElemOutI(&elementOutput); } @@ -114,12 +126,14 @@ OpenCsdTreeSession::~OpenCsdTreeSession() noexcept = default; void OpenCsdTreeSession::createDecoder(const std::string& decoderName, int createFlags, const CSConfig& config) { + validateChannel(config.getTraceID()); OpenCsdSessionValidation::requireSuccess(m_tree->createDecoder(decoderName, createFlags, &config), "failed to create OpenCSD decoder"); } void OpenCsdTreeSession::attachDecoderCallbacks(std::uint8_t channel, ITrcTypedBase& packetMonitor) { + validateChannel(channel); auto* element = m_tree->getDecoderElement(channel); OpenCsdSessionValidation::requireObject(element, "OpenCSD decoder element is not initialized"); auto* manager = element->getDecoderMngr(); @@ -138,6 +152,32 @@ void OpenCsdTreeSession::attachDecoderCallbacks(std::uint8_t channel, ITrcTypedB "failed to attach OpenCSD packet monitor"); } +void OpenCsdTreeSession::attachRawFrameMonitor(ITrcRawFrameIn& frameMonitor) +{ + if (m_sourceType != OCSD_TRC_SRC_FRAME_FORMATTED) { + throw OpenCsdTreeSessionError("OpenCSD raw frame monitor requires a formatted DecodeTree"); + } + auto* deformatter = m_tree->getFrameDeformatter(); + OpenCsdSessionValidation::requireObject(deformatter, "OpenCSD frame deformatter is not initialized"); + auto* attachPoint = deformatter->getTrcRawFrameAttachPt(); + OpenCsdSessionValidation::requireObject(attachPoint, "OpenCSD raw-frame attach point is not initialized"); + OpenCsdSessionValidation::requireSuccess(attachPoint->attach(&frameMonitor), + "failed to attach OpenCSD raw frame monitor"); +} + +void OpenCsdTreeSession::validateChannel(std::uint8_t channel) const +{ + if (m_sourceType == OCSD_TRC_SRC_SINGLE) { + if (channel != 0U) { + throw OpenCsdTreeSessionError("OpenCSD SINGLE tree requires decoder channel 0"); + } + return; + } + if (!OCSD_IS_VALID_CS_SRC_ID(channel)) { + throw OpenCsdTreeSessionError("OpenCSD formatted tree requires a decoder channel between 1 and 111"); + } +} + ocsd_datapath_resp_t OpenCsdTreeSession::traceDataIn(ocsd_datapath_op_t operation, ocsd_trc_index_t index, std::uint32_t size, const std::uint8_t* data, std::uint32_t* processed) diff --git a/tools/ctrace/src/decode/OpenCsdTreeSession.h b/tools/ctrace/src/decode/OpenCsdTreeSession.h index de99ac2c0..bbfbde960 100644 --- a/tools/ctrace/src/decode/OpenCsdTreeSession.h +++ b/tools/ctrace/src/decode/OpenCsdTreeSession.h @@ -20,6 +20,7 @@ class CSConfig; class DecodeTree; class ITraceErrorLog; class ITrcGenElemIn; +class ITrcRawFrameIn; class ITrcTypedBase; /** @brief Reports an OpenCSD tree-session creation or API failure. */ @@ -107,6 +108,12 @@ class OpenCsdTreeSession final { * @param packetMonitor Raw protocol-packet monitor attached to the packet processor. */ void attachDecoderCallbacks(std::uint8_t channel, ITrcTypedBase& packetMonitor); + /** + * @brief Attaches the one raw-frame monitor supported by a formatted tree. + * @param frameMonitor Monitor receiving deformatter observations. + * @throws OpenCsdTreeSessionError If this is not a formatted tree or attachment fails. + */ + void attachRawFrameMonitor(ITrcRawFrameIn& frameMonitor); /** @brief Routes one data-path operation through the DecodeTree root. */ ocsd_datapath_resp_t traceDataIn(ocsd_datapath_op_t operation, ocsd_trc_index_t index, std::uint32_t size, const std::uint8_t* data, std::uint32_t* processed); @@ -123,9 +130,14 @@ class OpenCsdTreeSession final { /** @brief Returns production DecodeTree construction operations. */ static TreeLifecycle defaultLifecycle(); + /** @brief Rejects source types unsupported by the common ctrace tree wrapper. */ + static ocsd_dcd_tree_src_t validateSourceType(ocsd_dcd_tree_src_t sourceType); + /** @brief Enforces the channel namespace of the configured tree source type. */ + void validateChannel(std::uint8_t channel) const; // Declaration order is intentional: reverse destruction removes the tree // before the global logger lease can restore its predecessor. + ocsd_dcd_tree_src_t m_sourceType; ITraceErrorLog& m_errorLogger; std::unique_ptr m_loggerLease; std::unique_ptr m_tree; diff --git a/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp b/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp index f3b5d0d29..e7ec2c110 100644 --- a/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp +++ b/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp @@ -47,6 +47,8 @@ static std::string displayErrorMessage(const TraceEvent& event, const TraceIssue return atRawOffset("invalid ITM packet header", event); case TraceIssueCode::OpenCsdIncompleteTail: return "incomplete ITM packet starting at raw offset " + std::to_string(event.index) + " at end of input"; + case TraceIssueCode::OpenCsdFormattedInputError: + return issue.message.empty() ? atRawOffset("formatted trace input error", event) : issue.message; case TraceIssueCode::OpenCsdNoProgress: return atRawOffset("OpenCSD made no decode progress", event); case TraceIssueCode::OpenCsdWaitTimeout: diff --git a/tools/ctrace/src/model/TraceEvent.h b/tools/ctrace/src/model/TraceEvent.h index 6c2e549ee..71bd93f41 100644 --- a/tools/ctrace/src/model/TraceEvent.h +++ b/tools/ctrace/src/model/TraceEvent.h @@ -53,6 +53,7 @@ enum class TraceIssueCode { OpenCsdNoProgress, OpenCsdWaitTimeout, OpenCsdInitializationError, + OpenCsdFormattedInputError, }; /** @brief Contains one decoded ITM software stimulus event. */ diff --git a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp index 4eeab91c5..8fa9f29b5 100644 --- a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp +++ b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp @@ -131,6 +131,24 @@ std::size_t countOccurrences(std::string_view text, std::string_view value) return count; } +std::size_t countCsvStreamRows(std::string_view csv, std::string_view stream) +{ + std::size_t count = 0U; + auto lineStart = csv.find('\n'); + while (lineStart != std::string_view::npos && lineStart + 1U < csv.size()) { + ++lineStart; + const auto lineEnd = csv.find('\n', lineStart); + const auto firstComma = csv.find(',', lineStart); + const auto secondComma = firstComma == std::string_view::npos ? firstComma : csv.find(',', firstComma + 1U); + if (firstComma != std::string_view::npos && secondComma != std::string_view::npos && + csv.substr(firstComma + 1U, secondComma - firstComma - 1U) == stream) { + ++count; + } + lineStart = lineEnd; + } + return count; +} + std::string normalizeGeneratedTextLineEndings(std::string text, std::string_view artifact) { std::string normalized; @@ -318,6 +336,111 @@ TEST_F(CtraceIntegTests, RejectsPartialFormattedFrameBeforeCreatingArtifacts) EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Partial.TB.traceanalysis.xml")); } +TEST_F(CtraceIntegTests, SkipsUnsupportedFormattedSourceOnceAndKeepsConfiguredRoute) +{ + writeFile(workDirectory() / "Mixed.ctrace-run.yml", R"yml(ctrace-run: + trace-format: formatted + ctrace-setup: + - pname: core + timestamps: + clock: 400000000 + ctrace-refs: + - ctrace-ref: core/itm + type: itm + pname: core + stream: 1 +)yml"); + // Memory-aligned formatter frames contain two ID-42 runs between clean ID-1 ITM packets. + constexpr std::array raw{{ + 0x03U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x55U, 0x80U, 0xdeU, 0xadU, 0x03U, 0x09U, 0x55U, 0x41U, 0xbeU, 0x48U, + 0x03U, 0xefU, 0x10U, 0x42U, 0x01U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x03U, + }}; + writeFile(workDirectory() / "Mixed.TB.raw", + {reinterpret_cast(raw.data()), static_cast(raw.size())}); + + const auto result = run({"ctrace", workDirectory().string(), "--target", "Mixed", "--all"}); + EXPECT_EQ(0, result.exitCode) << result.stderrText; + EXPECT_EQ(countOccurrences(result.stderrText, "skipping unsupported formatted CoreSight trace source"), 1U); + expectContains(result.stderrText, "stream=42"); + EXPECT_EQ("cycles,stream,type,source,value,pc,address,note\n" + "0,1,itm,1,0x41,,,\n" + "0,1,itm,2,0x42,,,\n", + readTextFile(workDirectory() / "Mixed.TB.csv")); + expectNonEmptyFile(workDirectory() / "Mixed.ctf" / "stream_1"); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Mixed.ctf" / "stream_42")); + expectNonEmptyFile(workDirectory() / "Mixed.TB.traceanalysis.xml"); +} + +TEST_F(CtraceIntegTests, AbortsAllOutputsOnFormattedProtocolError) +{ + writeFile(workDirectory() / "Invalid.ctrace-run.yml", R"yml(ctrace-run: + trace-format: formatted + ctrace-setup: + - pname: core + timestamps: + clock: 400000000 + ctrace-refs: + - ctrace-ref: core/itm + type: itm + pname: core + stream: 1 +)yml"); + // ID 1 carries a hardware sync followed by the reserved ITM header 0x04. + constexpr std::array raw{{ + 0x03U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x80U, + 0x04U, + 0x01U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + }}; + writeFile(workDirectory() / "Invalid.TB.raw", + {reinterpret_cast(raw.data()), static_cast(raw.size())}); + + const auto result = run({"ctrace", workDirectory().string(), "--target", "Invalid", "--all"}); + EXPECT_EQ(1, result.exitCode); + expectContains(result.stderrText, "invalid ITM packet header at raw offset 6"); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Invalid.TB.csv")); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Invalid.ctf")); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Invalid.TB.traceanalysis.xml")); +} + +TEST_F(CtraceIntegTests, AbortsAllOutputsOnFormattedDataBeforeFirstSourceId) +{ + writeFile(workDirectory() / "Unassigned.ctrace-run.yml", R"yml(ctrace-run: + trace-format: formatted + ctrace-setup: + - pname: core + timestamps: + clock: 400000000 + ctrace-refs: + - ctrace-ref: core/itm + type: itm + pname: core + stream: 1 +)yml"); + writeFile(workDirectory() / "Unassigned.TB.raw", std::string(16U, '\0')); + + const auto result = run({"ctrace", workDirectory().string(), "--target", "Unassigned", "--all"}); + EXPECT_EQ(1, result.exitCode); + expectContains(result.stderrText, "formatted trace data has no source ID at raw input offset 0"); + expectContains(result.stderrText, "stream=1"); + expectContains(result.stderrText, "[info] decoded 1 events from 16 bytes"); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Unassigned.TB.csv")); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Unassigned.ctf")); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Unassigned.TB.traceanalysis.xml")); +} + TEST_F(CtraceIntegTests, ExpandsDwtEventCountersAcrossCsvAndCtf) { writeFile(workDirectory() / "Events.ctrace-run.yml", R"yml(ctrace-run: @@ -351,8 +474,24 @@ TEST_F(CtraceIntegTests, ConvertsDwtMatchAcrossCsvAndCtf) // This Armv8-M packet stream is completely synthetic and was generated from // the architecture specification without a capture from real hardware. constexpr std::array expectedRaw{{ - 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x45U, 0x01U, 0x10U, - 0x55U, 0x01U, 0x20U, 0x65U, 0x01U, 0x30U, 0x75U, 0x01U, 0x40U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x80U, + 0x45U, + 0x01U, + 0x10U, + 0x55U, + 0x01U, + 0x20U, + 0x65U, + 0x01U, + 0x30U, + 0x75U, + 0x01U, + 0x40U, }}; EXPECT_EQ(readBinaryFile(workDirectory() / "trace-match.SWO.raw"), std::vector(expectedRaw.begin(), expectedRaw.end())); @@ -481,8 +620,7 @@ TEST_F(CtraceIntegTests, ExpandsPmuEventCountersAcrossCsvAndCtf) readTextFile(workDirectory() / "Pmu.SWO.csv")); expectContains(readTextFile(workDirectory() / "Pmu.ctf" / "metadata"), "name = \"PMU_EVENT\""); expectNonEmptyFile(workDirectory() / "Pmu.ctf" / "stream_0"); - expectContains(readTextFile(workDirectory() / "Pmu.SWO.traceanalysis.xml"), - ""), + std::string::npos); + EXPECT_NE(xml.find(""), std::string::npos); + EXPECT_NE(xml.find(""), + std::string::npos); + EXPECT_NE(xml.find(""), + std::string::npos); + const auto matchHandler = xml.find(""); + const auto matchHandlerEnd = xml.find("", matchHandler); + ASSERT_NE(matchHandler, std::string::npos); + ASSERT_NE(matchHandlerEnd, std::string::npos); + const auto matchPulseEnd = xml.find("value=\"timestamp + 1000\"", matchHandler); + ASSERT_NE(matchPulseEnd, std::string::npos); + EXPECT_LT(matchPulseEnd, matchHandlerEnd); + EXPECT_NE(xml.find("value=\"cmsis_dwt_address_type\""), std::string::npos); + EXPECT_NE(xml.find("value=\"cmsis_dwt_address.u8\" forcedType=\"long\""), std::string::npos); + EXPECT_NE(xml.find("value=\"cmsis_dwt_address.u16\" forcedType=\"long\""), std::string::npos); + EXPECT_NE(xml.find("value=\"cmsis_dwt_address.u32\" forcedType=\"long\""), std::string::npos); + const auto threadModeEntry = xml.find("path=\"EXCEPTION/Thread Mode\""); + const auto returnEntry = xml.find("path=\"EXCEPTION_RETURN/*\" displayText=\"true\""); + const auto interruptEntries = xml.find("path=\"EXCEPTION/(?!Thread Mode).+\""); + ASSERT_NE(threadModeEntry, std::string::npos); + ASSERT_NE(returnEntry, std::string::npos); + ASSERT_NE(interruptEntries, std::string::npos); + EXPECT_LT(threadModeEntry, returnEntry); + EXPECT_LT(returnEntry, interruptEntries); +} + +TEST(CtraceUnitTests, testTraceCompassXmlScopesGraphicalViewsPerRoute) +{ + const TemporaryTestPath path("ctrace-trace-compass-route-views.xml"); + TraceCompassXmlWriter::writeRoutedFile(path.path(), {{1U, "CM4"}, {2U, "CM&<7>\"'"}, {3U, {}}}); + const auto xml = readTestTextFile(path.path()); + + EXPECT_NE(xml.find("id=\"arm.cmsis.swo.xy.dwt_value.stream1.v1\""), std::string::npos); + EXPECT_NE(xml.find("id=\"arm.cmsis.swo.xy.dwt_value.stream2.v1\""), std::string::npos); + EXPECT_NE(xml.find("