diff --git a/.github/matrix_includes_ctrace.json b/.github/matrix_includes_ctrace.json index 38b83e13e..013ef9bef 100644 --- a/.github/matrix_includes_ctrace.json +++ b/.github/matrix_includes_ctrace.json @@ -3,6 +3,7 @@ "runs_on":"macos-14", "target":"darwin", "arch": "arm64", + "run_tests": true, "binary": "ctrace", "runOn": "publicRepo" }, @@ -10,6 +11,7 @@ "runs_on":"ubuntu-24.04", "target":"linux", "arch": "amd64", + "run_tests": true, "binary": "ctrace", "runOn": "always" }, @@ -17,6 +19,7 @@ "runs_on":"ubuntu-24.04", "target":"linux", "arch": "arm64", + "run_tests": false, "binary": "ctrace", "runOn": "always" }, @@ -24,6 +27,7 @@ "runs_on":"windows-2022", "target":"windows", "arch": "amd64", + "run_tests": true, "binary": "ctrace.exe", "runOn": "always" }, @@ -31,6 +35,7 @@ "runs_on":"windows-2022", "target":"windows", "arch": "arm64", + "run_tests": false, "binary": "ctrace.exe", "runOn": "always" } diff --git a/.github/workflows/ctrace.yml b/.github/workflows/ctrace.yml index 60cb78201..5aab81633 100644 --- a/.github/workflows/ctrace.yml +++ b/.github/workflows/ctrace.yml @@ -33,6 +33,7 @@ permissions: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + BABELTRACE2_DEB_VERSION: 2.0.5-3build2 jobs: setup: @@ -181,6 +182,14 @@ jobs: with: submodules: true + - name: Install pinned Babeltrace consumer + if: matrix.run_tests && matrix.target == 'linux' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + babeltrace2=${{ env.BABELTRACE2_DEB_VERSION }} \ + libbabeltrace2-0=${{ env.BABELTRACE2_DEB_VERSION }} + # https://github.com/Open-CMSIS-Pack/devtools-build-action - name: Build CtraceUnitTests id: build_unit_tests @@ -201,19 +210,26 @@ jobs: - name: Run ctrace unit tests id: run_unit_tests - if: always() && (matrix.arch != 'arm64') && (steps.build_unit_tests.outcome == 'success') + if: always() && matrix.run_tests && (steps.build_unit_tests.outcome == 'success') run: ctest -V -C Debug -R '^CtraceUnitTests$' working-directory: ./build - name: Run ctrace integration tests id: run_integration_tests - if: always() && (matrix.arch != 'arm64') && (steps.build_integration_target.outcome == 'success') - run: ctest -V -C Debug -R '^(CtraceIntegTests|ctrace-)' + if: always() && matrix.run_tests && (steps.build_integration_target.outcome == 'success') + run: ctest -V -C Debug -R '^(CtraceIntegTests|CtraceFixtureIntegrity|ctrace-)' + working-directory: ./build + + - name: Run pinned Babeltrace consumer test + if: | + always() && matrix.run_tests && matrix.target == 'linux' && + (steps.build_integration_target.outcome == 'success') + run: ctest -V -C Debug --no-tests=error -L '^linux-consumer$' working-directory: ./build - name: Archive unit test results # Keep the report available when test execution fails. - if: always() && (matrix.arch != 'arm64') && (steps.run_unit_tests.outcome != 'skipped') + if: always() && matrix.run_tests && (steps.run_unit_tests.outcome != 'skipped') uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: unit_test_result_ctrace-${{ matrix.target }}-${{ matrix.arch }} @@ -223,7 +239,7 @@ jobs: - name: Archive integration test results # Keep the report available when test execution fails. - if: always() && (matrix.arch != 'arm64') && (steps.run_integration_tests.outcome != 'skipped') + if: always() && matrix.run_tests && (steps.run_integration_tests.outcome != 'skipped') uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: integ_test_result_ctrace-${{ matrix.target }}-${{ matrix.arch }} @@ -256,6 +272,13 @@ jobs: with: submodules: true + - name: Install pinned Babeltrace consumer + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + babeltrace2=${{ env.BABELTRACE2_DEB_VERSION }} \ + libbabeltrace2-0=${{ env.BABELTRACE2_DEB_VERSION }} + # https://github.com/Open-CMSIS-Pack/devtools-build-action - name: Build CtraceUnitTests uses: Open-CMSIS-Pack/devtools-build-action@5b24a2b5145eb406a664269b3704bb983c19242d # arm64 @@ -276,7 +299,11 @@ jobs: working-directory: ./build - name: Run ctrace integration tests - run: ctest -V -C Debug -R '^(CtraceIntegTests|ctrace-)' + run: ctest -V -C Debug -R '^(CtraceIntegTests|CtraceFixtureIntegrity|ctrace-)' + working-directory: ./build + + - name: Run pinned Babeltrace consumer test + run: ctest -V -C Debug --no-tests=error -L '^linux-consumer$' working-directory: ./build - name: Get retention days @@ -310,6 +337,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 +346,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 +391,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..db5335375 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 cleanup and return epilogues to otherwise +# covered source lines. Stabilize that attribution only in coverage-instrumented +# ctrace objects; release, debug, and external-dependency builds stay 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/README.md b/tools/ctrace/README.md index 3aa6d9123..f0faf09df 100644 --- a/tools/ctrace/README.md +++ b/tools/ctrace/README.md @@ -14,7 +14,7 @@ ctrace [options] --ctf Generate CTF and Trace Compass XML output -a, --all Generate all output formats --type Select event types - --stream Select CoreSight Trace Bus IDs (0 to 111) + --stream Select streams (0 for unformatted; ATB IDs 1 to 111) -h, --help Print command-line help -V, --version Print the version ``` @@ -32,7 +32,7 @@ Input and output files share a solution-set base name: .trace/ Board.ctrace-run.yml Board.SWO.raw - Board.TB.raw # optional Trace Bus input + Board.TB.raw # optional Trace Buffer input ``` For `ctrace .trace --target Board --all`, the supported input produces: @@ -43,9 +43,19 @@ For `ctrace .trace --target Board --all`, the supported input produces: Board.ctf/ metadata stream_0 - Board.SWO.traceanalysis.xml + Board.SWO.traceanalysis.xml # when retained streams use one clock domain; views are data-driven ``` +Without an explicit format declaration, ctrace preserves the legacy SWO-only +selection and decodes `Board.SWO.raw` as unformatted ITM. The ctrace-private +provisional root field `trace-format: unformatted | formatted` makes SWO, TB, +and named-TB inputs eligible under the declared byte format; discovery then +requires exactly one eligible input. The field does not identify a particular +file. Formatted input currently requires complete 16-byte memory-aligned +CoreSight frames; there is no public `trace-framing` field yet. See the +[constraints](docs/constraints.md) for the full discovery, routing, and +compatibility contract. + ## Build and test Initialize all dependencies and configure the repository from its root: @@ -59,9 +69,20 @@ cmake --build build --target ctrace CtraceUnitTests CtraceIntegTests Run the GoogleTest unit and integration suites plus the executable smoke tests: ```bash -ctest --test-dir build -C Debug -R '^(CtraceUnitTests|CtraceIntegTests|ctrace-)' +ctest --test-dir build -C Debug -R '^(CtraceUnitTests|CtraceIntegTests|CtraceFixtureIntegrity|ctrace-)' +``` + +On native Linux, installing exactly Babeltrace `2.0.5` before configuration +also registers the external consumer gate: + +```bash +ctest --test-dir build -C Debug --no-tests=error -L '^linux-consumer$' ``` +CI installs the pinned consumer and treats a missing labelled test as a +failure. The separate versioned Trace Compass acceptance record is documented +with the [integration tests](test/integration/README.md). + Editors using `clangd` should open the devtools repository root and configure into `build`. The tool-local `.clangd` file points clangd at that compilation database. diff --git a/tools/ctrace/docs/OpenCSD-NOTICE.txt b/tools/ctrace/docs/OpenCSD-NOTICE.txt index d475ee88e..0ddb4869b 100644 --- a/tools/ctrace/docs/OpenCSD-NOTICE.txt +++ b/tools/ctrace/docs/OpenCSD-NOTICE.txt @@ -2,9 +2,8 @@ OpenCSD 1.8.3 https://github.com/Linaro/OpenCSD The ctrace executable incorporates unmodified OpenCSD source code from the -pinned revision. The following copyright notices are retained from the -incorporated OpenCSD source files (capitalization and punctuation are -preserved): +pinned revision. The following copyright notices are retained from the pinned +OpenCSD source tree (capitalization and punctuation are preserved): Copyright (c) 2015, ARM Limited. All Rights Reserved. Copyright (c) 2015, 2019 ARM Limited. All Rights Reserved. diff --git a/tools/ctrace/docs/architecture.md b/tools/ctrace/docs/architecture.md index 76e4b3d40..308656615 100644 --- a/tools/ctrace/docs/architecture.md +++ b/tools/ctrace/docs/architecture.md @@ -11,16 +11,14 @@ constraints](constraints.md) record preserved contracts; the compact [TODO list] `ctrace` combines a trace-run configuration with raw CoreSight trace data and converts supported trace channels into backend-independent semantic events. Output backends consume these events to create CSV or CTF artifacts. -The first release profile supports SWO data containing ITM and DWT packets. The command line accepts the stable type -names `itm`, `dwt`, `event`, `pmu`, `exception`, `pcsample`, `global_ts`, `overflow`, and `error`. Output semantics are -currently implemented for every listed type. Valid DWT event-counter and PMU trace-on-overflow packets reach CSV as -one row containing the hardware mask. The CTF backend expands each mask into one timestamped record per set bit so -Trace Compass can show exact table rows and labeled one-microsecond visualization pulses. DWT records use their fixed -architectural counter names; PMU records provisionally use `Event0` through `Event7` until trace-run configuration can -resolve the programmable counter assignments. Periodic PC samples reach CSV and CTF as semantic events; the CTF event -distinguishes a sampled PC from a processor-sleep indication, and Trace Compass shows processor-sleep intervals as a -timeline. Trace Bus input is discovered so that a complete trace directory can be inspected, but `*.TB.raw` files are -reported and skipped until a decoder is implemented. +The current profile supports unformatted ITM byte streams from SWO or explicitly declared TB input, and +memory-aligned formatted CoreSight input carrying ITM and DWT packets. Each configured ITM route supports the public +event selections `itm`, `dwt`, `event`, `pmu`, `exception`, `pcsample`, `global_ts`, `overflow`, and `error`. +Backend-specific representations are documented in the [CTF profile](ctf-format.md), not in the decoder contract. + +Exactly one raw input is active for each trace-run configuration. Formatted input distributes bytes to configured +ITM routes by Trace Bus ID; unformatted input uses one synthetic route. Other protocols require explicit decoder +integration, not guesses based on observed IDs. Deferred inputs and decoders are tracked in the [TODO list](todo.md). The architecture separates protocol decoding, semantic interpretation, and output generation. This keeps output formats independent of OpenCSD and allows another raw trace channel to reuse the event model and output backends. @@ -33,21 +31,22 @@ raw input, and generated output files are grouped by their common base name. The main in-memory path is: ```text -command line + trace-run YAML + SWO raw file +command line + trace-run YAML + selected SWO/TB raw file | v TraceDirectoryJob / FileDecodeJob - discovery, metadata, output plan + input descriptor, routes, output plan | v 64 KiB non-owning byte views | v - OpenCSD - ITM framing, packets, recovery + OpenCSD DecodeTree session + SINGLE or FRAME_FORMATTED root + route-bound ITM decoders | v - OpenCsdTraceElement values + routed OpenCsdTraceElement values | v CortexMStreamDecoder / CortexMPostDecoder @@ -66,44 +65,84 @@ The `TraceEvent` boundary is the central design point. Before it, code handles b recovery, and Cortex-M state. After it, code sees backend-independent events in decode order and does not depend on OpenCSD types. +The similarly named decode types are consecutive pipeline stages, not interchangeable implementations of one decoder +contract. `OpenCsdItmDecoder` decodes transport bytes, `CortexMStreamDecoder` routes normalized elements, and each +`CortexMPostDecoder` reconstructs semantic events with help from `DwtPacketDecoder`. Common interfaces exist only at +actual substitution boundaries such as `OpenCsdItmSessionInterface`, `OpenCsdTraceElementSink`, and `TraceEventSink`; +there is deliberately no common base class for all decode stages. + +## Input and compatibility contract + +Input selection and route binding belong to `tracerun` and complete before decoder or output construction. +The provisional, ctrace-private `trace-format` declaration describes effective capture bytes rather than target +capability or file identity. This keeps format selection explicit while preserving legacy SWO-only discovery until +the producer contract supplies an unambiguous input identity and format. Framing remains an internal decoder contract. + +The [input constraints](constraints.md#input-format-framing-and-discovery) define the accepted declarations, defaults, +file candidates, and framing limits. The [routing invariants](constraints.md#routing-invariants) define reference +binding, valid IDs, and the synthetic unformatted route; backends preserve its legacy output representation. + ## Processing state and ownership -One `DecodePipeline` is created for each supported raw file. It owns the OpenCSD adapter and Cortex-M stream decoder, -so protocol, timestamp, and DWT state survive arbitrary file-read boundaries. `RawFileReader` owns a single 64 KiB -buffer; each `RawByteView` borrows that buffer only for the synchronous `DecodePipeline::push` call. Calling +One `DecodePipeline` is created for the selected raw file. It owns the OpenCSD adapter and Cortex-M stream decoder, so +protocol, formatter, timestamp, and DWT state survive arbitrary file-read boundaries. `RawFileReader` owns a single +64 KiB buffer; each `RawByteView` borrows that buffer only for the synchronous `DecodePipeline::push` call. Calling `DecodePipeline::finish` flushes both the OpenCSD and Cortex-M layers before the pipeline is destroyed. -`CortexMStreamDecoder` maintains an independent post-decoder for each observed Trace Bus ID. All post-decoders emit -into the same `TraceEventSink`, preserving input order while keeping stream-specific timestamp and DWT state apart. +Both input formats use the same `OpenCSD DecodeTree` ownership boundary. `SINGLE` connects one synthetic, no-ATB-ID +route to one ITM decoder. `FRAME_FORMATTED` owns the frame deformatter and one route-bound ITM decoder for each +configured Trace Bus ID. Ctrace creates and feeds one tree at a time because OpenCSD's alternate logger and live-tree +registry use process-global state; the session restores the previously installed logger when it is destroyed. + +Ownership is deliberately split by responsibility while preserving one enclosing lifetime: `OpenCsdTreeSession` +owns the tree and configured decoder components. For formatted input, `OpenCsdFormattedItmSession` additionally owns +the route adapters, packet/frame monitors, and captured callback errors installed in that tree. For SINGLE input, +the decoder implementation owns the packet collector and error controller that `OpenCsdItmSession` connects +directly. Members are ordered so the tree is destroyed first, before any callback target or diagnostic state it can +reference. This keeps format-specific feed and recovery policy out of the low-level tree wrapper without weakening +callback lifetime safety. + +`CortexMStreamDecoder` maintains an independent post-decoder for each normalized route. All post-decoders emit into +the same `TraceEventSink`, preserving input order while keeping route-specific timestamp and DWT state apart. There is no application-wide event queue. `DecodeConsumers` forwards each event synchronously to the output lifecycle and issue reporter. ## Recovery after damaged trace -Recoverable OpenCSD packet errors establish a discontinuity at the reported raw-file offset. Decoder callbacks before -that offset are retained; callbacks at or after it belong to the failed transaction and are discarded. `ctrace` then -resets the OpenCSD decoder and feeds the following input bytes to it unchanged. OpenCSD resumes only after finding a -real ITM hardware-sync sequence; `ctrace` never inserts a synthetic sync sequence. +Recoverable OpenCSD packet errors establish a discontinuity on the affected route at the reported raw-file offset. +Callbacks before that offset are retained. Route-aware transaction buffering discards callbacks from the failing +route at or after it while retaining callbacks from other routes in their original order. Error callbacks only +capture a stable batch; rollback and reset decisions happen after the synchronous OpenCSD operation returns. + +For a known ITM route, `ctrace` resets only that route's packet-processor/full-decoder chain. The formatted +deformatter, its current source ID, partially delivered frame, and every other route remain intact. A bounded root +flush drains already unpacked frame segments before the next aligned input block. The raw cursor advances only by the +byte count returned by the DecodeTree, so consumed bytes are never fed twice. OpenCSD resumes the affected route only +after finding a real ITM hardware-sync sequence; `ctrace` never inserts a synthetic sync sequence. -Bytes consumed while no usable trace elements are produced form one explicit `data-loss` interval. At its boundary, -the Cortex-M post-decoder flushes pending events, resets incomplete DWT correlation, and marks timestamps unreliable -until the stream supplies enough timing information again. The issue remains part of the ordered `TraceEvent` stream, -so diagnostics and enabled output backends observe the same recovery boundary. +Bytes lost before resynchronization form one explicit route-bound `data-loss` interval. At its boundary, that route's +Cortex-M post-decoder flushes pending events, resets incomplete DWT correlation, and marks timestamps unreliable until +the stream supplies enough timing information again. A route that does not resynchronize closes its interval as +unresolved at end of input. The issue remains part of the ordered `TraceEvent` stream, so diagnostics and enabled +output backends observe the same recovery boundary. -Failure to reset OpenCSD, repeated lack of decoder progress, or an unsuccessful wait/flush operation aborts the -current raw-file job. +An error without a usable route, a deformatter error, failure to reset the route, repeated lack of decoder progress, +or an unsuccessful bounded wait/flush operation aborts the current raw-file job and every active output. ## Suggested code-reading path 1. Start at [`CtraceMain.cpp`](../src/CtraceMain.cpp) for command-line handling and top-level error policy. -2. Follow [`TraceDirectoryJob.cpp`](../src/control/TraceDirectoryJob.cpp) to see how solution sets, YAML, SWO, and - unsupported Trace Bus input are associated. +2. Follow [`TraceDirectoryJob.cpp`](../src/control/TraceDirectoryJob.cpp) and + [`TraceRunDiscovery.cpp`](../src/tracerun/TraceRunDiscovery.cpp) to see how a solution set, YAML, and exactly one + SWO/TB input become a preflighted descriptor. 3. Read [`FileDecodeJob.cpp`](../src/control/FileDecodeJob.cpp) for output preflight, chunked input, pipeline construction, and finalization. 4. Continue through [`DecodePipeline.cpp`](../src/decode/DecodePipeline.cpp), - [`OpenCsdItmDecoder.cpp`](../src/decode/OpenCsdItmDecoder.cpp), and - [`CortexMStreamDecoder.cpp`](../src/decode/CortexMStreamDecoder.cpp) for the two decode representations. + [`OpenCsdItmDecoder.cpp`](../src/decode/OpenCsdItmDecoder.cpp), + [`OpenCsdTreeSession.cpp`](../src/decode/OpenCsdTreeSession.cpp), and + [`CortexMStreamDecoder.cpp`](../src/decode/CortexMStreamDecoder.cpp) for the tree, protocol, and semantic decode + representations. 5. Use [`TraceEvent.h`](../src/model/TraceEvent.h) as the semantic contract between decoding and all consumers. 6. Finish with [`DecodeConsumers.cpp`](../src/control/DecodeConsumers.cpp) and [`TraceOutputLifecycle.cpp`](../src/output/TraceOutputLifecycle.cpp), then inspect either the CSV or CTF backend. @@ -132,10 +171,12 @@ must not depend on control jobs or command-line details. | --- | --- | | `src/tracerun` | File discovery, YAML parsing, schema subset validation, and normalized metadata | -The YAML reader's validation and metadata rules are recorded in the [constraints](constraints.md#boundaries). +The YAML reader's validation, provisional input-format, and metadata rules are recorded in the +[constraints](constraints.md). -`CtraceRunMeta` is the boundary between the YAML representation and runtime processing. Decode and output modules use -normalized metadata instead of navigating YAML nodes. +`CtraceRunMeta` is the boundary between the YAML representation and runtime processing. `TraceRunInputDescriptor` +combines it with the selected open raw file and effective format. Decode and +output modules consume these normalized values instead of navigating YAML nodes or repeating discovery decisions. ### Decode and event model @@ -149,8 +190,10 @@ modules. The post-decoder maps them to `TraceEvent` variants such as software tr overflow, and trace issues. `TraceSelection` owns the stable public type names and maps semantic events onto that release-facing set. -Each event can retain its raw index, Trace Bus ID, timestamp, and quality state. This allows diagnostics and backends -to make independent decisions without reconstructing decoder state. +Each event retains a normalized route identity and can retain its raw index, architectural Trace Bus ID, timestamp, +and quality state. The private route ID distinguishes even routes without an ATB ID; it is never exposed as a public +stream number. This allows diagnostics and backends to make independent decisions without reconstructing decoder +state. ### Output @@ -160,14 +203,28 @@ to make independent decisions without reconstructing decoder state. | `src/output/csv` | Stable CSV schema, row mapping, filtering, and file output | | `src/output/ctf` | CTF metadata and stream encoding plus Trace Compass analysis XML | -The generated event IDs, fields, enum values, quality markers, and visualization semantics are specified in the -[ctrace CTF profile](ctf-format.md). +Output requirements are evaluated per backend and selected route. For example, missing CTF-specific metadata on an +active route may disable CTF while an independent CSV output remains valid; metadata on a route excluded by the +stream filter is not required. `--all` therefore does not make the backends share failure state unnecessarily. + +CSV writes one combined file in semantic callback order. `CtfBundleOutput` owns a bundle-local metadata model and +lazily creates a stream writer for each formatted route that emits selected events. Representation changes stay in +the backends: for example, CSV retains a DWT/PMU counter mask in one row while CTF expands it into individual records. -Output requirements are evaluated per backend. For example, missing CTF-specific metadata may disable CTF while an -independent CSV output remains valid. `--all` therefore does not make the backends share failure state unnecessarily. +CTF finalization retains only emitted streams, then generates Trace Compass XML from their observed graphical topics. +This avoids empty views and invented durations for point events. Route identity stays separate from display labels, +so equal processor names cannot merge views. Formatted routes retain distinct clock domains because the input contract +does not establish cross-route synchronization. Multi-clock data remains valid CTF but cannot safely drive the supported +reader's combined XML analysis. -Outputs use an explicit `start`, `writeEvent`, `stop`, and `abort` lifecycle. A successful backend can finish even if -another backend fails. Decode or finalization failures trigger cleanup of incomplete artifacts. +The [CTF profile](ctf-format.md) defines event schemas, metadata, clock mappings, legacy layouts, and +[XML projection](ctf-format.md#generated-trace-compass-analysis). Cross-backend compatibility and failure rules belong +to the [output constraints](constraints.md#observable-behavior-and-output-safety). + +Outputs use an explicit `start`, `writeEvent`, `stop`, and `abort` lifecycle. `TraceOutput` owns the active state and +failure cleanup; concrete backends implement only the protected prepare, start, write, stop, and abort hooks. A +successful backend can finish even if another backend fails. Decode or finalization failures trigger cleanup of +incomplete artifacts. ## Diagnostics and failure semantics @@ -186,8 +243,9 @@ status. Unhandled internal ctrace failures also terminate the command after an e ## External dependencies `cxxopts` provides command-line parsing, `yaml-cpp` is confined to the trace-run reader, OpenCSD is isolated behind -the decode adapters, and GoogleTest is used only by test targets. Exact revisions, licenses, and dependency build -configuration are documented in the [third-party notices](THIRD_PARTY_NOTICES.md). +the decode adapters, and GoogleTest is used only by test targets. Babeltrace and Trace Compass are acceptance +consumers, not linked runtime dependencies. Exact revisions, licenses, and build configuration for the packaged +dependencies are documented in the [third-party notices](THIRD_PARTY_NOTICES.md). The ITM adapter currently uses OpenCSD `common/` and `interfaces/` headers because the public OpenCSD 1.8.3 boundary does not provide equivalent access: its installed headers omit the ITM configuration and packet types required by the @@ -201,8 +259,10 @@ A known decoder defect and its proposed upstream fix are recorded in the [OpenCS ### Add a raw trace channel Implement a decoder that produces `TraceEvent` values, add it below `src/decode`, and select it in the control layer -for the corresponding channel. A Trace Bus implementation should replace the current warning and skip behavior while -reusing diagnostics, event selection, and output backends. +for the corresponding channel. A new protocol carried by formatted CoreSight input additionally needs an explicit +protocol route from configuration; an observed unknown formatter ID is deliberately not enough to choose a decoder. +Reuse the input descriptor, diagnostics, event selection, and output backends rather than creating a second raw-input +architecture. ### Add an event type @@ -211,8 +271,9 @@ backend that can represent the event. Tests should cover semantic mapping separa ### Add an output backend -Implement `TraceOutput`, define backend-specific preflight requirements, and add it to the output plan and lifecycle. -Do not introduce backend-specific state into the decode pipeline or event model. +Derive from `TraceOutput`, implement its backend hooks, define backend-specific preflight requirements, and add it to +the output plan and lifecycle. The concrete destructor must call `abortNoexcept()` while its backend members are still +alive. Do not introduce backend-specific state into the decode pipeline or event model. ## Test architecture @@ -224,13 +285,20 @@ Executable-level coverage and fixture ownership are documented next to the ## Build and CI structure -The source tree has seven static library targets: `model`, `cli`, `trace-run`, `diagnostics`, `decode`, `output`, and -`control`. The shared `ctracelib` object contains `CtraceMain`; the executable adds only the platform trampoline and -manifest where required. Dependencies form a directed, cycle-free graph with `control` as the composition root. - -The tool-specific GitHub workflow is selected by a `tools/ctrace/` release tag. It builds Windows AMD64 and -Arm64, Linux AMD64 and Arm64, and macOS Arm64 binaries. Unit and integration tests run on Windows AMD64 and Linux -AMD64; the remaining targets are compile-only. -The version compiled into the executable is derived from the same tag. Archive contents and license material are -described in the [third-party notices](THIRD_PARTY_NOTICES.md); unfinished release work remains in the +The source tree has seven static library targets: `ctrace-model`, `ctrace-cli`, `ctrace-trace-run`, +`ctrace-diagnostics`, `ctrace-decode`, `ctrace-output`, and `ctrace-control`. Their `ctrace::` aliases expose the +shorter module names inside CMake. The shared `ctracelib` object contains `CtraceMain`; the executable adds only the +platform trampoline and manifest where required. Dependencies form a directed, cycle-free graph with `control` as +the composition root. + +The tool-specific GitHub workflow is triggered for matching pull requests and pushes to `main` and reacts to +published releases. Only its release job is selected by a `tools/ctrace/` release tag. The build matrix +covers Windows AMD64 and Arm64, Linux AMD64 and Arm64, and macOS Arm64 binaries. Unit and +integration tests run on Windows AMD64, Linux AMD64, and macOS Arm64; Windows Arm64 and Linux Arm64 remain +compile-only. Native Linux additionally runs the exact Babeltrace 2.0.5 consumer gate on AMD64. CI enforces 100% +source-line coverage for `tools/ctrace/src`; branch coverage is retained for review but is not the merge gate. The +versioned manual Trace Compass Server/TSP acceptance record is documented beside the +[integration tests](../test/integration/README.md). +The release version compiled into the executable is derived from the same tag. Archive contents and license material +are described in the [third-party notices](THIRD_PARTY_NOTICES.md); unfinished release work remains in the [TODO list](todo.md). diff --git a/tools/ctrace/docs/architecture.svg b/tools/ctrace/docs/architecture.svg index 28f40ea53..28f169f1d 100644 --- a/tools/ctrace/docs/architecture.svg +++ b/tools/ctrace/docs/architecture.svg @@ -3,13 +3,14 @@ Copyright (c) 2026 Arm Limited. All rights reserved. SPDX-License-Identifier: Apache-2.0 --> - ctrace software architecture - Seven library modules and the first-release ITM and DWT data flow from command-line, trace-run YAML, and SWO raw - input through orchestration, hardware-sync recovery, the trace event model, DecodeConsumers, diagnostics, and CSV - and CTF output backends. Trace Bus input is discovered but not decoded. + ITM and DWT runtime data flow from command-line, trace-run YAML, and one selected raw input through normalized + routes, mode-specific output planning, a SINGLE or formatted OpenCSD DecodeTree, route-local recovery and + Cortex-M state, semantic trace events, diagnostics, CSV, multi-stream CTF, and conditional Trace Compass XML + output. @@ -40,122 +41,147 @@ .flow { fill: none; stroke: #34495e; stroke-width: 2.2; marker-end: url(#arrow); } .dependency { fill: none; stroke: #66788a; stroke-width: 1.8; stroke-dasharray: 7 5; marker-end: url(#arrow-muted); } - .unsupported { fill: none; stroke: #66788a; stroke-width: 1.8; stroke-dasharray: 2 5; + .conditional { fill: none; stroke: #66788a; stroke-width: 1.8; stroke-dasharray: 2 5; marker-end: url(#arrow-muted); } - .flow, .dependency, .unsupported { stroke-linecap: round; stroke-linejoin: round; } + .flow, .dependency, .conditional { stroke-linecap: round; stroke-linejoin: round; } .group { fill: none; stroke: #d5dde5; stroke-width: 1.2; rx: 14; } .legend { font-size: 12px; fill: #52667a; } - + ctrace architecture - Seven library boundaries · initial ITM/DWT release profile + Runtime data flow · unformatted and formatted ITM/DWT profile INPUTS - - Command line - trace directory, target, filters - - - *.ctrace-run.yml - setup, routes, source metadata - - - *.SWO.raw - ITM and DWT byte stream - - - *.TB.raw - discovered; not decoded yet - - ORCHESTRATION - - - - cli - options · cxxopts - - - tracerun - YAML · yaml-cpp - - - control - read raw files; compose decode jobs - - - diagnostics - severity · context · impact - - - - - - - - - - DECODE PIPELINE - - - - DecodePipeline - RawByteView · chunk continuity - - - OpenCSD adapter - packets · errors · recovery - resume at real hardware sync - - - Cortex-M decode - timestamps and DWT semantics - - - TraceEvent model - events, type mapping, selection - - - OpenCSD 1.8.3 - pinned external library - - - DecodeConsumers - outputs · issue reporting - - - - - - - - - - OUTPUTS - - - - output orchestration - requirements, lifecycle, cleanup - - - CSV backend - *.SWO.csv - - - CTF backend - *.ctf + Trace Compass XML - - - - - - - runtime data flow - - service or library dependency - - unsupported input path + + Command line + mode · target + type / stream filters + + + *.ctrace-run.yml + trace-format · ctrace-setup + ctrace-refs · optional metadata + + + *.SWO.raw + legacy or explicit candidate + effective global format + + + *.TB[_name].raw + explicit candidate + same effective global format + + ORCHESTRATION + + + + cli + normalize and validate options + + + tracerun + YAML reader · CtraceRunMeta + canonical routes · deferred values + discover · preflight one input + + + control + TraceDirectoryJob · FileDecodeJob + descriptor · output plan · 64 KiB reads + + + diagnostics + severity · context · impact + CLI/config · decoder · output failures + + + + + + + + + + DECODE PIPELINE + + + + DecodePipeline + borrowed RawByteView chunks + finish flushes both layers + + + OpenCSD ITM adapter + SINGLE / FRAME_FORMATTED tree + one ITM decoder per route + cursor · transactions · route reset + + + Cortex-M decode + timestamps · DWT pairing + quality state per route + ordered semantic mapping + + + TraceEvent model + route identity · timing quality + stable type selection + backend-independent values + + + OpenCSD 1.8.3 + pinned external library + + + DecodeConsumers + synchronous output / issue fan-out + + + + + + + + + + OUTPUTS + + + + TraceOutput + TraceOutputLifecycle + backend state · multi-backend coordination + prepare · start · write · stop · abort + + + CSV backend + one file · callback order + Trace Bus ID on formatted rows + + + CTF bundle + legacy stream_0 eager · formatted lazy + explicit route clocks · no fallback + + + Trace Compass XML + observed graphical topics only + requires one retained clock domain + + + + + + + + + + runtime data flow + + selected runtime configuration or service use + + conditional artifact diff --git a/tools/ctrace/docs/constraints.md b/tools/ctrace/docs/constraints.md index d5690d3ea..f22cfda99 100644 --- a/tools/ctrace/docs/constraints.md +++ b/tools/ctrace/docs/constraints.md @@ -2,9 +2,11 @@ This document records contracts that implementation changes must preserve. Runtime design and the supported feature profile belong in the [architecture description](architecture.md), working instructions in the [README](../README.md), -and unfinished work in the [TODO list](todo.md). The CMSIS-Toolbox -[trace specification](https://open-cmsis-pack.github.io/cmsis-toolbox/Experimental-Features/) remains authoritative -for the external `*.ctrace-run.yml` format. +and unfinished work in the [TODO list](todo.md). The CMSIS-Toolbox [trace +specification](https://open-cmsis-pack.github.io/cmsis-toolbox/Experimental-Features/#trace) remains authoritative for +standardized `*.ctrace-run.yml` fields. The root `trace-format` field described below is a ctrace-private, +provisional extension, not a normative CMSIS-Toolbox field or a producer-emission requirement. Its standardization +and producer integration remain tracked as unfinished work. ## Boundaries @@ -14,35 +16,134 @@ for the external `*.ctrace-run.yml` format. structured decoder error from each data-path operation; falling back to only the last error or formatted log text would change recovery behavior. - YAML types remain inside the trace-run reader. The rest of ctrace consumes normalized configuration and metadata. -- The YAML reader validates fields consumed by ctrace; unrelated fields are outside its validation scope. Malformed - consumed fields remain errors. An ITM reference without `source` values is valid and contributes no source events. +- The YAML reader ignores unrelated fields. Structural and routing fields required to construct the normalized + configuration are validated while reading or normalizing; malformed consumed values remain errors. Optional null + scalars and null collection entries are read as absent wherever their schema permits it, while presence-only nodes + retain their defined flag semantics. Backend-dependent values such as `timestamps.clock` and DWT `address`, + `data-type`, and `size` retain parse failures for later validation. CTF preflight reports them only when the + corresponding route or DWT source is selected; absence remains valid where the field is optional. Defaults and + other operation-specific requirements are likewise evaluated after reading. Missing or null + `ctrace-setup.itm.enable` is absent; a malformed enable value bound to an active route is an Error. Conflicting valid + masks on one route produce one Warning and disable that route's optional received-on-disabled-channel check. An ITM + reference without `source` values is valid and contributes no source events. - DWT data metadata comes from reference-level `address`, `size`, and `data-type`. When reference `size` is absent, the referenced `ctrace-setup.data.size` supplies it. DWT instruction-control references may bind a processor stream but do not create decoded data-source routes. - Backend-specific requirements and failures remain independent; requesting CTF must not disable otherwise valid CSV output, or vice versa. +## Input format, framing, and discovery + +- Root-level `trace-format` accepts only `unformatted` or `formatted`. Missing or null selects `unformatted` without + an Error and remains an undeclared value for discovery compatibility. An explicit non-null declaration opts the + eligible SWO, TB, and named-TB candidates into the new selection rule. +- A legacy undeclared configuration activates only `.SWO.raw`; coexisting TB files retain their non-failing + excluded-input Warning. With an explicit format, exactly one existing `.SWO.raw`, `.TB.raw`, or + `.TB_.raw` must be selected. Zero or multiple candidates fail before decoder or output construction. + In either case, the selected input must be a regular, readable file and is opened during preflight, before decoder + or output construction. Event Recorder input remains diagnosed and excluded from the active candidate count. +- The standardized `trace-buffer` selection belongs to solution/build-run producer configuration, not to the + `*.ctrace-run.yml` file consumed by ctrace. Until the producer passes an unambiguous selected-file identity and its + effective format/framing, ctrace's explicit-format discovery rule remains a transitional input policy. +- Formatted input globally uses 16-byte memory-aligned CoreSight frames. Its length must be a multiple of 16, and it + contains neither FSYNC nor HSYNC framing. Ctrace does not parse or emit a `trace-framing` YAML field; supporting + another framing mode requires a public trace contract first. +- Format describes the effective bytes in the selected capture, not target capability. Ctrace does not infer it from + filenames, synchronization patterns, configured route count, or target setup. + +## Routing invariants + +- Generated `ctrace-refs.stream` values are the routing authority. Processor `itm` references using a `[pname/]itm` + path are the preferred route anchors. For compatibility with current pyTS output, only these reference-type and + `[pname/]feature` pairs may establish a route without that anchor, and only when the reference supplies `stream`: + `dwt` with a resolved `data#`, `timestamps`, or `synchronization`; `itm` with `timestamps`; `exception` with + `exceptions`; `event` or `pmu` with `events#`; and `pcsample` with `pcsampling`. The optional `pname/` prefix + is one path segment; nested feature paths are not fallbacks. `overflow`/`overflow` and `global_ts`/`timesync` may + describe an established route but cannot establish one. A copied or enriched `ctrace-setup.itm.atbid` is tolerated + but never creates, changes, or invalidates a route. +- Configured architectural Trace Bus IDs are restricted to `1` through `111` and bind one supported ITM protocol + route each. DWT and PMU data travel on that processor's ITM route rather than creating separate decoders. +- Unformatted input has one synthetic route with no architectural Trace Bus ID. OpenCSD channel `0`, public stream + selector `0`, and CTF stream-class ID `0` are compatibility representations of that route, not configured ATB ID 0. +- Formatted ID `0` is NULL/padding and creates no route, semantic event, CTF stream, or Trace Compass lane. A normal + observed ID without a configured ITM route is diagnosed once and skipped without guessing its protocol. +- Normalized route identity, processor binding, timestamp prescaler, source metadata, errors, synchronization, + overflow, and data-loss state remain route-local. ITM stimulus ports are restricted to `0` through `31`; port `0` + is decoded for stream integrity but excluded from payload output. + ## Decode invariants +- Both unformatted and formatted inputs use an OpenCSD `DecodeTree`: `SINGLE` for the synthetic route and + `FRAME_FORMATTED` with one decoder per configured ITM Trace Bus ID. There is no direct-ITM fallback path. +- Only one tree may be live and fed at a time. Its session owns the process-global OpenCSD logger lease, destroys the + tree before releasing callback state, and restores the previously installed logger on every exit path. - Raw trace bytes are passed to the decoder unchanged; ctrace never injects synthetic synchronization. Recovery resumes only at synchronization present in the input. - File-read chunks are not packet boundaries. Decoder state must survive arbitrary read boundaries. -- Incomplete input and unrecoverable decoder responses remain visible errors. Discontinuities flush or clear pending - DWT state and invalidate timestamp quality before decoding continues. -- Internally, unformatted input uses Trace Bus ID `0`; its CSV `stream` field is empty. Routed IDs are restricted to - `1` through `111`. -- ITM stimulus ports are restricted to `0` through `31`. Timestamp prescalers are stream-specific, default to `1`, - and accept only `1`, `4`, `16`, or `64`. +- Error callbacks collect complete stable batches; they do not reset, roll back, emit output, or throw through + OpenCSD. Classification and recovery happen only after the current synchronous data-path operation returns. +- A recoverable error assigned to a known ITM route discards only that route's failed transaction suffix and resets + only its packet-processor/full-decoder chain. The frame deformatter, current formatter ID, partially delivered + frame, and unaffected routes remain intact. A bounded root flush drains pending frame segments before new input. +- The DecodeTree-reported processed-byte count is the only formatted-input cursor. Bytes reported as consumed are + never re-fed. Only the affected route remains in data loss until a real hardware sync; unresolved loss is closed at + end of input. +- A channel-less or deformatter error, failed route reset, incomplete formatted framing/input, unrecoverable response, + exhausted wait, or repeated lack of progress is input-fatal and aborts every active output. An incomplete packet at + the end of an unformatted ITM stream retains the legacy recoverable behavior: it is published as a decoder issue and + does not by itself abort otherwise valid output. +- Discontinuities flush or clear pending route-local DWT state and invalidate timestamp quality before decoding + continues. Timestamp prescalers default to `1`, accept only `1`, `4`, `16`, or `64`, and are applied exactly once + after OpenCSD exposes raw ITM ticks. ## Observable behavior and output safety -- ITM port `0` is decoded for stream integrity but excluded from payload output. Decoder warnings and errors remain - observable regardless of payload filtering. +- CSV remains one combined file in semantic callback order. The unformatted route has an empty `stream` field; + formatted routes expose their architectural IDs. Type and stream filters affect output, not decoding or diagnostic + reporting. Ctrace deliberately names the seventh CSV column `address`; the currently published CMSIS-Toolbox trace + specification still says `offset`, and must be corrected to match this intended schema before the difference is + treated as standardized. +- Formatted CTF stream files are created lazily as `stream_` only for routes with selected semantic output. Every + emitted stream class references an explicit clock domain. When selected, the legacy unformatted path retains eager + `stream_0`, its UUID-optional `swo_clock` metadata form, and companion XML compatibility. +- Generalized CTF metadata records a bound processor name in the corresponding stream-scoped environment entry. + Its event context preserves the CMSIS-profile `uint8_t cmsis_trace_bus_id` field and adds the ctrace-private + `ctrace_route` enum used by generated Trace Compass XML. The enum label is the processor name when bound and the + decimal CTF stream-class ID otherwise. Generalized XML prefixes every state path with that label and then the + architectural `cmsis_trace_bus_id`; it exposes a separate graphical provider per emitted route and topic, named + with the resolved processor label when available but never with its numeric ID. The exact legacy CTF event context + remains unchanged. +- XML declares only graphical outputs: DWT values and addresses as XY series; trace-origin exceptions, DWT matches, + DWT/PMU overflow events, and processor sleep state as time graphs. Each block is emitted only if the completed stream + contains matching trace data; synthetic exception bootstrap records alone do not enable an exception block, and + `Processor State` specifically requires a sleep indication. ITM payloads, ordinary sampled + PCs, and trace-status records stay available through the CTF event table. Trace Compass XML has no data-driven + table-view type, so ctrace does not model these point records as artificial timelines. +- `timestamps.clock` has no ctrace fallback. For every route selected for CTF, missing, null, invalid, zero, or + conflicting frequency is accepted for validation-only and CSV operation but prevents CTF generation with an Error. + A filter selecting no configured route requires no clock because it can emit no CTF stream. With `--all`, valid CSV + still completes while the invocation returns non-zero. +- Different processor bindings are independent CTF clock domains even when their frequencies match. A multi-clock + CTF bundle remains valid, but ctrace emits one Warning, removes any stale companion XML, and creates no new Trace + Compass XML because the supported reader cannot establish a correct combined order. Cross-domain time correlation + is not inferred. +- Decoder warnings and errors remain observable regardless of payload filtering. A recoverable protocol error may be + published with route-bound error/data-loss events even though its Error diagnostic makes the invocation fail. - Structured diagnostic impact determines command failure; formatted stderr text does not. - CTF timestamps never regress, and a global timestamp does not by itself establish local timestamp quality. - Validation-only mode creates no output. Unsupported trace channels are diagnosed and skipped. - Cleanup of incomplete output artifacts is attempted after failure, and cleanup failures are reported. Incompatible - target types and overlapping CTF/XML paths are rejected before replacement. + existing output filesystem types and overlapping CTF/XML paths are rejected before replacement. + +## Build and CI constraints + +- Keep the ctrace workflow's `push.paths` and `pull_request.paths` filters identical. They are limited to the ctrace + workflow and matrix, the root and ctrace CMake configuration, and ctrace source and tests. Changes made only to + `.gitmodules` or paths below `external/` must not trigger this workflow. +- Changes to the input, routing, or output-profile contracts require the supported-platform CI, portable unit and + integration suite, native-Linux Babeltrace consumer gate, source-line coverage gate, and branch-report review + described in the [build and CI architecture](architecture.md#build-and-ci-structure). XML-shape changes additionally + follow the golden-update and external-acceptance requirements in the [CTF profile](ctf-format.md#maintaining-the-profile). Changes to these contracts require corresponding unit or integration coverage. Update the architecture document only when the implementation structure or data flow changes. diff --git a/tools/ctrace/docs/ctf-format.md b/tools/ctrace/docs/ctf-format.md index ed0e25c84..9f1f95f01 100644 --- a/tools/ctrace/docs/ctf-format.md +++ b/tools/ctrace/docs/ctf-format.md @@ -23,10 +23,13 @@ cmsis_ctf_profile_version = 1 ## Files and common structure -The CTF bundle contains a `metadata` file and a binary `stream_0` file. The generated -`..traceanalysis.xml` file is stored next to the bundle and defines the Trace Compass views. +Every CTF bundle contains a `metadata` file and zero or more binary stream files. When selected, unformatted +single-source input preserves the established layout: stream class `0` is written eagerly to `stream_0` and references +`swo_clock`. Formatted input uses the generalized layout: each emitted Trace Bus route has its own stream class, +binary `stream_` file, and explicit clock-domain reference. Formatted streams without emitted records are omitted +from the final bundle and metadata; a stream filter that selects no route therefore produces a metadata-only bundle. -CTF stream ID `0` is the output container for supported SWO events. Every event has this common prefix: +Every event has this common header and public context: ```text uint32_t id @@ -34,16 +37,43 @@ uint64_t timestamp uint8_t cmsis_trace_bus_id ``` -`timestamp` is a cycle count in the configured `swo_clock` domain. CTF output requires a non-zero -`timestamps.clock`. Timestamps in the shared binary stream never decrease; when events from several Trace Bus IDs -are multiplexed, `ctrace` clamps a regressing value to the last emitted timestamp. +Generalized streams add one private display context after `cmsis_trace_bus_id`: + +```text +cmsis_stream__route_t ctrace_route +``` + +`timestamp` is a cycle count in the clock domain referenced by its stream class. CTF preflight requires a non-zero +`timestamps.clock` for every configured route selected by the stream filter, before ctrace knows which routes will +emit records. A stream filter that selects no configured route requires no clock. Timestamps never decrease within +one binary stream; independent routes are not clamped against each other. Generalized clock domains have distinct +UUIDs even when their configured frequencies are equal, because equal frequency alone does not establish +synchronization. `cmsis_trace_bus_id` identifies the CoreSight Trace Bus route. Value `0` denotes unformatted single-source input; -formatted IDs use values `1` through `111`. It is routing context, not a CPU identity. +formatted IDs use values `1` through `111`. It is routing context, not a CPU identity. `ctrace_route` is a private +enum carrying the resolved processor name for display, or the numeric stream-class ID as an internal fallback. The +optional processor name is also stored in the generalized environment as +`cmsis_stream__processor_name`. + +The packet context has this exact field order: -The packet context records packet size, content size, first and last timestamps, a sequence number, and -`events_discarded`. Trace loss is represented by `TRACE_STATUS` events rather than the CTF -`events_discarded` counter, which is currently zero. +```text +uint32_t packet_size +uint32_t content_size +uint64_t timestamp_begin +uint64_t timestamp_end +uint32_t events_discarded +uint32_t packet_seq_num +``` + +The timestamp fields use the stream class's clock mapping. Trace loss is represented by `TRACE_STATUS` events rather +than the CTF `events_discarded` counter, which is currently zero. + +The optional `..traceanalysis.xml` companion is stored next to the bundle. It is generated +only when the completed metadata retains at least one stream and all retained streams reference one clock domain, +because the supported Trace Compass reader cannot safely combine independent clocks. This limitation affects only +the generated visualization; a metadata-only or multi-clock CTF bundle remains valid. ## Event catalogue @@ -98,6 +128,17 @@ The variant tag immediately precedes the selected value. ITM input uses the unsi from the resolved `data-type` and `size` metadata; supported sizes are 1, 2, and 4 bytes, and `float` requires 4 bytes. +Optional DWT PC and data-address fragments use a separate width tag and variant: + +| Tag | Variant | Meaning | +| ---: | --- | --- | +| 0 | `none` | No fragment; the variant value is ignored. | +| 1 | `u8` | Unsigned 8-bit raw fragment. | +| 2 | `u16` | Unsigned 16-bit raw fragment. | +| 4 | `u32` | Unsigned 32-bit raw fragment. | + +The exact payload width is preserved; ctrace does not widen every address fragment to 16 or 32 bits. + ## Instrumentation events ### ITM (event ID 0) @@ -113,7 +154,8 @@ The channel enumeration covers ITM stimulus ports 1 through 31. Labels from `ctr names `ITM1` through `ITM31`; duplicate labels receive a unique fallback. Port 0 is decoded for stream integrity but is intentionally excluded from CTF payload output. -Trace Compass exposes the values by ITM channel in an event table and a time graph. +Trace Compass exposes ITM as point events through its standard CTF event table. The generated XML does not invent a +time graph for these values. ## DWT data-trace events @@ -123,39 +165,32 @@ Trace Compass exposes the values by ITM channel in an event table and a time gra cmsis_dwt_comparator_t cmsis_dwt_comparator cmsis_dwt_access_t cmsis_dwt_access value tag and selected value variant -uint8_t cmsis_has_pc -uint32_t cmsis_pc[cmsis_has_pc] -uint8_t cmsis_has_address_lo16 -uint16_t cmsis_address_lo16[cmsis_has_address_lo16] +PC width tag and selected cmsis_dwt_pc variant +address width tag and selected cmsis_dwt_address variant uint8_t cmsis_sample_flags uint32_t cmsis_overflow_count ``` -`cmsis_dwt_access` is `read` (`0`) or `write` (`1`). The zero-or-one-element arrays make the associated PC and low -address bits optional. DWT comparator labels and data types are resolved from `ctrace-run.yml`; fallback labels are -`DWT0` through `DWT3`. +`cmsis_dwt_access` is `read` (`0`) or `write` (`1`). DWT comparator labels and data types are resolved from +`ctrace-run.yml`; fallback labels are `DWT0` through `DWT3`. The configured value size controls the CTF scalar type. If it differs from the SWO payload size, `ctrace` emits a -warning for that route. Trace Compass provides a value table and an XY view per comparator. +warning for that route. Trace Compass provides the standard event table and, when such data was emitted, a generated +XY view with one series per comparator. ### DWT_ADDR (event ID 2) ```text cmsis_dwt_comparator_t cmsis_dwt_comparator -uint8_t cmsis_has_pc -uint8_t cmsis_has_address_lo16 -uint32_t cmsis_pc -uint16_t cmsis_address_lo16 +PC width tag and selected cmsis_dwt_pc variant +address width tag and selected cmsis_dwt_address variant uint8_t cmsis_sample_flags uint32_t cmsis_overflow_count ``` -The fixed fields contain either a PC, the lower 16 address bits, or both. The corresponding `cmsis_has_*` fields -determine which values are valid; absent values are written as zero. Trace Compass exposes the address value per DWT -comparator. - -This describes the current profile. Any change that preserves 1-, 2-, or 4-byte address fragments instead of the -fixed lower-16-bit representation must update this section and review the profile version in the same change. +The independent tags state whether a PC fragment, data-address fragment, or both are present and preserve each raw +fragment's exact width. Trace Compass provides the standard event table and, when a data-address fragment was +emitted, a generated address XY view with one series per comparator. ### DWT_MATCH (event ID 9) @@ -166,8 +201,8 @@ uint32_t cmsis_overflow_count ``` The event indicates that an Armv8-M DWT comparator matched without supplying a PC, address, or value. It is a point -event without architectural duration. The generated Trace Compass time graph shows a `Something happened` pulse on -the comparator lane for one microsecond; this artificial width is visualization only. +event without architectural duration. When such data was emitted, the generated Trace Compass time graph shows a +`Something happened` pulse on the comparator lane for one microsecond; this artificial width is visualization only. ## Trace-integrity events @@ -189,8 +224,8 @@ The status reason is: | 4 | `data_loss` | An input interval could not be decoded reliably | An overflow or data-loss boundary closes active exception and sleep visualization state. The unknown interval is -therefore visible as a gap instead of being attributed to the previously active state. Trace Compass provides a lane -for each status reason. +therefore visible as a gap instead of being attributed to the previously active state. Status records remain point +events in the standard CTF event table; the generated XML does not create a status timeline. ## Execution-state events @@ -210,9 +245,10 @@ architectural label, while `cmsis_exception_number_value` preserves the numeric (`synthetic`, `1`). Synthetic records close the previously active context and establish mutually exclusive timeline lanes; they do not claim that another exception packet existed in the raw trace. -The Trace Compass time graph orders Thread Mode first, Exception Return second, and the observed exceptions below -them. Overflow and data loss close the active context and leave a gap until a later trace transition establishes the -state again. +If at least one decoded exception transition was emitted, the generated Trace Compass time graph orders Thread Mode +first, Exception Return second, and the observed exceptions below them. Synthetic bootstrap records alone do not +create the view. Overflow and data loss close the active context and leave a gap until a later trace transition +establishes the state again. ### PC_SAMPLE (event ID 6) @@ -227,8 +263,9 @@ State `0` denotes processor sleep and leaves the PC array empty. State `1` denot 32-bit PC. The state therefore doubles as the zero-or-one array length. PC samples are point observations and are available in the CTF event table. Trace Compass does not invent execution -duration between sampled PCs. A sleep indication opens the `Sleep` interval; the next PC sample, overflow, or data -loss closes it. +duration between sampled PCs. If a sleep indication was emitted, the generated `Processor State` view opens a +`Sleep` interval; the next PC sample, overflow, or data loss closes it. Ordinary PC samples alone do not create this +view. ## Time-correlation events @@ -240,7 +277,8 @@ uint8_t cmsis_clock_change ``` The payload preserves the decoded Global Timestamp value and its clock-change indication. The CTF event header still -contains the current local `swo_clock` timestamp. Emitting this event does not by itself synchronize independent trace +contains the current local timestamp in the stream's referenced clock domain (`swo_clock` for the legacy layout or +`cmsis_clock_` for a generalized stream). Emitting this event does not by itself synchronize independent trace clock domains or make an unreliable local timestamp reliable. ## Profiling events @@ -275,36 +313,71 @@ uint32_t cmsis_overflow_count The PMU overflow mask is likewise expanded into one CTF record per set bit. Bits 0 through 7 are provisionally named `Event0` through `Event7`, because the raw packet does not identify the programmable event assigned to the counter. -DWT and PMU counter records are point events without architectural duration. Their Trace Compass timelines use -named lanes and artificial one-microsecond pulses. Overlapping pulses are stacked so that a later scheduled close -does not hide another occurrence. +DWT and PMU counter records are point events without architectural duration. If such records were emitted, their +Trace Compass timelines use named lanes and artificial one-microsecond pulses. Overlapping pulses are stacked so +that a later scheduled close does not hide another occurrence. ## Generated Trace Compass analysis -The generated XML creates tables for ITM, DWT values and addresses, matches, profiling events, exceptions, trace -status, and PC samples. It creates XY views for DWT values and addresses and time graphs for ITM, DWT matches, -exceptions, trace status, PC-sample sleep intervals, and DWT/PMU profiling events. +Trace Compass provides its standard CTF event table for every encoded event; ctrace does not duplicate these point +events as generated XML tables. The companion XML contains only graphical views backed by data actually emitted: + +- XY views for DWT values and data-address fragments. +- Time graphs for DWT matches, DWT and PMU counter pulses, decoded exception activity, and processor sleep. + +ITM values, trace-status records, Global Timestamps, and ordinary PC samples stay in the standard event table because +they do not establish a duration. Each generalized route receives a separate graphical view. Its visible suffix is +the resolved processor name, if available; numeric Trace Bus IDs are omitted from visible labels but remain in the +public CTF event context and internally in provider IDs and state queries. Routes and topics without corresponding +emitted data do not add graphical views. + +The production output planner conservatively assigns each formatted route a distinct clock domain and UUID, even +when configured frequencies match. After lazy stream projection, ctrace writes XML only if the completed bundle +retains at least one stream and exactly one referenced domain; with current planning, this normally means one retained +formatted route. Multiple retained domains deliberately omit the XML and produce one warning rather than presenting +unrelated cycle domains as a shared timeline. The underlying CTF model and XML writer retain support for explicitly +described shared domains once the input contract can establish one. The state-provider version is a deterministic hash of the generated XML contents. A semantic XML change therefore changes the version automatically and prevents a Trace Compass server from reusing stale analysis state. ## Current profile boundaries -- Processor identity is not encoded. Trace Bus IDs distinguish routes, but complete multi-CPU representation, - separate trace clock domains, and cross-stream synchronization remain open. -- Formatted Trace Buffer input, including ETB/ETF capture, is not decoded. +- Formatted, memory-aligned CoreSight frames carrying ITM/DWT are decoded and represented as separate route streams. +- The optional resolved processor name is encoded as stream-scoped environment metadata and private display context; + `cmsis_trace_bus_id` remains the stable public routing field. +- Separate trace clock domains are represented, but Global Timestamp packets do not yet establish cross-stream + synchronization and the generated Trace Compass XML therefore requires one shared domain. - ETM and MTB instruction trace have no CTF event definitions yet. +- Event Recorder input has no CTF event definitions yet. +- Formatted input with FSYNC/HSYNC transport framing is not decoded; the current input contract requires complete, + memory-aligned 16-byte formatter frames. - ITM paging above stimulus port 31 is not supported. - PMU counter assignments are not resolved to configured architectural event names. -- CTF describes one fixed `swo_clock` frequency and does not reconstruct stopped or changing trace clocks. +- CTF records configured fixed clock frequencies but does not reconstruct stopped or changing trace clocks. + +The profile remains version `1`. Existing unformatted single-source input keeps its stream-0 byte layout and event +schema unchanged; the generalized layout is used only for the newly supported formatted multi-route input. It adds +per-stream metadata and the private `ctrace_route` context while retaining `cmsis_trace_bus_id` and all public event +IDs, fields, enum values, and interpretations. A future incompatible change to those public contracts requires a +version review. ## Maintaining the profile -The implementation sources are [`CtfSchema.h`](../src/output/ctf/CtfSchema.h), +The primary implementation sources are [`OutputRequirements.cpp`](../src/output/OutputRequirements.cpp), +[`CtfSchema.h`](../src/output/ctf/CtfSchema.h), +[`CtfMetadataModel.cpp`](../src/output/ctf/CtfMetadataModel.cpp), [`CtfMetadataWriter.cpp`](../src/output/ctf/CtfMetadataWriter.cpp), -[`CtfEncoder.cpp`](../src/output/ctf/CtfEncoder.cpp), and +[`CtfEncoder.cpp`](../src/output/ctf/CtfEncoder.cpp), +[`CtfStreamWriter.cpp`](../src/output/ctf/CtfStreamWriter.cpp), +[`CtfBundleOutput.cpp`](../src/output/ctf/CtfBundleOutput.cpp), and [`TraceCompassXmlWriter.cpp`](../src/output/ctf/TraceCompassXmlWriter.cpp). +The data-driven XML shape is an approved compatibility refinement of the former eager legacy XML: only graphical +topics observed in completed output create views. The checked-in legacy XML remains the current golden. Any further +XML-shape change must update focused XML tests, review and update that golden plus its fixture-manifest hash, and +re-run the external Trace Compass acceptance. + Changes to an event ID, name, type, field, enum value, or interpretation must update this document and the relevant metadata, encoder, Trace Compass, and CTF decoding tests together. Compatibility-impacting changes must also review `cmsis_ctf_profile_version`. diff --git a/tools/ctrace/docs/todo.md b/tools/ctrace/docs/todo.md index 0e14bd217..475f24d76 100644 --- a/tools/ctrace/docs/todo.md +++ b/tools/ctrace/docs/todo.md @@ -1,33 +1,37 @@ # 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. +- [ ] Clarify the compound DWT/PMU payload-validation expressions in `DwtPacketDecoder` with explicit grouping. ## 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 time correlation -- [ ] Propagate processor identity into decoded events and outputs. -- [ ] Support separate trace clock domains and cross-stream synchronization in CTF. +- [ ] Resolve the cross-tool raw-input contract tracked by + [vscode-cmsis-debugger #1150](https://github.com/Open-CMSIS-Pack/vscode-cmsis-debugger/issues/1150) and + [devtools #2573](https://github.com/Open-CMSIS-Pack/devtools/issues/2573): standardize selected raw-input identity + plus effective byte format/framing in the CMSIS-Toolbox trace specification, then migrate the provisional global + `trace-format` reader contract and producer output deliberately. +- [ ] Correct the CMSIS-Toolbox CSV schema from the obsolete seventh-column name `offset` to the implemented + `address` name. +- [ ] Support FSYNC and FSYNC+HSYNC formatted input after a public framing field is specified. +- [ ] 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/CMakeLists.txt b/tools/ctrace/src/CMakeLists.txt index 2c51aa3af..a34113c4d 100644 --- a/tools/ctrace/src/CMakeLists.txt +++ b/tools/ctrace/src/CMakeLists.txt @@ -3,7 +3,9 @@ # SPDX-License-Identifier: Apache-2.0 set(CTRACE_MODEL_HEADER_FILES + model/CoreSightFormatter.h model/TraceEvent.h + model/TraceRoute.h model/TraceSelection.h model/TraceStreamId.h ) @@ -28,10 +30,12 @@ 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 decode/OpenCsdTraceElement.h + decode/OpenCsdTreeSession.h decode/SaturatingArithmetic.h ) set(CTRACE_OUTPUT_HEADER_FILES @@ -44,9 +48,12 @@ set(CTRACE_OUTPUT_HEADER_FILES output/ctf/CtfBundleOutput.h output/ctf/CtfEncoder.h output/ctf/CtfExceptionLaneTracker.h + output/ctf/CtfGraphicalTopic.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 @@ -127,9 +134,11 @@ 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 + decode/OpenCsdTreeSession.cpp ${CTRACE_DECODE_HEADER_FILES} ) add_library(ctrace::decode ALIAS ctrace-decode) @@ -149,8 +158,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/control/DecodeConsumers.cpp b/tools/ctrace/src/control/DecodeConsumers.cpp index 7fce924a3..0b1cd72f2 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 @@ -33,11 +34,9 @@ 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) { @@ -59,24 +58,23 @@ void DecodeConsumers::reportItmConfigurationMismatch(const TraceEvent& event) return; } - auto enableMask = m_itmEnableMask; - const auto streamMask = m_itmEnableMasksByTraceBusId.find(event.traceBusId); - if (streamMask != m_itmEnableMasksByTraceBusId.end()) { - enableMask = streamMask->second; - } - if (!enableMask.has_value() || ((*enableMask & (1U << software->channel)) != 0U) || - !m_reportedDisabledItmChannels.emplace(event.traceBusId, software->channel).second) { + const auto streamMask = m_itmEnableMasksByRoute.find(event.route.id); + if (streamMask == m_itmEnableMasksByRoute.end() || + ((streamMask->second & (1U << software->channel)) != 0U) || + !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(streamMask->second)); 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..3f5c3dc95 100644 --- a/tools/ctrace/src/control/DecodeConsumers.h +++ b/tools/ctrace/src/control/DecodeConsumers.h @@ -13,11 +13,11 @@ #include "TraceIssueReporter.h" #include "TraceOutput.h" #include "TraceOutputLifecycle.h" +#include "TraceRoute.h" #include #include #include -#include #include #include #include @@ -27,8 +27,7 @@ class DecodeConsumers final : public TraceEventSink { public: /** @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; @@ -46,9 +45,8 @@ class DecodeConsumers final : public TraceEventSink { void reportItmConfigurationMismatch(const TraceEvent& event); 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 e897eda37..4f839d3b1 100644 --- a/tools/ctrace/src/control/FileDecodeJob.cpp +++ b/tools/ctrace/src/control/FileDecodeJob.cpp @@ -25,10 +25,10 @@ #include #include #include -#include #include #include #include +#include #include #include #include @@ -44,21 +44,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 +64,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; }; @@ -100,14 +94,44 @@ 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 Converts normalized trace-run routes into semantic decoder routes. */ +static std::vector decodeRoutes(const CtraceRunMeta& ctraceRunMeta) +{ + 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. */ +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 Counts source metadata directly from the canonical route catalogue. */ +static std::size_t sourceCount(const CtraceRunMeta& ctraceRunMeta) { - auto fallback = ctraceRunMeta.timestampPrescaler(); - if (!fallback.has_value() && !ctraceRunMeta.hasDistinctProcessorPrescalers()) { - fallback = TraceRunSchema::kDefaultTimestampPrescaler; + std::size_t result = 0U; + for (const auto& route : ctraceRunMeta.routes()) { + result += route.sources.size(); } - return {fallback, ctraceRunMeta.timestampPrescalersByTraceBusId()}; + return result; } /** @brief Converts command-line output selection into an output request. */ @@ -139,77 +163,108 @@ static std::vector> createConfiguredOutputs(const T return outputs; } -FileDecodeJob::FileDecodeJob(CliOptions options, std::filesystem::path rawInputPath, DiagnosticSink& diagnostics, - CtraceRunMeta ctraceRunMeta) +/** @brief Reports the normalized trace-run model selected for one decode job. */ +static void reportTraceRunMeta(const CtraceRunMeta& meta, DiagnosticSink& diagnostics) +{ + diagnostics.report({ + DiagnosticSink::Severity::Info, + "applied ctrace-run meta", + { + {"path", meta.configPath()}, + {"routes", std::to_string(meta.routes().size())}, + {"sources", std::to_string(sourceCount(meta))}, + }, + }); +} + +/** @brief Reports the timestamp prescaler applied to every normalized route. */ +static void reportTimestampPrescalers(const CtraceRunMeta& meta, DiagnosticSink& diagnostics) +{ + for (const auto& route : meta.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); + } + diagnostics.report({DiagnosticSink::Severity::Info, "using timestamp prescaler", std::move(context)}); + } +} + +/** @brief Creates the production or injected OpenCSD decode pipeline. */ +static std::unique_ptr +createDecodePipeline(const std::vector& routes, OpenCsdItmInputMode inputMode, + DecodeConsumers& consumers, const OpenCsdItmSessionFactory& sessionFactory, + DiagnosticSink& diagnostics) +{ + if (sessionFactory) { + return std::make_unique(routes, inputMode, consumers, sessionFactory); + } + return std::make_unique( + routes, inputMode, consumers, [&diagnostics](std::uint8_t traceBusId, std::uint64_t sourceOffset) { + diagnostics.report({ + DiagnosticSink::Severity::Warning, + "skipping unsupported formatted CoreSight trace source", + { + {"stream", std::to_string(traceBusId)}, + {"rawOffset", std::to_string(sourceOffset)}, + }, + }); + }); +} + +/** @brief Streams one preflighted raw input through its configured decode pipeline. */ +static DecodeResult decodeRawInput(const std::filesystem::path& path, std::istream& stream, DecodePipeline& pipeline) +{ + RawFileReader input(path, stream); + while (true) { + const auto read = input.read(); + if (read.eof) { + break; + } + pipeline.push(read.bytes); + } + return pipeline.finish(); +} + +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); + const auto& ctraceRunMeta = m_input.metadata(); + 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; } - m_diagnostics.report({ - 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())}, - }, - }); + reportTraceRunMeta(ctraceRunMeta, m_diagnostics); 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, itmEnableMasks(ctraceRunMeta)); + reportTimestampPrescalers(ctraceRunMeta, m_diagnostics); - 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())}}, - }); - } const auto decodeStart = std::chrono::steady_clock::now(); DecodeResult decode; bool decoderFatal = false; try { - RawFileReader input(m_rawInputPath); - std::unique_ptr pipeline; - if (m_sessionFactory) { - pipeline = std::make_unique(prescalers, consumers, m_sessionFactory); - } else { - pipeline = std::make_unique(prescalers, consumers); - } - while (true) { - const auto read = input.read(); - if (read.eof) { - break; - } - pipeline->push(read.bytes); - } - decode = pipeline->finish(); + auto pipeline = createDecodePipeline(routes, inputMode, consumers, m_sessionFactory, m_diagnostics); + decode = decodeRawInput(m_input.path(), m_input.stream(), *pipeline); } catch (const OpenCsdFatalError& error) { decoderFatal = true; decode.bytesIn = error.bytesProcessed(); 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 de7c9cf8a..6b924ad48 100644 --- a/tools/ctrace/src/control/TraceDirectoryJob.cpp +++ b/tools/ctrace/src/control/TraceDirectoryJob.cpp @@ -102,61 +102,49 @@ void TraceDirectoryJob::run() } for (const auto& configFile : configFiles) { - const auto solutionSet = TraceRunDiscovery::solutionSetName(configFile); - try { - const auto config = m_configReader.read(configFile.string()); - m_diagnostics.report({ - DiagnosticSink::Severity::Info, - "selected trace-run configuration", - { - {"solutionSet", solutionSet}, - {"path", config.path}, - {"references", std::to_string(config.references.size())}, - {"setups", std::to_string(config.setups.size())}, - }, - }); - reportConsumedReferenceDiagnostics(config, m_diagnostics); - const auto ctraceRunMeta = CtraceRunMeta::fromConfig(config); - reportTraceRunWarnings(ctraceRunMeta, m_diagnostics); - 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; - } + processConfigFile(configFile); + } +} - FileDecodeJob fileJob(m_options, rawInput.path, m_diagnostics, ctraceRunMeta); - fileJob.run(); - processedSolutionSet = true; - } - if (!processedSolutionSet) { - m_diagnostics.report({ - DiagnosticSink::Severity::Error, - "no supported .SWO.raw input found", - { - {"solutionSet", solutionSet}, - {"traceDir", configFile.parent_path().string()}, - }, - }); - } - } catch (const std::exception& error) { +void TraceDirectoryJob::processConfigFile(const std::filesystem::path& configFile) +{ + const auto solutionSet = TraceRunDiscovery::solutionSetName(configFile); + try { + const auto config = m_configReader.read(configFile.string()); + m_diagnostics.report({ + DiagnosticSink::Severity::Info, + "selected trace-run configuration", + { + {"solutionSet", solutionSet}, + {"path", config.path}, + {"references", std::to_string(config.references.size())}, + {"setups", std::to_string(config.setups.size())}, + }, + }); + reportConsumedReferenceDiagnostics(config, m_diagnostics); + auto ctraceRunMeta = CtraceRunMeta::fromConfig(config); + reportTraceRunWarnings(ctraceRunMeta, m_diagnostics); + auto input = TraceRunDiscovery::resolveInput(std::move(ctraceRunMeta), [&](const auto& rawInput) { m_diagnostics.report({ - DiagnosticSink::Severity::Error, - error.what(), + DiagnosticSink::Severity::Warning, + "skipping raw trace channel excluded from active input selection", { {"solutionSet", solutionSet}, - {"config", configFile.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, + error.what(), + { + {"solutionSet", solutionSet}, + {"config", configFile.string()}, + }, + }); } } diff --git a/tools/ctrace/src/control/TraceDirectoryJob.h b/tools/ctrace/src/control/TraceDirectoryJob.h index 1bec158ad..90f52bf70 100644 --- a/tools/ctrace/src/control/TraceDirectoryJob.h +++ b/tools/ctrace/src/control/TraceDirectoryJob.h @@ -12,6 +12,8 @@ #include "DiagnosticSink.h" #include "TraceRunConfigReader.h" +#include + /** @brief Discovers and decodes the selected trace-run configurations in a directory. */ class TraceDirectoryJob { public: @@ -32,6 +34,9 @@ class TraceDirectoryJob { void run(); private: + /** @brief Reads, normalizes, and decodes one selected trace-run configuration. */ + void processConfigFile(const std::filesystem::path& configFile); + CliOptions m_options; DiagnosticSink& m_diagnostics; const TraceRunConfigReader& m_configReader; diff --git a/tools/ctrace/src/decode/CortexMPostDecoder.cpp b/tools/ctrace/src/decode/CortexMPostDecoder.cpp index 5902fa9d2..5b931eec8 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) { } @@ -106,40 +107,29 @@ std::uint64_t CortexMPostDecoder::eventCount() const void CortexMPostDecoder::appendSync(const OpenCsdTraceElement& element) { - TraceEvent event{SyncTraceEvent{}}; - event.index = element.sourceIndex; - event.traceBusId = element.traceBusId; + auto event = makeEvent(element.sourceIndex, SyncTraceEvent{}); queueOrEmitWhileAwaitingTimestamp(std::move(event)); } void CortexMPostDecoder::appendOverflow(const OpenCsdTraceElement& element) { - noteOverflow(); - const auto status = currentTraceStatus(); - finalizePendingDiscontinuityIssues(std::nullopt); - flushPendingDataTrace(status); - flushPendingEvents(std::nullopt, status); - m_dwtDecoder.reset(); + const auto status = markDiscontinuity(); - TraceEvent event{OverflowTraceEvent{ + auto event = makeEvent(element.sourceIndex, OverflowTraceEvent{ "overflow: new timestamp segment; time across boundary may be unreliable", - }}; - event.index = element.sourceIndex; - event.traceBusId = element.traceBusId; + }); event.tcyc = m_timelineKnown ? std::optional(m_currentTcyc) : std::nullopt; - event.quality = TraceQuality{true, false, m_overflowCount}; + event.quality = status; emitEvent(event); } void CortexMPostDecoder::appendGlobalTimestamp(const OpenCsdTraceElement& element) { flushPendingDataTrace(currentTraceStatus()); - TraceEvent event{GlobalTimestampTraceEvent{ + auto event = makeEvent(element.sourceIndex, GlobalTimestampTraceEvent{ element.timestampValue, element.clockChange, - }}; - event.index = element.sourceIndex; - event.traceBusId = element.traceBusId; + }); m_pendingEvents.push_back(std::move(event)); } @@ -147,26 +137,24 @@ 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) { const auto status = element.discontinuity ? markDiscontinuity() : currentTraceStatus(); - TraceEvent event{TraceIssueEvent{ + auto event = makeEvent(element.sourceIndex, TraceIssueEvent{ element.issueCode.value_or(TraceIssueCode::OpenCsdDecodeError), element.issueSeverity, element.errorMessage, element.rawBytesConsumed, std::nullopt, - }}; - event.index = element.sourceIndex; - event.traceBusId = element.traceBusId; + }); event.tcyc = m_currentTcyc; event.quality = status; if (element.awaitingResumeTimestamp) { @@ -180,13 +168,11 @@ void CortexMPostDecoder::appendError(const OpenCsdTraceElement& element) void CortexMPostDecoder::appendSoftware(const OpenCsdTraceElement& element) { flushPendingDataTrace(currentTraceStatus()); - TraceEvent event{SoftwareTraceEvent{ + auto event = makeEvent(element.sourceIndex, SoftwareTraceEvent{ element.channel, element.size, element.value, - }}; - event.index = element.sourceIndex; - event.traceBusId = element.traceBusId; + }); event.tcyc = m_currentTcyc; event.quality = currentTraceStatus(element.overflow); m_pendingEvents.push_back(std::move(event)); @@ -196,7 +182,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, @@ -216,9 +202,7 @@ void CortexMPostDecoder::appendTimestamp(const OpenCsdTraceElement& element) flushPendingDataTrace(status); flushPendingEvents(m_currentTcyc, status); - TraceEvent event{LocalTimestampTraceEvent{}}; - event.index = element.sourceIndex; - event.traceBusId = element.traceBusId; + auto event = makeEvent(element.sourceIndex, LocalTimestampTraceEvent{}); event.tcyc = m_currentTcyc; emitEvent(event); @@ -254,20 +238,17 @@ 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{ + auto event = makeEvent(sourceIndex, TraceIssueEvent{ issueCode, TraceIssueSeverity::Error, message, rawBytesConsumed, m_currentTcyc, - }}; - event.index = sourceIndex; - event.traceBusId = traceBusId; + }); 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..0edb0bc73 100644 --- a/tools/ctrace/src/decode/CortexMPostDecoder.h +++ b/tools/ctrace/src/decode/CortexMPostDecoder.h @@ -11,17 +11,19 @@ #include "OpenCsdTraceElement.h" #include "DwtPacketDecoder.h" #include "TraceEvent.h" +#include "TraceRoute.h" #include #include #include +#include #include /** @brief Converts OpenCSD elements from one Cortex-M stream into semantic events. */ 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 +52,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); @@ -65,6 +67,14 @@ class CortexMPostDecoder final : public OpenCsdTraceElementSink { void flushPendingEvents(std::optional tcyc, const TraceQuality& quality); /** @brief Appends reconstructed DWT events to the pending sequence. */ void appendPendingEvents(std::vector events); + /** @brief Creates an event with decoder-local source and route identity. */ + template TraceEvent makeEvent(std::uint64_t sourceIndex, Payload payload) const + { + TraceEvent event{std::move(payload)}; + event.index = sourceIndex; + event.route = m_route; + return event; + } /** @brief Sends one finalized event to the downstream sink. */ void emitEvent(const TraceEvent& event); /** @brief Maps a decoder-local timestamp onto the monotonic output timeline. */ @@ -79,6 +89,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..f4dbbc755 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(const 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..f243ad0fb 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(const 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..d91eafd80 100644 --- a/tools/ctrace/src/decode/DecodePipeline.cpp +++ b/tools/ctrace/src/decode/DecodePipeline.cpp @@ -15,17 +15,30 @@ #include #include #include +#include -DecodePipeline::DecodePipeline(ItmTimestampPrescalers timestampPrescalers, TraceEventSink& eventSink) - : m_streamDecoder(std::move(timestampPrescalers), eventSink), - m_decoder(m_streamDecoder) +/** @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(ItmTimestampPrescalers timestampPrescalers, TraceEventSink& eventSink, - const OpenCsdItmSessionFactory& sessionFactory) - : m_streamDecoder(std::move(timestampPrescalers), eventSink), - m_decoder(m_streamDecoder, sessionFactory) +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 d0ee3680d..b8c53eb6c 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 { @@ -37,18 +38,22 @@ 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 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(ItmTimestampPrescalers timestampPrescalers, TraceEventSink& eventSink); + DecodePipeline(std::vector routes, OpenCsdItmInputMode inputMode, TraceEventSink& eventSink, + OpenCsdUnsupportedTraceIdObserver unsupportedTraceIdSink = {}); /** - * @brief Creates a pipeline with an injected OpenCSD session factory. - * @param timestampPrescalers Default and per-stream timestamp prescalers. + * @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(ItmTimestampPrescalers timestampPrescalers, TraceEventSink& eventSink, + DecodePipeline(std::vector routes, OpenCsdItmInputMode inputMode, TraceEventSink& eventSink, const OpenCsdItmSessionFactory& sessionFactory); /** @@ -69,4 +74,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/DwtPacketDecoder.cpp b/tools/ctrace/src/decode/DwtPacketDecoder.cpp index 48cbe4928..508a7d472 100644 --- a/tools/ctrace/src/decode/DwtPacketDecoder.cpp +++ b/tools/ctrace/src/decode/DwtPacketDecoder.cpp @@ -80,131 +80,121 @@ static std::string invalidPmuEventCounterMessage(const DwtPayloadPacket& payload return message.str(); } +/** @brief Wraps a DWT payload with its decoded-event metadata. */ +static TraceEvent makeDwtEvent(std::uint64_t index, const TraceRouteIdentity& route, std::uint64_t tcyc, + const TraceQuality& quality, TraceEventPayload payload) +{ + TraceEvent event{std::move(payload)}; + event.index = index; + event.route = route; + event.tcyc = tcyc; + event.quality = quality; + return event; +} + +/** @brief Wraps a decoded event with metadata from its source DWT packet. */ +static TraceEvent makeDwtEvent(const DwtPayloadPacket& packet, TraceEventPayload payload) +{ + return makeDwtEvent(packet.index, packet.route, packet.tcyc, packet.quality, std::move(payload)); +} + std::vector DwtPacketDecoder::decode(const DwtPayloadPacket& payload) { - std::vector output; - const auto discriminator = payload.discriminator; + switch (static_cast(payload.discriminator)) { + case DwtPacketSource::EventCounter: + return decodeEventCounter(payload); + case DwtPacketSource::ExceptionTrace: + return decodeExceptionTrace(payload); + case DwtPacketSource::PeriodicPcSample: + return decodePeriodicPcSample(payload); + case DwtPacketSource::PmuTraceOnOverflow: + return decodePmuTraceOnOverflow(payload); + } - const auto source = static_cast(discriminator); - if (source == DwtPacketSource::EventCounter) { - output = flush(payload.quality, payload.tcyc); - const auto validPayload = payload.size == 1U && payload.value != 0U && - (payload.value & ~static_cast(kDwtEventCounterValidMask)) == 0U; - if (!validPayload) { - TraceEvent error{TraceIssueEvent{ - TraceIssueCode::UnsupportedDwtEventCounterPayload, - TraceIssueSeverity::Error, - invalidEventCounterMessage(payload), - std::nullopt, - std::nullopt, - }}; - error.index = payload.index; - error.traceBusId = payload.traceBusId; - error.tcyc = payload.tcyc; - error.quality = payload.quality; - output.push_back(std::move(error)); - return output; - } - TraceEvent packet{DwtEventTraceEvent{static_cast(payload.value)}}; - packet.index = payload.index; - packet.traceBusId = payload.traceBusId; - packet.tcyc = payload.tcyc; - packet.quality = payload.quality; - output.push_back(std::move(packet)); + std::vector output; + if (payload.discriminator >= kFirstDataTraceSource && payload.discriminator <= kLastDataTraceSource) { + decodeDataTrace(payload, output); return output; } - if (source == DwtPacketSource::PmuTraceOnOverflow) { - output = flush(payload.quality, payload.tcyc); - const auto validPayload = payload.size == 1U && payload.value != 0U && (payload.value & ~kPmuOverflowMask) == 0U; - if (!validPayload) { - TraceEvent error{TraceIssueEvent{ - TraceIssueCode::UnsupportedPmuEventCounterPayload, - TraceIssueSeverity::Error, - invalidPmuEventCounterMessage(payload), - std::nullopt, - std::nullopt, - }}; - error.index = payload.index; - error.traceBusId = payload.traceBusId; - error.tcyc = payload.tcyc; - error.quality = payload.quality; - output.push_back(std::move(error)); - return output; - } - TraceEvent packet{PmuTraceEvent{static_cast(payload.value)}}; - packet.index = payload.index; - packet.traceBusId = payload.traceBusId; - packet.tcyc = payload.tcyc; - packet.quality = payload.quality; - output.push_back(std::move(packet)); + output = flush(payload.quality, payload.tcyc); + return output; +} + +std::vector DwtPacketDecoder::decodeEventCounter(const DwtPayloadPacket& payload) +{ + auto output = flush(payload.quality, payload.tcyc); + const auto validPayload = payload.size == 1U && payload.value != 0U && + (payload.value & ~static_cast(kDwtEventCounterValidMask)) == 0U; + if (!validPayload) { + output.push_back(makeDwtEvent(payload, TraceIssueEvent{ + TraceIssueCode::UnsupportedDwtEventCounterPayload, + TraceIssueSeverity::Error, + invalidEventCounterMessage(payload), + std::nullopt, + std::nullopt, + })); return output; } + output.push_back(makeDwtEvent(payload, DwtEventTraceEvent{static_cast(payload.value)})); + return output; +} - if (source == DwtPacketSource::ExceptionTrace) { - output = flush(payload.quality, payload.tcyc); - const auto exceptionNumber = static_cast(payload.value & kExceptionNumberMask); - const auto action = exceptionAction((payload.value >> kExceptionActionShift) & kExceptionActionMask); - if (action == ExceptionAction::Unknown) { - TraceEvent error{TraceIssueEvent{ - TraceIssueCode::InvalidExceptionAction, - TraceIssueSeverity::Error, - "invalid exception action 0x0 for exception " + std::to_string(exceptionNumber), - std::nullopt, - std::nullopt, - }}; - error.index = payload.index; - error.traceBusId = payload.traceBusId; - error.tcyc = payload.tcyc; - error.quality = payload.quality; - output.push_back(std::move(error)); - return output; - } - TraceEvent packet{ExceptionTraceEvent{exceptionNumber, action}}; - packet.index = payload.index; - packet.traceBusId = payload.traceBusId; - packet.tcyc = payload.tcyc; - packet.quality = payload.quality; - output.push_back(std::move(packet)); +std::vector DwtPacketDecoder::decodePmuTraceOnOverflow(const DwtPayloadPacket& payload) +{ + auto output = flush(payload.quality, payload.tcyc); + const auto validPayload = payload.size == 1U && payload.value != 0U && (payload.value & ~kPmuOverflowMask) == 0U; + if (!validPayload) { + output.push_back(makeDwtEvent(payload, TraceIssueEvent{ + TraceIssueCode::UnsupportedPmuEventCounterPayload, + TraceIssueSeverity::Error, + invalidPmuEventCounterMessage(payload), + std::nullopt, + std::nullopt, + })); return output; } + output.push_back(makeDwtEvent(payload, PmuTraceEvent{static_cast(payload.value)})); + return output; +} - if (source == DwtPacketSource::PeriodicPcSample) { - output = flush(payload.quality, payload.tcyc); - const auto isPc = payload.size == 4U; - const auto isSleeping = payload.size == 1U && payload.value == 0U; - if (!isPc && !isSleeping) { - TraceEvent error{TraceIssueEvent{ - TraceIssueCode::UnsupportedDwtPcSamplePayload, - TraceIssueSeverity::Error, - "unsupported DWT PC-sample payload: size " + std::to_string(payload.size) + - ", value " + std::to_string(payload.value) + - "; expected a 4-byte PC or a 1-byte zero sleep indication", - std::nullopt, - std::nullopt, - }}; - error.index = payload.index; - error.traceBusId = payload.traceBusId; - error.tcyc = payload.tcyc; - error.quality = payload.quality; - output.push_back(std::move(error)); - return output; - } - TraceEvent packet{PcSampleTraceEvent{payload.value, isSleeping}}; - packet.index = payload.index; - packet.traceBusId = payload.traceBusId; - packet.tcyc = payload.tcyc; - packet.quality = payload.quality; - output.push_back(std::move(packet)); +std::vector DwtPacketDecoder::decodeExceptionTrace(const DwtPayloadPacket& payload) +{ + auto output = flush(payload.quality, payload.tcyc); + const auto exceptionNumber = static_cast(payload.value & kExceptionNumberMask); + const auto action = exceptionAction((payload.value >> kExceptionActionShift) & kExceptionActionMask); + if (action == ExceptionAction::Unknown) { + output.push_back(makeDwtEvent(payload, TraceIssueEvent{ + TraceIssueCode::InvalidExceptionAction, + TraceIssueSeverity::Error, + "invalid exception action 0x0 for exception " + std::to_string(exceptionNumber), + std::nullopt, + std::nullopt, + })); return output; } + output.push_back(makeDwtEvent(payload, ExceptionTraceEvent{exceptionNumber, action})); + return output; +} - if (discriminator >= kFirstDataTraceSource && discriminator <= kLastDataTraceSource) { - decodeDataTrace(payload, output); +std::vector DwtPacketDecoder::decodePeriodicPcSample(const DwtPayloadPacket& payload) +{ + auto output = flush(payload.quality, payload.tcyc); + const auto isPc = payload.size == 4U; + const auto isSleeping = payload.size == 1U && payload.value == 0U; + if (!isPc && !isSleeping) { + output.push_back(makeDwtEvent(payload, TraceIssueEvent{ + TraceIssueCode::UnsupportedDwtPcSamplePayload, + TraceIssueSeverity::Error, + "unsupported DWT PC-sample payload: size " + std::to_string(payload.size) + + ", value " + std::to_string(payload.value) + + "; expected a 4-byte PC or a 1-byte zero sleep indication", + std::nullopt, + std::nullopt, + })); return output; } - - output = flush(payload.quality, payload.tcyc); + output.push_back(makeDwtEvent(payload, PcSampleTraceEvent{payload.value, isSleeping})); return output; } @@ -246,57 +236,57 @@ 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) { - const auto isMatch = !secondarySubtype && payload.size == kArmv8MMatchBytes && payload.value == kArmv8MMatchValue; - if (isMatch) { - auto& pending = m_pendingDataTrace[comparator]; - if (pending.has_value()) { - flushPending(comparator, qualityForPendingFlush(*pending, payload.quality), payload.tcyc, output); - } - TraceEvent match{DwtMatchTraceEvent{comparator}}; - match.index = payload.index; - match.traceBusId = payload.traceBusId; - match.tcyc = payload.tcyc; - match.quality = payload.quality; - output.push_back(std::move(match)); - return; - } - const auto supportedSize = isSupportedAddressFragmentSize(payload.size); - if (!supportedSize) { - auto flushed = flush(payload.quality, payload.tcyc); - output.insert(output.end(), std::make_move_iterator(flushed.begin()), std::make_move_iterator(flushed.end())); - TraceEvent error{TraceIssueEvent{ - TraceIssueCode::UnsupportedDwtAddressPayload, - TraceIssueSeverity::Error, - "unsupported DWT " + std::string(secondarySubtype ? "data address" : "PC or match") + - " payload size " + std::to_string(payload.size) + - "; expected 1, 2, or 4 bytes", - std::nullopt, - std::nullopt, - }}; - error.index = payload.index; - error.traceBusId = payload.traceBusId; - error.tcyc = payload.tcyc; - error.quality = payload.quality; - output.push_back(std::move(error)); - return; - } - const DwtAddressFragment fragment{payload.size, payload.value}; - if (secondarySubtype) { - event.address = fragment; - event.hasAddress = true; - } else { - event.pc = fragment; - event.hasPc = true; + decodeDataAddressTrace(payload, comparator, secondarySubtype, std::move(event), output); + return; + } + decodeDataValueTrace(payload, comparator, secondarySubtype, std::move(event), output); +} + +void DwtPacketDecoder::decodeDataAddressTrace(const DwtPayloadPacket& payload, std::uint32_t comparator, + bool secondarySubtype, PendingDataTrace event, + std::vector& output) +{ + const auto isMatch = !secondarySubtype && payload.size == kArmv8MMatchBytes && payload.value == kArmv8MMatchValue; + if (isMatch) { + auto& pending = m_pendingDataTrace[comparator]; + if (pending.has_value()) { + flushPending(comparator, qualityForPendingFlush(*pending, payload.quality), payload.tcyc, output); } - sendDataTraceEvent(comparator, event, payload.quality, payload.tcyc, output); + output.push_back(makeDwtEvent(payload, DwtMatchTraceEvent{comparator})); + return; + } + if (!isSupportedAddressFragmentSize(payload.size)) { + auto flushed = flush(payload.quality, payload.tcyc); + output.insert(output.end(), std::make_move_iterator(flushed.begin()), std::make_move_iterator(flushed.end())); + output.push_back(makeDwtEvent(payload, TraceIssueEvent{ + TraceIssueCode::UnsupportedDwtAddressPayload, + TraceIssueSeverity::Error, + "unsupported DWT " + std::string(secondarySubtype ? "data address" : "PC or match") + + " payload size " + std::to_string(payload.size) + "; expected 1, 2, or 4 bytes", + std::nullopt, + std::nullopt, + })); return; } - // Discriminators 8..23 encode either an address or a value packet. The - // address case returned above, so the remaining packet is a value. + const DwtAddressFragment fragment{payload.size, payload.value}; + if (secondarySubtype) { + event.address = fragment; + event.hasAddress = true; + } else { + event.pc = fragment; + event.hasPc = true; + } + sendDataTraceEvent(comparator, event, payload.quality, payload.tcyc, output); +} + +void DwtPacketDecoder::decodeDataValueTrace(const DwtPayloadPacket& payload, std::uint32_t comparator, + bool secondarySubtype, PendingDataTrace event, + std::vector& output) +{ event.value = payload.value; event.size = payload.size; event.isRead = !secondarySubtype; @@ -321,7 +311,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; @@ -348,16 +338,16 @@ void DwtPacketDecoder::flushPending(std::uint32_t comparator, const TraceQuality { auto& pending = m_pendingDataTrace[comparator]; - const auto makePacket = [&]() -> TraceEvent { + const auto makePayload = [&]() -> TraceEventPayload { if (pending->hasValue) { - return TraceEvent(DwtDataTraceEvent{ + return DwtDataTraceEvent{ comparator, pending->size, pending->value, pending->isRead ? AccessType::Read : AccessType::Write, pending->hasAddress ? std::optional(pending->address) : std::nullopt, pending->hasPc ? std::optional(pending->pc) : std::nullopt, - }); + }; } DwtAddressTraceLocation location = DwtDataAddressTraceLocation{pending->address}; @@ -366,15 +356,10 @@ void DwtPacketDecoder::flushPending(std::uint32_t comparator, const TraceQuality } else if (pending->hasPc) { location = DwtPcTraceLocation{pending->pc}; } - return TraceEvent(DwtAddressTraceEvent{comparator, location}); + return DwtAddressTraceEvent{comparator, location}; }; - TraceEvent packet = makePacket(); - packet.index = pending->index; - packet.traceBusId = pending->traceBusId; - packet.tcyc = tcyc; - packet.quality = quality; - output.push_back(std::move(packet)); + output.push_back(makeDwtEvent(pending->index, pending->route, tcyc, quality, makePayload())); pending.reset(); } diff --git a/tools/ctrace/src/decode/DwtPacketDecoder.h b/tools/ctrace/src/decode/DwtPacketDecoder.h index 3ddf5bb5b..121aaa0a6 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; @@ -55,8 +56,22 @@ class DwtPacketDecoder { TraceQuality quality; }; + /** @brief Decodes a DWT event-counter packet. */ + std::vector decodeEventCounter(const DwtPayloadPacket& payload); + /** @brief Decodes a PMU trace-on-overflow packet. */ + std::vector decodePmuTraceOnOverflow(const DwtPayloadPacket& payload); + /** @brief Decodes a DWT exception packet. */ + std::vector decodeExceptionTrace(const DwtPayloadPacket& payload); + /** @brief Decodes a periodic PC or sleep sample. */ + std::vector decodePeriodicPcSample(const DwtPayloadPacket& payload); /** @brief Accumulates one DWT data-trace packet and emits completed events. */ void decodeDataTrace(const DwtPayloadPacket& payload, std::vector& output); + /** @brief Decodes an address, match, or PC fragment for one comparator. */ + void decodeDataAddressTrace(const DwtPayloadPacket& payload, std::uint32_t comparator, bool secondarySubtype, + PendingDataTrace event, std::vector& output); + /** @brief Decodes a data-value fragment for one comparator. */ + void decodeDataValueTrace(const DwtPayloadPacket& payload, std::uint32_t comparator, bool secondarySubtype, + PendingDataTrace event, std::vector& output); /** @brief Converts one complete pending comparator state into an event. */ void sendDataTraceEvent(std::uint32_t comparator, const PendingDataTrace& event, const TraceQuality& quality, std::uint64_t tcyc, std::vector& output); diff --git a/tools/ctrace/src/decode/OpenCsdErrorController.cpp b/tools/ctrace/src/decode/OpenCsdErrorController.cpp index 4ff158a88..89fdaaa21 100644 --- a/tools/ctrace/src/decode/OpenCsdErrorController.cpp +++ b/tools/ctrace/src/decode/OpenCsdErrorController.cpp @@ -15,6 +15,7 @@ #include #include #include +#include /** @brief Removes line terminators and trailing whitespace from an OpenCSD message. */ static std::string trimTrailingWhitespace(std::string value) @@ -29,9 +30,7 @@ static std::string trimTrailingWhitespace(std::string value) /** @brief Uses OpenCSD itself to format one native error enum. */ static std::string openCsdErrorText(const OpenCsdErrorRecord& error) { - const auto nativeError = - error.hasIndex ? ocsdError(error.severity, error.code, static_cast(error.index), error.message) - : ocsdError(error.severity, error.code, error.message); + const ocsdError nativeError(error.severity, error.code, error.message); return trimTrailingWhitespace(ocsdError::getErrorString(nativeError)); } @@ -46,6 +45,11 @@ void OpenCsdErrorController::beginDataPathCall() m_callErrors.clear(); } +void OpenCsdErrorController::setCallbackOrderSource(std::function()> callbackOrderSource) +{ + m_callbackOrderSource = std::move(callbackOrderSource); +} + OpenCsdErrorController::Decision OpenCsdErrorController::decide(ocsd_datapath_resp_t response) const { Decision decision; @@ -118,7 +122,10 @@ TraceIssueCode OpenCsdErrorController::issueCode(const Decision& decision) std::string OpenCsdErrorController::describeApiError(ocsd_err_t code, const std::string& message) { - return openCsdErrorText({OCSD_ERR_SEV_ERROR, code, 0U, false, message}); + OpenCsdErrorRecord error; + error.code = code; + error.message = message; + return openCsdErrorText(error); } std::string OpenCsdErrorController::describeSummary(const Decision& decision) @@ -187,7 +194,11 @@ void OpenCsdErrorController::LogError(ocsd_hndl_err_log_t handle, const ocsdErro if (error == nullptr) { return; } - m_callErrors.push_back(makeRecord(*error)); + auto record = makeRecord(*error); + if (m_callbackOrderSource) { + record.callbackOrder = m_callbackOrderSource(); + } + m_callErrors.push_back(std::move(record)); ocsdDefaultErrorLogger::LogError(handle, error); } @@ -196,8 +207,13 @@ OpenCsdErrorRecord OpenCsdErrorController::makeRecord(const ocsdError& error) OpenCsdErrorRecord record; record.severity = error.getErrorSeverity(); record.code = error.getErrorCode(); - record.hasIndex = error.getErrorIndex() != OCSD_BAD_TRC_INDEX; - record.index = record.hasIndex ? static_cast(error.getErrorIndex()) : 0U; + const auto index = error.getErrorIndex(); + record.hasIndex = index != OCSD_BAD_TRC_INDEX; + record.index = record.hasIndex ? static_cast(index) : 0U; + const auto channel = error.getErrorChanID(); + if (channel != OCSD_BAD_CS_SRC_ID) { + record.channel = channel; + } record.message = trimTrailingWhitespace(error.getMessage()); return record; } diff --git a/tools/ctrace/src/decode/OpenCsdErrorController.h b/tools/ctrace/src/decode/OpenCsdErrorController.h index 8cf43cd70..db31aa1dd 100644 --- a/tools/ctrace/src/decode/OpenCsdErrorController.h +++ b/tools/ctrace/src/decode/OpenCsdErrorController.h @@ -13,6 +13,7 @@ #include "opencsd/ocsd_if_types.h" #include +#include #include #include #include @@ -23,6 +24,8 @@ struct OpenCsdErrorRecord { ocsd_err_t code = OCSD_OK; std::uint64_t index = 0; bool hasIndex = false; + std::optional channel; + std::optional callbackOrder; std::string message; }; @@ -50,6 +53,8 @@ class OpenCsdErrorController final : public ocsdDefaultErrorLogger { /** @brief Clears errors before invoking one OpenCSD data-path operation. */ void beginDataPathCall(); + /** @brief Binds the operation-local order source shared with trace callbacks. */ + void setCallbackOrderSource(std::function()> callbackOrderSource); /** @brief Classifies the response and errors from the current data-path operation. */ Decision decide(ocsd_datapath_resp_t response) const; @@ -74,6 +79,7 @@ class OpenCsdErrorController final : public ocsdDefaultErrorLogger { static OpenCsdErrorRecord makeRecord(const ocsdError& error); std::vector m_callErrors; + std::function()> m_callbackOrderSource; }; #endif // CTRACE_SRC_DECODE_OPENCSDERRORCONTROLLER_H diff --git a/tools/ctrace/src/decode/OpenCsdFormattedItmSession.cpp b/tools/ctrace/src/decode/OpenCsdFormattedItmSession.cpp new file mode 100644 index 000000000..cced8a1b1 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdFormattedItmSession.cpp @@ -0,0 +1,259 @@ +/* + * 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 std::move(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) +{ + // OpenCSD reaches these overrides only through interfaces implemented in the + // external library. Retain the concrete callback identities at the binding site. + const auto packetCallback = &RoutePacketMonitor::RawPacketDataMon; + const auto rawFrameCallback = &RawFrameMonitor::TraceRawFrameIn; + (void)packetCallback; + (void)rawFrameCallback; + + 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) +{ + const auto response = completeOperation(m_treeSession.traceDataIn(OCSD_OP_DATA, index, size, data, &processed)); + m_receivedInput = m_receivedInput || processed > 0U; + return response; +} + +ocsd_datapath_resp_t OpenCsdFormattedItmSession::flush() +{ + // OpenCSD's formatted frontend has no initialized frame to flush until it + // has consumed input. Avoid entering that external undefined state. + if (!m_receivedInput) { + return OCSD_RESP_CONT; + } + return completeOperation(m_treeSession.traceDataIn(OCSD_OP_FLUSH, 0, 0, nullptr, nullptr)); +} + +ocsd_datapath_resp_t OpenCsdFormattedItmSession::resetRoute(std::uint8_t channel, ocsd_trc_index_t index) +{ + return completeOperation(m_treeSession.resetDecoder(channel, index)); +} + +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..b055fe7e2 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdFormattedItmSession.h @@ -0,0 +1,133 @@ +/* + * 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 one ITM decoder pair while preserving deformatter state. */ + ocsd_datapath_resp_t resetRoute(std::uint8_t channel, ocsd_trc_index_t index) 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; + bool m_receivedInput = false; +}; + +#endif // CTRACE_SRC_DECODE_OPENCSDFORMATTEDITMSESSION_H diff --git a/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp b/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp index a4a663820..b4af062c1 100644 --- a/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp +++ b/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp @@ -7,31 +7,57 @@ #include "OpenCsdItmDecoder.h" +#include "CoreSightFormatter.h" #include "TraceEvent.h" #include "OpenCsdErrorController.h" +#include "OpenCsdFormattedItmSession.h" #include "OpenCsdPacketCollector.h" #include "OpenCsdItmSession.h" #include "OpenCsdTraceElement.h" +#include "TraceRoute.h" #include "opencsd/ocsd_if_types.h" #include +#include #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 Implements OpenCSD feeding, bounded retry, and hardware-sync recovery. */ class OpenCsdItmDecoderImpl { public: - /** @brief Creates a decoder implementation around one session factory. */ - OpenCsdItmDecoderImpl(OpenCsdTraceElementSink& elementSink, const OpenCsdItmSessionFactory& sessionFactory) - : m_collector(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)) { + m_errorController.setCallbackOrderSource([this] { return m_collector.reserveTransactionOrder(); }); 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"); } @@ -46,10 +72,25 @@ 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 % CoreSightFormatter::kMemoryAlignedFrameSize != 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); - processBlock(data + offset, span); + const auto span = + isFormatted() ? std::min(CoreSightFormatter::kMemoryAlignedFrameSize, size - offset) + : std::min(kMaxTraceDataInBytes, size - offset); + if (isFormatted()) { + processFormattedFrame(data + offset, span); + } else { + processSingleBlock(data + offset, span); + } offset += span; } m_result.bytesIn = static_cast(m_traceIndex); @@ -61,11 +102,14 @@ class OpenCsdItmDecoderImpl { if (m_finished) { return m_result; } + if (isFormatted()) { + return finishFormatted(); + } 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: "); @@ -88,6 +132,365 @@ class OpenCsdItmDecoderImpl { private: static constexpr std::uint32_t kMaxTraceDataInBytes = 4U * 1024U; + static constexpr std::uint32_t kMaxFlushCalls = 1024U; + + /** @brief Describes the first recoverable failure observed for one formatted route. */ + struct FormattedRouteFailure { + TraceRouteIdentity route; + std::uint64_t sourceOffset = 0U; + }; + + /** @brief Stores the route-local loss interval opened by a successful decoder reset. */ + struct FormattedRecoveryState { + TraceRouteIdentity route; + std::uint64_t sourceOffset = 0U; + }; + + using FormattedRouteFailures = std::map; + + /** @brief Classifies one completed formatted-tree operation without mutating OpenCSD. */ + struct FormattedOperationOutcome { + FormattedRouteFailures failures; + OpenCsdErrorController::Decision fatalDecision; + bool fatal = false; + bool wait = false; + }; + + /** @brief Tracks progress while one memory-aligned formatter frame is consumed. */ + struct FormattedFrameState { + std::uint32_t processed = 0U; + bool retriedWithoutProgress = false; + }; + + /** @brief Captures the result of one formatted root DATA operation. */ + struct FormattedPushResult { + std::uint64_t baseOffset = 0U; + std::uint32_t supplied = 0U; + std::uint32_t consumed = 0U; + OpenCsdErrorController::Decision decision; + FormattedOperationOutcome outcome; + }; + + /** @brief Tracks input progress and bounded retries within one SINGLE block. */ + struct SingleBlockState { + std::uint32_t processed = 0U; + bool retriedWithoutProgress = false; + }; + + /** @brief Captures the result of one SINGLE root DATA operation. */ + struct SinglePushResult { + std::uint64_t baseOffset = 0U; + std::uint32_t supplied = 0U; + std::uint32_t consumed = 0U; + OpenCsdErrorController::Decision decision; + }; + + /** @brief Tracks which formatted diagnostics have already opened a discontinuity. */ + struct FormattedDiagnosticState { + std::set discontinuousRoutes; + bool emittedError = false; + bool inputWideDiscontinuity = true; + }; + + /** @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 Returns the earliest raw failure offset for every affected formatted route. */ + static std::map failureOffsets(const FormattedRouteFailures& failures) + { + std::map offsets; + for (const auto& [routeId, failure] : failures) { + offsets.emplace(routeId, failure.sourceOffset); + } + return offsets; + } + + /** @brief Records one route's earliest recoverable failure in an operation outcome. */ + static void recordFormattedFailure(FormattedOperationOutcome& outcome, const TraceRouteIdentity& route, + std::uint64_t sourceOffset) + { + const auto found = outcome.failures.find(route.id); + if (found == outcome.failures.end()) { + outcome.failures.emplace(route.id, FormattedRouteFailure{route, sourceOffset}); + } else { + found->second.sourceOffset = std::min(found->second.sourceOffset, sourceOffset); + } + } + + /** @brief Classifies one formatted error callback as route-local or input-fatal. */ + void classifyFormattedError(FormattedOperationOutcome& outcome, const OpenCsdErrorRecord& error, + std::uint64_t baseOffset) const + { + if (error.severity != OCSD_ERR_SEV_ERROR) { + return; + } + const auto* route = error.channel.has_value() ? m_collector.routeForChannel(*error.channel) : nullptr; + if (!OpenCsdErrorController::isRecoverableStreamError(error.code) || route == nullptr) { + if (!outcome.fatal) { + outcome.fatalDecision.error = error; + } + outcome.fatal = true; + return; + } + + recordFormattedFailure(outcome, *route, error.hasIndex ? error.index : baseOffset); + } + + /** @brief Returns whether a callback batch contains an input-fatal decoder error. */ + static bool hasNonRecoverableError(const OpenCsdErrorController::Decision& decision) + { + return std::any_of(decision.errors.begin(), decision.errors.end(), [](const auto& error) { + return error.severity == OCSD_ERR_SEV_ERROR && + !OpenCsdErrorController::isRecoverableStreamError(error.code); + }); + } + + /** @brief Applies root response semantics after callback errors have been classified. */ + static void classifyFormattedResponse(FormattedOperationOutcome& outcome, + const OpenCsdErrorController::Decision& decision) + { + const auto recoverableInvalidData = + decision.response == OCSD_RESP_FATAL_INVALID_DATA && !outcome.fatal && !outcome.failures.empty(); + if (OCSD_DATA_RESP_IS_FATAL(decision.response) && !recoverableInvalidData) { + outcome.fatal = true; + if (decision.response != OCSD_RESP_FATAL_INVALID_DATA && !hasNonRecoverableError(decision)) { + outcome.fatalDecision.error.reset(); + outcome.fatalDecision.errors.clear(); + } + } + if (OpenCsdErrorController::responseReportsError(decision.response) && outcome.failures.empty()) { + outcome.fatal = true; + } + } + + /** @brief Resolves recoverable formatted errors and rejects every input-wide failure. */ + FormattedOperationOutcome classifyFormattedOperation(const OpenCsdErrorController::Decision& decision, + std::uint64_t baseOffset) const + { + FormattedOperationOutcome outcome; + outcome.fatalDecision = decision; + outcome.wait = OCSD_DATA_RESP_IS_WAIT(decision.response); + for (const auto& error : decision.errors) { + classifyFormattedError(outcome, error, baseOffset); + } + classifyFormattedResponse(outcome, decision); + + const auto cutoffs = failureOffsets(outcome.failures); + if (m_collector.transactionHasUnmatchedError(cutoffs)) { + outcome.fatal = true; + if (decision.errors.empty()) { + outcome.fatalDecision.error.reset(); + } + } + return outcome; + } + + /** @brief Seeds one formatted diagnostic batch with already active route recoveries. */ + FormattedDiagnosticState formattedDiagnosticState() const + { + FormattedDiagnosticState state; + for (const auto& [routeId, recovery] : m_formattedRecoveries) { + static_cast(recovery); + state.discontinuousRoutes.insert(routeId); + } + return state; + } + + /** @brief Appends one formatted warning or error with its route-local discontinuity state. */ + void appendFormattedReportedError(const OpenCsdErrorController::Decision& decision, + const OpenCsdErrorRecord& error, std::uint64_t baseOffset, + FormattedDiagnosticState& state) + { + auto item = decision; + item.error = error; + const auto sourceOffset = OpenCsdErrorController::errorOffset(item, baseOffset); + const auto isError = error.severity == OCSD_ERR_SEV_ERROR; + const auto* route = error.channel.has_value() ? m_collector.routeForChannel(*error.channel) : nullptr; + bool discontinuity = false; + if (isError) { + state.emittedError = true; + if (route != nullptr) { + discontinuity = state.discontinuousRoutes.insert(route->id).second; + } else { + discontinuity = std::exchange(state.inputWideDiscontinuity, false); + } + } + + const auto severity = isError ? TraceIssueSeverity::Error : TraceIssueSeverity::Warning; + if (route != nullptr) { + m_collector.appendReportedDecodeError(*route, static_cast(sourceOffset), + OpenCsdErrorController::describeSummary(item), error.callbackOrder, + OpenCsdErrorController::issueCode(item), discontinuity, severity); + } else { + m_collector.appendReportedDecodeError(static_cast(sourceOffset), + OpenCsdErrorController::describeSummary(item), error.callbackOrder, + OpenCsdErrorController::issueCode(item), discontinuity, severity); + } + } + + /** @brief Reports a formatted callback batch while retaining exact channel attribution. */ + void appendFormattedReportedErrors(const OpenCsdErrorController::Decision& decision, std::uint64_t baseOffset, + bool force = false) + { + auto state = formattedDiagnosticState(); + for (const auto& error : decision.errors) { + if (error.severity == OCSD_ERR_SEV_ERROR || error.severity == OCSD_ERR_SEV_WARN) { + appendFormattedReportedError(decision, error, baseOffset, state); + } + } + if (!state.emittedError && (force || OpenCsdErrorController::responseReportsError(decision.response))) { + m_collector.appendDecodeError( + static_cast(OpenCsdErrorController::errorOffset(decision, baseOffset)), + OpenCsdErrorController::describeSummary(decision), OpenCsdErrorController::issueCode(decision), true, + TraceIssueSeverity::Error); + } + } + + /** @brief Inserts completed data-loss intervals immediately before each route's recovered sync. */ + void closeFormattedRecoveries(const FormattedRouteFailures& failures) + { + for (auto recovery = m_formattedRecoveries.begin(); recovery != m_formattedRecoveries.end();) { + std::optional beforeOffset; + const auto failure = failures.find(recovery->first); + if (failure != failures.end()) { + beforeOffset = failure->second.sourceOffset; + } + const auto syncOffset = m_collector.transactionFirstSyncOffset(recovery->second.route, beforeOffset); + if (!syncOffset.has_value()) { + ++recovery; + continue; + } + + const auto startOffset = recovery->second.sourceOffset; + const auto rawBytesConsumed = *syncOffset > startOffset ? *syncOffset - startOffset : 0U; + const auto message = "OpenCSD discarded " + std::to_string(rawBytesConsumed) + + " raw bytes for this ITM route while searching for the next hardware sync"; + static_cast(m_collector.insertDataLossBeforeSync( + recovery->second.route, static_cast(startOffset), message, rawBytesConsumed, beforeOffset)); + recovery = m_formattedRecoveries.erase(recovery); + } + } + + /** @brief Opens route-local recovery intervals without shortening an already active interval. */ + void openFormattedRecoveries(const FormattedRouteFailures& failures) + { + for (const auto& [routeId, failure] : failures) { + m_formattedRecoveries.try_emplace(routeId, FormattedRecoveryState{failure.route, failure.sourceOffset}); + } + } + + /** @brief Commits one formatted callback batch according to its route-local failure boundaries. */ + void commitFormattedOperation(const FormattedOperationOutcome& outcome, + const OpenCsdErrorController::Decision& decision, std::uint64_t baseOffset) + { + closeFormattedRecoveries(outcome.failures); + appendFormattedReportedErrors(decision, baseOffset); + if (outcome.failures.empty()) { + if (m_collector.transactionElementCount() == 0U) { + m_collector.rollbackTransaction(); + } else { + m_collector.commitTransaction(); + } + } else { + m_collector.commitTransactionForRouteFailures(failureOffsets(outcome.failures)); + } + } + + /** @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); + } + + /** @brief Aborts a formatted operation after rolling back every unsafe callback. */ + [[noreturn]] void abortFormattedDecode(const OpenCsdErrorController::Decision& decision, std::uint32_t size, + std::uint64_t baseOffset, std::uint32_t bytesConsumed, + const std::string& prefix, bool preserveIncompleteTail = false) + { + std::size_t retainedErrors = 0U; + if (preserveIncompleteTail) { + retainedErrors = m_collector.commitTransactionErrors(TraceIssueCode::OpenCsdIncompleteTail); + } else { + m_collector.rollbackTransaction(); + } + appendFormattedReportedErrors(decision, baseOffset, retainedErrors == 0U); + const auto processed = baseOffset + std::min(bytesConsumed, size); + throw OpenCsdFatalError(prefix + OpenCsdErrorController::describeSummary(decision), processed); + } + + /** @brief Aborts on an invalid formatted root-consumption result. */ + [[noreturn]] void abortFormattedProgress(std::uint64_t baseOffset, std::uint32_t size, std::uint32_t bytesConsumed, + const std::string& message) + { + m_collector.rollbackTransaction(); + m_collector.appendDecodeError(static_cast(baseOffset), message, TraceIssueCode::OpenCsdNoProgress, + false); + const auto processed = baseOffset + std::min(bytesConsumed, size); + throw OpenCsdFatalError(message, processed); + } void appendReportedErrors(const OpenCsdErrorController::Decision& decision, std::uint64_t baseOffset, bool discontinuity, bool force = false) @@ -124,11 +527,210 @@ class OpenCsdItmDecoderImpl { { m_collector.rollbackTransaction(); completeConsumedDataLoss(OpenCsdErrorController::errorOffset(decision, baseOffset)); - appendReportedErrors(decision, baseOffset, true); + appendReportedErrors(decision, baseOffset, true, true); const auto processed = baseOffset + std::min(bytesConsumed, size); throw OpenCsdFatalError(prefix + OpenCsdErrorController::describeSummary(decision), processed); } + /** @brief Resets one formatted protocol chain without changing deformatter state. */ + void resetFormattedRoute(const FormattedRouteFailure& failure) + { + const auto channel = *failure.route.traceBusId; + m_collector.beginTransaction(); + m_errorController.beginDataPathCall(); + const auto response = invokeSessionOperation( + [&] { return m_session->resetRoute(channel, static_cast(failure.sourceOffset)); }, + m_traceIndex, 0U, nullptr, "OpenCSD route-local decoder reset failed: "); + const auto decision = m_errorController.decide(response); + const auto outcome = classifyFormattedOperation(decision, failure.sourceOffset); + if (outcome.fatal || outcome.wait || !outcome.failures.empty()) { + m_collector.rollbackTransaction(); + const auto hasReportedError = std::any_of(decision.errors.begin(), decision.errors.end(), + [](const auto& error) { return error.severity == OCSD_ERR_SEV_ERROR; }); + appendFormattedReportedErrors(decision, failure.sourceOffset); + if (!hasReportedError) { + m_collector.appendDecodeError(failure.route, static_cast(failure.sourceOffset), + "OpenCSD route-local decoder reset failed: " + + OpenCsdErrorController::describeSummary(decision), + TraceIssueCode::OpenCsdDecodeError, false); + } + throw OpenCsdFatalError("OpenCSD route-local decoder reset failed: " + + OpenCsdErrorController::describeSummary(decision), + static_cast(m_traceIndex)); + } + appendFormattedReportedErrors(decision, failure.sourceOffset); + if (m_collector.transactionElementCount() == 0U) { + m_collector.rollbackTransaction(); + } else { + m_collector.commitTransaction(); + } + } + + /** @brief Resets every route that failed during one completed root operation. */ + void resetFormattedRoutes(const FormattedRouteFailures& failures) + { + for (const auto& [routeId, failure] : failures) { + static_cast(routeId); + resetFormattedRoute(failure); + } + } + + /** @brief Drains pending deformatter segments with bounded root FLUSH calls. */ + void drainFormattedPending() + { + for (std::uint32_t call = 0U; call < kMaxFlushCalls; ++call) { + m_collector.beginTransaction(); + m_errorController.beginDataPathCall(); + const auto response = invokeSessionOperation([&] { return m_session->flush(); }, m_traceIndex, 0U, nullptr, + "OpenCSD aborted while draining formatted trace: "); + const auto decision = m_errorController.decide(response); + const auto outcome = classifyFormattedOperation(decision, m_traceIndex); + if (outcome.fatal) { + abortFormattedDecode(outcome.fatalDecision, 0U, m_traceIndex, 0U, + "OpenCSD aborted while draining formatted trace: "); + } + + commitFormattedOperation(outcome, decision, m_traceIndex); + if (!outcome.failures.empty()) { + openFormattedRecoveries(outcome.failures); + resetFormattedRoutes(outcome.failures); + continue; + } + if (!outcome.wait) { + return; + } + } + + const auto limit = std::to_string(kMaxFlushCalls); + m_collector.appendDecodeError( + m_traceIndex, "OpenCSD formatted drain did not clear after " + limit + " FLUSH operations; decode aborted", + TraceIssueCode::OpenCsdWaitTimeout); + throw OpenCsdFatalError("OpenCSD formatted drain did not clear after " + limit + " FLUSH operations", + static_cast(m_traceIndex)); + } + + /** @brief Executes and validates one formatted root DATA operation. */ + FormattedPushResult pushFormattedData(const std::uint8_t* data, std::uint32_t size) + { + FormattedPushResult result; + result.baseOffset = static_cast(m_traceIndex); + result.supplied = size; + m_collector.beginTransaction(); + m_errorController.beginDataPathCall(); + const auto response = invokeSessionOperation( + [&] { return m_session->pushData(m_traceIndex, result.supplied, data, result.consumed); }, result.baseOffset, + result.supplied, &result.consumed, "OpenCSD aborted decode: "); + result.decision = m_errorController.decide(response); + result.outcome = classifyFormattedOperation(result.decision, result.baseOffset); + if (result.outcome.fatal) { + abortFormattedDecode(result.outcome.fatalDecision, result.supplied, result.baseOffset, result.consumed, + "OpenCSD aborted decode: "); + } + if (result.consumed > result.supplied) { + abortFormattedProgress(result.baseOffset, result.supplied, result.supplied, + "OpenCSD reported more formatted bytes processed than were supplied"); + } + if (result.consumed != 0U && result.consumed != result.supplied) { + abortFormattedProgress(result.baseOffset, result.supplied, result.consumed, + "OpenCSD stopped inside a memory-aligned formatter frame"); + } + return result; + } + + /** @brief Performs any route reset and root draining requested by a formatted DATA operation. */ + void recoverFormattedData(const FormattedOperationOutcome& outcome) + { + if (!outcome.failures.empty()) { + openFormattedRecoveries(outcome.failures); + resetFormattedRoutes(outcome.failures); + drainFormattedPending(); + } else if (outcome.wait) { + drainFormattedPending(); + } + } + + /** @brief Updates the bounded retry state after recovery and rejects permanent stalls. */ + void updateFormattedProgress(FormattedFrameState& state, const FormattedPushResult& result) + { + if (result.consumed != 0U) { + state.retriedWithoutProgress = false; + return; + } + if (!result.outcome.wait && result.outcome.failures.empty()) { + 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)); + } + if (state.retriedWithoutProgress) { + m_collector.appendDecodeError(m_traceIndex, + "OpenCSD made no progress after draining formatted trace; decode aborted", + TraceIssueCode::OpenCsdNoProgress, false); + throw OpenCsdFatalError("OpenCSD made no progress after draining formatted trace", + static_cast(m_traceIndex)); + } + state.retriedWithoutProgress = true; + } + + /** @brief Processes exactly one memory-aligned formatter frame. */ + void processFormattedFrame(const std::uint8_t* data, std::uint32_t size) + { + FormattedFrameState state; + while (state.processed < size) { + const auto result = pushFormattedData(data + state.processed, size - state.processed); + commitFormattedOperation(result.outcome, result.decision, result.baseOffset); + state.processed += result.consumed; + m_traceIndex += result.consumed; + recoverFormattedData(result.outcome); + updateFormattedProgress(state, result); + } + } + + /** @brief Emits one explicit unresolved interval for every route still awaiting hardware sync. */ + void closeUnresolvedFormattedRecoveries() + { + for (const auto& [routeId, recovery] : m_formattedRecoveries) { + static_cast(routeId); + const auto rawBytesConsumed = + m_traceIndex > recovery.sourceOffset ? static_cast(m_traceIndex) - recovery.sourceOffset : 0U; + const auto message = "OpenCSD discarded " + std::to_string(rawBytesConsumed) + + " raw bytes for this ITM route; no later hardware sync before end of input"; + m_collector.appendDataLossError(recovery.route, static_cast(recovery.sourceOffset), message, + rawBytesConsumed); + } + m_formattedRecoveries.clear(); + } + + /** @brief Completes a formatted stream without resetting unresolved routes at end-of-input. */ + OpenCsdItmDecodeResult finishFormatted() + { + m_collector.beginTransaction(); + m_errorController.beginDataPathCall(); + 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); + const auto outcome = classifyFormattedOperation(decision, m_traceIndex); + if (outcome.fatal) { + abortFormattedDecode(outcome.fatalDecision, 0U, m_traceIndex, 0U, + "OpenCSD aborted end-of-trace processing: ", true); + } + + commitFormattedOperation(outcome, decision, m_traceIndex); + if (!outcome.failures.empty()) { + openFormattedRecoveries(outcome.failures); + if (outcome.wait) { + resetFormattedRoutes(outcome.failures); + drainFormattedPending(); + } + } else if (outcome.wait) { + drainFormattedPending(); + } + closeUnresolvedFormattedRecoveries(); + m_finished = true; + m_result.bytesIn = static_cast(m_traceIndex); + return m_result; + } + void completeConsumedDataLoss(std::uint64_t resumeOffset) { if (!m_consumedDataLossStart.has_value()) { @@ -154,109 +756,147 @@ class OpenCsdItmDecoderImpl { } } - void processBlock(const std::uint8_t* data, std::uint32_t size) + /** @brief Executes one SINGLE root DATA operation and normalizes its consumed-byte count. */ + SinglePushResult pushSingleData(const std::uint8_t* data, std::uint32_t size) { - std::uint32_t processed = 0; - bool retriedWithoutProgress = false; - while (processed < size) { - const auto callIndex = static_cast(m_traceIndex); - const auto callSize = size - processed; - 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 decision = m_errorController.decide(response); - if (decision.action == OpenCsdErrorController::Action::Abort) { - abortDecode(decision, callSize, callIndex, processedThisPass, "OpenCSD aborted decode: "); - } + SinglePushResult result; + result.baseOffset = static_cast(m_traceIndex); + result.supplied = size; + m_collector.beginTransaction(); + m_errorController.beginDataPathCall(); + const auto response = invokeSessionOperation( + [&] { return m_session->pushData(m_traceIndex, result.supplied, data, result.consumed); }, result.baseOffset, + result.supplied, &result.consumed, "OpenCSD aborted decode: "); + result.decision = m_errorController.decide(response); + if (result.decision.action == OpenCsdErrorController::Action::Abort) { + abortDecode(result.decision, result.supplied, result.baseOffset, result.consumed, "OpenCSD aborted decode: "); + } + result.consumed = std::min(result.consumed, result.supplied); + return result; + } - const auto consumed = std::min(processedThisPass, callSize); - if (consumed == 0U) { - if (retriedWithoutProgress) { - m_collector.rollbackTransaction(); - completeConsumedDataLoss(m_traceIndex); - m_collector.appendDecodeError(m_traceIndex, "OpenCSD made no progress after a retry; decode aborted", - TraceIssueCode::OpenCsdNoProgress, false); - throw OpenCsdFatalError("OpenCSD made no progress after a retry", static_cast(m_traceIndex)); - } - retriedWithoutProgress = true; - } else { - retriedWithoutProgress = false; - } + /** @brief Updates the bounded SINGLE retry state before response-specific recovery. */ + void updateSingleProgress(SingleBlockState& state, const SinglePushResult& result) + { + if (result.consumed != 0U) { + state.retriedWithoutProgress = false; + return; + } + if (state.retriedWithoutProgress) { + m_collector.rollbackTransaction(); + completeConsumedDataLoss(m_traceIndex); + m_collector.appendDecodeError(m_traceIndex, "OpenCSD made no progress after a retry; decode aborted", + TraceIssueCode::OpenCsdNoProgress, false); + throw OpenCsdFatalError("OpenCSD made no progress after a retry", static_cast(m_traceIndex)); + } + state.retriedWithoutProgress = true; + } - if (decision.action == OpenCsdErrorController::Action::RecoverStream) { - const auto sourceOffset = OpenCsdErrorController::errorOffset(decision, callIndex); - completeConsumedDataLoss( - std::min(sourceOffset, m_collector.transactionFirstSourceOffset().value_or(sourceOffset))); - // Callbacks before the bad packet remain valid; callbacks at or - // after its offset belong to the failed decode transaction. - m_collector.commitTransactionBefore(sourceOffset); - appendReportedErrors(decision, callIndex, true); - processed += consumed; - m_traceIndex += consumed; - m_dataLossActive = true; - m_consumedDataLossStart = static_cast(m_traceIndex); - m_consumedDataLossBoundaryMarked = true; - resetDecoder(); - continue; - } - if (decision.action == OpenCsdErrorController::Action::Wait) { - if (m_collector.transactionElementCount() == 0U) { - m_collector.rollbackTransaction(); - } else { - completeConsumedDataLoss(m_collector.transactionFirstSourceOffset().value_or(callIndex)); - m_collector.commitTransaction(); - } - appendReportedErrors(decision, callIndex, false); - processed += consumed; - m_traceIndex += consumed; - flushAfterWait(); - continue; - } - if (consumed == 0U) { - m_collector.rollbackTransaction(); - m_collector.appendDecodeError( - m_traceIndex, - "OpenCSD made no progress while raw data was present; decoder reset and searching " - "for next real ITM async sync", - TraceIssueCode::OpenCsdNoProgress); - m_dataLossActive = true; - m_consumedDataLossStart = static_cast(m_traceIndex); - m_consumedDataLossBoundaryMarked = true; - resetDecoder(); - continue; - } - if (m_collector.transactionElementCount() == 0U) { - m_collector.rollbackTransaction(); - appendReportedErrors(decision, callIndex, false); - if (!m_dataLossActive) { - m_consumedDataLossStart = static_cast(m_traceIndex); - m_consumedDataLossBoundaryMarked = false; - m_dataLossActive = true; - } - processed += consumed; - m_traceIndex += consumed; - continue; - } - completeConsumedDataLoss(m_collector.transactionFirstSourceOffset().value_or(callIndex)); + /** @brief Advances both the caller-visible block cursor and the absolute raw trace index. */ + void advanceSingleInput(SingleBlockState& state, std::uint32_t consumed) + { + state.processed += consumed; + m_traceIndex += consumed; + } + + /** @brief Commits valid callbacks before a recoverable SINGLE stream error and resets the decoder. */ + void recoverSingleStream(SingleBlockState& state, const SinglePushResult& result) + { + const auto sourceOffset = OpenCsdErrorController::errorOffset(result.decision, result.baseOffset); + completeConsumedDataLoss( + std::min(sourceOffset, m_collector.transactionFirstSourceOffset().value_or(sourceOffset))); + // Callbacks before the bad packet remain valid; callbacks at or + // after its offset belong to the failed decode transaction. + m_collector.commitTransactionBefore(sourceOffset); + appendReportedErrors(result.decision, result.baseOffset, true); + advanceSingleInput(state, result.consumed); + m_dataLossActive = true; + m_consumedDataLossStart = static_cast(m_traceIndex); + m_consumedDataLossBoundaryMarked = true; + resetDecoder(static_cast(sourceOffset)); + } + + /** @brief Commits any valid SINGLE callbacks before draining a WAIT response. */ + void drainSingleWait(SingleBlockState& state, const SinglePushResult& result) + { + if (m_collector.transactionElementCount() == 0U) { + m_collector.rollbackTransaction(); + } else { + completeConsumedDataLoss(m_collector.transactionFirstSourceOffset().value_or(result.baseOffset)); m_collector.commitTransaction(); - appendReportedErrors(decision, callIndex, false); - m_dataLossActive = false; - processed += consumed; - m_traceIndex += consumed; + } + appendReportedErrors(result.decision, result.baseOffset, false); + advanceSingleInput(state, result.consumed); + flushAfterWait(); + } + + /** @brief Resets a stalled SINGLE decoder so the next call can search for hardware sync. */ + void recoverSingleNoProgress() + { + m_collector.rollbackTransaction(); + m_collector.appendDecodeError( + m_traceIndex, + "OpenCSD made no progress while raw data was present; decoder reset and searching " + "for next real ITM async sync", + TraceIssueCode::OpenCsdNoProgress); + m_dataLossActive = true; + m_consumedDataLossStart = static_cast(m_traceIndex); + m_consumedDataLossBoundaryMarked = true; + resetDecoder(m_traceIndex); + } + + /** @brief Records consumed SINGLE input that produced no trace elements. */ + void consumeSilentSingleData(SingleBlockState& state, const SinglePushResult& result) + { + m_collector.rollbackTransaction(); + appendReportedErrors(result.decision, result.baseOffset, false); + if (!m_dataLossActive) { + m_consumedDataLossStart = static_cast(m_traceIndex); + m_consumedDataLossBoundaryMarked = false; + m_dataLossActive = true; + } + advanceSingleInput(state, result.consumed); + } + + /** @brief Commits a successful SINGLE DATA operation containing trace elements. */ + void commitSingleData(SingleBlockState& state, const SinglePushResult& result) + { + completeConsumedDataLoss(m_collector.transactionFirstSourceOffset().value_or(result.baseOffset)); + m_collector.commitTransaction(); + appendReportedErrors(result.decision, result.baseOffset, false); + m_dataLossActive = false; + advanceSingleInput(state, result.consumed); + } + + /** @brief Processes one bounded block of unformatted SINGLE trace input. */ + void processSingleBlock(const std::uint8_t* data, std::uint32_t size) + { + SingleBlockState state; + while (state.processed < size) { + const auto result = pushSingleData(data + state.processed, size - state.processed); + updateSingleProgress(state, result); + if (result.decision.action == OpenCsdErrorController::Action::RecoverStream) { + recoverSingleStream(state, result); + } else if (result.decision.action == OpenCsdErrorController::Action::Wait) { + drainSingleWait(state, result); + } else if (result.consumed == 0U) { + recoverSingleNoProgress(); + } else if (m_collector.transactionElementCount() == 0U) { + consumeSilentSingleData(state, result); + } else { + commitSingleData(state, result); + } } } void flushAfterWait() { // Bound backpressure handling so a broken decoder cannot stall a file forever. - static constexpr std::uint32_t kMaxFlushCalls = 1024U; 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) { abortDecode(decision, 0U, m_traceIndex, 0U, "OpenCSD aborted while flushing a WAIT response: "); @@ -265,7 +905,7 @@ class OpenCsdItmDecoderImpl { const auto sourceOffset = OpenCsdErrorController::errorOffset(decision, m_traceIndex); m_collector.commitTransactionBefore(sourceOffset); appendReportedErrors(decision, m_traceIndex, true); - resetDecoder(); + resetDecoder(static_cast(sourceOffset)); return; } if (m_collector.transactionElementCount() == 0U) { @@ -286,10 +926,11 @@ class OpenCsdItmDecoderImpl { static_cast(m_traceIndex)); } - void resetDecoder() + /** @brief Resets the channel-zero SINGLE decoder at the reported recovery boundary. */ + void resetDecoder(ocsd_trc_index_t index) { m_errorController.beginDataPathCall(); - const auto response = m_session->reset(); + const auto response = m_session->resetRoute(0U, index); m_collector.rethrowOutputError(); const auto decision = m_errorController.decide(response); if (decision.action != OpenCsdErrorController::Action::Continue) { @@ -306,6 +947,9 @@ class OpenCsdItmDecoderImpl { throw OpenCsdFatalError(message, static_cast(m_traceIndex)); } + // Declaration order is intentional: the external session is destroyed + // before the callback targets whose addresses may still be installed in it. + OpenCsdItmInputMode m_inputMode = OpenCsdItmInputMode::Single; OpenCsdPacketCollector m_collector; OpenCsdErrorController m_errorController; std::unique_ptr m_session; @@ -313,20 +957,24 @@ class OpenCsdItmDecoderImpl { bool m_dataLossActive = false; std::optional m_consumedDataLossStart; bool m_consumedDataLossBoundaryMarked = false; + std::map m_formattedRecoveries; OpenCsdItmDecodeResult m_result; bool m_finished = false; }; -OpenCsdItmDecoder::OpenCsdItmDecoder(OpenCsdTraceElementSink& elementSink) - : OpenCsdItmDecoder(elementSink, [](OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController) { - return std::make_unique(collector, errorController); - }) +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(OpenCsdTraceElementSink& elementSink, +OpenCsdItmDecoder::OpenCsdItmDecoder(std::vector routes, OpenCsdItmInputMode inputMode, + OpenCsdTraceElementSink& elementSink, const OpenCsdItmSessionFactory& sessionFactory) - : m_impl(std::make_unique(elementSink, 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 4ae654251..ccef5b3f6 100644 --- a/tools/ctrace/src/decode/OpenCsdItmDecoder.h +++ b/tools/ctrace/src/decode/OpenCsdItmDecoder.h @@ -9,12 +9,23 @@ #define CTRACE_SRC_DECODE_OPENCSDITMDECODER_H #include "OpenCsdTraceElement.h" +#include "TraceRoute.h" #include #include #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 { @@ -62,16 +73,24 @@ class OpenCsdItmDecoderImpl; class OpenCsdItmDecoder { public: /** - * @brief Creates a decoder using the production OpenCSD session. + * @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(OpenCsdTraceElementSink& elementSink); + OpenCsdItmDecoder(std::vector routes, OpenCsdItmInputMode inputMode, + OpenCsdTraceElementSink& elementSink, + OpenCsdUnsupportedTraceIdObserver unsupportedTraceIdSink = {}); /** - * @brief Creates a decoder with an injected OpenCSD session factory. + * @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(OpenCsdTraceElementSink& elementSink, const OpenCsdItmSessionFactory& sessionFactory); + OpenCsdItmDecoder(std::vector routes, OpenCsdItmInputMode inputMode, + OpenCsdTraceElementSink& elementSink, const OpenCsdItmSessionFactory& sessionFactory); /** @brief Destroys the decoder implementation and external session. */ ~OpenCsdItmDecoder(); @@ -98,4 +117,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/OpenCsdItmSession.cpp b/tools/ctrace/src/decode/OpenCsdItmSession.cpp index 775ffb252..b32acfefa 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() +ocsd_datapath_resp_t OpenCsdItmSession::resetRoute(std::uint8_t channel, ocsd_trc_index_t index) { - return m_input->TraceDataIn(OCSD_OP_RESET, 0, 0, nullptr, nullptr); + return m_treeSession.resetDecoder(channel, index); } 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..460f97dc6 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 { @@ -40,41 +35,19 @@ class OpenCsdItmSessionInterface { std::uint32_t& processed) = 0; /** @brief Flushes pending OpenCSD decoder work. */ virtual ocsd_datapath_resp_t flush() = 0; - /** @brief Resets OpenCSD decoder state for stream recovery. */ - virtual ocsd_datapath_resp_t reset() = 0; + /** + * @brief Resets one transport route while preserving the formatted root frontend. + * + * Every session must state this behavior explicitly so a formatted implementation + * cannot silently fall back to resetting the complete tree. + */ + virtual ocsd_datapath_resp_t resetRoute(std::uint8_t channel, ocsd_trc_index_t index) = 0; /** @brief Signals the end of the current trace stream. */ 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 +57,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; @@ -116,27 +77,14 @@ class OpenCsdItmSession final : public OpenCsdItmSessionInterface { std::uint32_t& processed) override; /** @brief Flushes the external decoder. */ ocsd_datapath_resp_t flush() override; - /** @brief Resets the external decoder. */ - ocsd_datapath_resp_t reset() override; + /** @brief Resets the synthetic SINGLE route without resetting the tree root. */ + ocsd_datapath_resp_t resetRoute(std::uint8_t channel, ocsd_trc_index_t index) override; /** @brief Signals end of trace to the external decoder. */ 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/OpenCsdPacketCollector.cpp b/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp index d17f3a052..993543e36 100644 --- a/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp +++ b/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp @@ -8,45 +8,128 @@ #include "OpenCsdPacketCollector.h" #include "TraceEvent.h" -#include "TraceStreamId.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" #include "opencsd/ocsd_if_types.h" #include "opencsd/trc_gen_elem_types.h" +#include #include #include #include +#include #include +#include #include #include #include -OpenCsdPacketCollector::OpenCsdPacketCollector(OpenCsdTraceElementSink& elementSink) +OpenCsdPacketCollector::OpenCsdPacketCollector(TraceRouteIdentity route, OpenCsdTraceElementSink& elementSink) + : 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; + const auto duplicateRouteId = std::any_of(m_routesByChannel.begin(), m_routesByChannel.end(), + [&](const auto& item) { return item.second.id == route.id; }); + if (duplicateRouteId) { + throw std::invalid_argument("duplicate normalized route ID in formatted OpenCSD packet routes"); + } + 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; + m_nextTransactionOrder = 1U; m_transactionElements.clear(); } +std::optional OpenCsdPacketCollector::reserveTransactionOrder() noexcept +{ + if (!m_transactionActive) { + return std::nullopt; + } + return m_nextTransactionOrder++; +} + void OpenCsdPacketCollector::commitTransaction() { - for (auto& element : m_transactionElements) { - appendCommitted(std::move(element)); + for (auto& buffered : m_transactionElements) { + appendCommitted(std::move(buffered.element)); } m_transactionElements.clear(); m_transactionActive = false; } +void OpenCsdPacketCollector::commitTransactionForRouteFailures( + const std::map& sourceOffsetsByRoute) +{ + for (const auto& routeCutoff : sourceOffsetsByRoute) { + const auto routeId = routeCutoff.first; + const auto knownConfiguredRoute = (m_singleRoute.has_value() && m_singleRoute->id == routeId) || + std::any_of(m_routesByChannel.begin(), m_routesByChannel.end(), + [routeId](const auto& item) { return item.second.id == routeId; }); + if (!knownConfiguredRoute) { + throw std::invalid_argument("route-aware OpenCSD transaction cutoff references an unknown normalized route"); + } + } + + for (auto& buffered : m_transactionElements) { + auto& element = buffered.element; + const auto cutoff = sourceOffsetsByRoute.find(element.route.id); + const auto affected = cutoff != sourceOffsetsByRoute.end(); + const auto safeBeforeFailure = !affected || element.sourceIndex < cutoff->second; + const auto reportedDiagnostic = affected && buffered.reportedDiagnostic; + if (safeBeforeFailure || reportedDiagnostic) { + appendCommitted(std::move(element)); + } + } + m_transactionElements.clear(); + m_transactionActive = false; +} + +std::size_t OpenCsdPacketCollector::commitTransactionErrors(TraceIssueCode issueCode) +{ + std::vector retained; + for (auto& buffered : m_transactionElements) { + auto& element = buffered.element; + 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) { + for (auto& buffered : m_transactionElements) { + auto& element = buffered.element; const auto elementOffset = element.sourceIndex; if (elementOffset < sourceOffset && element.kind != OpenCsdTraceElement::Kind::Error) { appendCommitted(std::move(element)); @@ -77,10 +160,30 @@ std::size_t OpenCsdPacketCollector::transactionElementCount() const return m_transactionElements.size(); } +bool OpenCsdPacketCollector::transactionHasUnmatchedError( + const std::map& sourceOffsetsByRoute) const +{ + return std::any_of(m_transactionElements.begin(), m_transactionElements.end(), [&](const auto& buffered) { + const auto& element = buffered.element; + if (element.kind != OpenCsdTraceElement::Kind::Error || element.issueSeverity != TraceIssueSeverity::Error) { + return false; + } + if (element.issueCode == TraceIssueCode::OpenCsdIncompleteTail) { + return true; + } + if (element.issueCode == TraceIssueCode::DataLoss) { + return false; + } + const auto cutoff = sourceOffsetsByRoute.find(element.route.id); + return cutoff == sourceOffsetsByRoute.end() || element.sourceIndex < cutoff->second; + }); +} + std::optional OpenCsdPacketCollector::transactionFirstSourceOffset() const { std::optional firstOffset; - for (const auto& element : m_transactionElements) { + for (const auto& buffered : m_transactionElements) { + const auto& element = buffered.element; const auto offset = element.sourceIndex; if (!firstOffset.has_value() || offset < *firstOffset) { firstOffset = offset; @@ -89,10 +192,56 @@ std::optional OpenCsdPacketCollector::transactionFirstSourceOffse return firstOffset; } +std::optional +OpenCsdPacketCollector::transactionFirstSyncOffset(const TraceRouteIdentity& route, + std::optional beforeOffset) const +{ + const auto sync = std::find_if(m_transactionElements.begin(), m_transactionElements.end(), [&](const auto& buffered) { + return buffered.element.route == route && buffered.element.kind == OpenCsdTraceElement::Kind::Sync && + (!beforeOffset.has_value() || buffered.element.sourceIndex < *beforeOffset); + }); + return sync == m_transactionElements.end() ? std::nullopt : std::optional(sync->element.sourceIndex); +} + void OpenCsdPacketCollector::appendDecodeError(ocsd_trc_index_t index, const std::string& message, TraceIssueCode issueCode, bool discontinuity, TraceIssueSeverity severity) { + appendDecodeError(defaultRoute(), index, message, issueCode, discontinuity, severity); +} + +void OpenCsdPacketCollector::appendDecodeError(const TraceRouteIdentity& route, ocsd_trc_index_t index, + const std::string& message, TraceIssueCode issueCode, bool discontinuity, + TraceIssueSeverity severity) +{ + appendDecodeErrorImpl(route, index, message, issueCode, discontinuity, severity, std::nullopt, false); +} + +void OpenCsdPacketCollector::appendReportedDecodeError(ocsd_trc_index_t index, const std::string& message, + std::optional callbackOrder, + TraceIssueCode issueCode, bool discontinuity, + TraceIssueSeverity severity) +{ + appendReportedDecodeError(defaultRoute(), index, message, callbackOrder, issueCode, discontinuity, severity); +} + +void OpenCsdPacketCollector::appendReportedDecodeError(const TraceRouteIdentity& route, ocsd_trc_index_t index, + const std::string& message, + std::optional callbackOrder, + TraceIssueCode issueCode, bool discontinuity, + TraceIssueSeverity severity) +{ + appendDecodeErrorImpl(route, index, message, issueCode, discontinuity, severity, callbackOrder, true); +} + +void OpenCsdPacketCollector::appendDecodeErrorImpl(const TraceRouteIdentity& route, ocsd_trc_index_t index, + const std::string& message, TraceIssueCode issueCode, + bool discontinuity, TraceIssueSeverity severity, + std::optional callbackOrder, bool reportedDiagnostic) +{ + if (!containsRoute(route)) { + throw std::invalid_argument("OpenCSD diagnostic references an unknown normalized route"); + } OpenCsdTraceElement element; element.kind = OpenCsdTraceElement::Kind::Error; element.sourceIndex = static_cast(index); @@ -100,13 +249,28 @@ void OpenCsdPacketCollector::appendDecodeError(ocsd_trc_index_t index, const std element.issueCode = issueCode; element.issueSeverity = severity; element.errorMessage = message; - appendElement(std::move(element)); + element.route = route; + if (!m_transactionActive || !callbackOrder.has_value()) { + if (m_transactionActive) { + const auto order = reserveTransactionOrder().value(); + m_transactionElements.push_back(BufferedElement{std::move(element), order, reportedDiagnostic}); + } else { + appendCommitted(std::move(element)); + } + return; + } + + const auto position = + std::lower_bound(m_transactionElements.begin(), m_transactionElements.end(), *callbackOrder, + [](const auto& buffered, std::uint64_t order) { return buffered.callbackOrder < order; }); + m_transactionElements.insert(position, BufferedElement{std::move(element), *callbackOrder, reportedDiagnostic}); } 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); @@ -114,16 +278,18 @@ void OpenCsdPacketCollector::prependDiscontinuity(ocsd_trc_index_t index, const element.issueCode = issueCode; element.errorMessage = message; element.rawBytesConsumed = rawBytesConsumed; + element.route = route; if (m_transactionActive) { - m_transactionElements.insert(m_transactionElements.begin(), std::move(element)); + m_transactionElements.insert(m_transactionElements.begin(), BufferedElement{std::move(element), 0U, false}); 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); @@ -131,38 +297,88 @@ void OpenCsdPacketCollector::prependDataLossError(ocsd_trc_index_t index, const element.errorMessage = message; element.rawBytesConsumed = rawBytesConsumed; element.awaitingResumeTimestamp = true; + element.route = route; if (m_transactionActive) { - m_transactionElements.insert(m_transactionElements.begin(), std::move(element)); + m_transactionElements.insert(m_transactionElements.begin(), BufferedElement{std::move(element), 0U, false}); return; } - appendElement(std::move(element)); + appendElement(std::move(element), route); +} + +void OpenCsdPacketCollector::appendDataLossError(const TraceRouteIdentity& route, ocsd_trc_index_t index, + const std::string& message, std::uint64_t rawBytesConsumed) +{ + if (!containsRoute(route)) { + throw std::invalid_argument("OpenCSD data-loss interval references an unknown normalized route"); + } + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Error; + element.sourceIndex = static_cast(index); + element.issueCode = TraceIssueCode::DataLoss; + element.errorMessage = message; + element.rawBytesConsumed = rawBytesConsumed; + element.awaitingResumeTimestamp = true; + appendElement(std::move(element), route); +} + +bool OpenCsdPacketCollector::insertDataLossBeforeSync(const TraceRouteIdentity& route, ocsd_trc_index_t index, + const std::string& message, std::uint64_t rawBytesConsumed, + std::optional beforeOffset) +{ + if (!containsRoute(route)) { + throw std::invalid_argument("OpenCSD data-loss interval references an unknown normalized route"); + } + if (!m_transactionActive) { + return false; + } + const auto sync = std::find_if(m_transactionElements.begin(), m_transactionElements.end(), [&](const auto& buffered) { + return buffered.element.route == route && buffered.element.kind == OpenCsdTraceElement::Kind::Sync && + (!beforeOffset.has_value() || buffered.element.sourceIndex < *beforeOffset); + }); + if (sync == m_transactionElements.end()) { + return false; + } + + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Error; + element.sourceIndex = static_cast(index); + element.issueCode = TraceIssueCode::DataLoss; + element.errorMessage = message; + element.rawBytesConsumed = rawBytesConsumed; + element.awaitingResumeTimestamp = true; + element.route = route; + m_transactionElements.insert(sync, BufferedElement{std::move(element), sync->callbackOrder, false}); + return true; } 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 = callbackRouteForChannel(trc_chan_id); + if (route == nullptr) { + return OCSD_RESP_CONT; + } if (elem.getType() != OCSD_GEN_TRC_ELEM_ITMTRACE) { return OCSD_RESP_CONT; } 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, *route); break; case DWT_PAYLOAD: - appendDwt(index_sop, traceBusId, 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, traceBusId, elem); + appendTimestamp(index_sop, elem, *route); break; case TS_GLOBAL: - appendGlobalTimestamp(index_sop, traceBusId, elem); + appendGlobalTimestamp(index_sop, elem, *route); break; } } catch (...) { @@ -175,44 +391,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(); @@ -220,85 +417,171 @@ 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 channel == 0U ? route : nullptr; + } + const auto found = m_routesByChannel.find(channel); + return found != m_routesByChannel.end() ? &found->second : nullptr; +} + +const TraceRouteIdentity* OpenCsdPacketCollector::callbackRouteForChannel(std::uint8_t channel) const noexcept +{ + if (const auto* route = singleRoute()) { + return route; + } + return routeForChannel(channel); +} + +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, std::uint8_t traceBusId, - 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.traceBusId = traceBusId; 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, std::uint8_t traceBusId, - 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; 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; element.overflow = info.overflow; - appendElement(std::move(element)); + appendElement(std::move(element), route); } -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 TraceRouteIdentity& route) { 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; element.overflow = info.overflow; - appendElement(std::move(element)); + appendElement(std::move(element), route); } -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 TraceRouteIdentity& route) { 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; - appendElement(std::move(element)); + appendElement(std::move(element), route); } LocalTimestampRelation OpenCsdPacketCollector::timestampRelation(swt_itm_type type) @@ -315,10 +598,12 @@ 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 = route; if (m_transactionActive) { - m_transactionElements.push_back(std::move(element)); + const auto order = reserveTransactionOrder().value(); + m_transactionElements.push_back(BufferedElement{std::move(element), order, false}); return; } appendCommitted(std::move(element)); diff --git a/tools/ctrace/src/decode/OpenCsdPacketCollector.h b/tools/ctrace/src/decode/OpenCsdPacketCollector.h index d4e1b63e3..3e86bc5d8 100644 --- a/tools/ctrace/src/decode/OpenCsdPacketCollector.h +++ b/tools/ctrace/src/decode/OpenCsdPacketCollector.h @@ -8,8 +8,10 @@ #ifndef CTRACE_SRC_DECODE_OPENCSDPACKETCOLLECTOR_H #define CTRACE_SRC_DECODE_OPENCSDPACKETCOLLECTOR_H +#include "OpenCsdFormattedItmSession.h" #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" @@ -20,17 +22,29 @@ #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. + * @param route Normalized semantic route assigned to every collected element. * @param elementSink Sink receiving elements after transaction commit. */ - explicit OpenCsdPacketCollector(OpenCsdTraceElementSink& elementSink); + OpenCsdPacketCollector(TraceRouteIdentity route, OpenCsdTraceElementSink& elementSink); + /** + * @brief Creates a collector that routes formatted callbacks by Trace Bus ID. + * @param routes Normalized routes, each carrying one unique architectural Trace Bus ID. + * @param elementSink Sink receiving elements after transaction commit. + * @throws std::invalid_argument If the route catalogue is empty, invalid, or ambiguous. + */ + OpenCsdPacketCollector(std::vector routes, OpenCsdTraceElementSink& elementSink); /** * @brief Starts buffering elements for one recoverable decoder operation. @@ -39,8 +53,26 @@ class OpenCsdPacketCollector : public ITrcGenElemIn, public IPktRawDataMon reserveTransactionOrder() noexcept; /** @brief Commits all buffered elements. */ void commitTransaction(); + /** + * @brief Commits one operation while discarding unsafe elements from failing routes. + * @param sourceOffsetsByRoute First unsafe raw offset for every failing route. + * + * Elements from unaffected routes and non-error elements before their route's + * cutoff retain their original callback order. Error elements on a failing + * route are replaced by the structured OpenCSD diagnostics emitted by the + * recovery controller. + */ + void commitTransactionForRouteFailures(const std::map& sourceOffsetsByRoute); + /** + * @brief Commits only matching failing issues and discards every other buffered element. + * @param issueCode Issue code retained from the current transaction. + * @return Number of retained issues. + */ + std::size_t commitTransactionErrors(TraceIssueCode issueCode); /** * @brief Commits buffered elements before a raw source offset. * @param sourceOffset First raw offset that remains buffered. @@ -52,8 +84,24 @@ class OpenCsdPacketCollector : public ITrcGenElemIn, public IPktRawDataMon& sourceOffsetsByRoute) const; /** @brief Returns the first buffered raw offset, if present. */ std::optional transactionFirstSourceOffset() const; + /** + * @brief Returns the first buffered hardware-sync offset for one route. + * @param route Route whose synchronization is requested. + * @param beforeOffset Optional exclusive failure boundary. + */ + std::optional + transactionFirstSyncOffset(const TraceRouteIdentity& route, + std::optional beforeOffset = std::nullopt) const; /** * @brief Appends a decoder issue element. * @param index Raw source offset associated with the issue. @@ -65,6 +113,35 @@ class OpenCsdPacketCollector : public ITrcGenElemIn, public IPktRawDataMon callbackOrder, + TraceIssueCode issueCode = TraceIssueCode::OpenCsdDecodeError, + bool discontinuity = true, TraceIssueSeverity severity = TraceIssueSeverity::Error); + /** + * @brief Appends a routed OpenCSD logger diagnostic at its original callback position. + * @param route Route receiving the diagnostic. + * @param callbackOrder Position reserved when the logger callback occurred. + */ + void appendReportedDecodeError(const TraceRouteIdentity& route, ocsd_trc_index_t index, const std::string& message, + std::optional callbackOrder, + TraceIssueCode issueCode = TraceIssueCode::OpenCsdDecodeError, + bool discontinuity = true, TraceIssueSeverity severity = TraceIssueSeverity::Error); /** * @brief Prepends a discontinuity before buffered resumed events. * @param index Raw source offset at which decoding resumes. @@ -81,38 +158,102 @@ class OpenCsdPacketCollector : public ITrcGenElemIn, public IPktRawDataMon beforeOffset = std::nullopt); + /** + * @brief Resolves an OpenCSD transport channel to its exact normalized route. + * + * SINGLE exposes only its synthetic channel 0. Formatted input exposes only + * configured architectural Trace Bus IDs. + */ + const TraceRouteIdentity* routeForChannel(std::uint8_t channel) const noexcept; /** @brief Receives one generic element callback from OpenCSD. */ ocsd_datapath_resp_t TraceElemIn(ocsd_trc_index_t index_sop, std::uint8_t trc_chan_id, const OcsdTraceElement& elem) override; /** @brief Receives one raw ITM packet callback from OpenCSD. */ void RawPacketDataMon(ocsd_datapath_op_t op, ocsd_trc_index_t index_sop, const ItmTrcPacket* pkt, std::uint32_t size, const std::uint8_t* data) override; + /** + * @brief Receives a raw ITM packet from a decoder adapter bound to one route. + * @param route Exact normalized route bound to the decoder callback. + * @param op OpenCSD data-path operation. + * @param index_sop Raw input offset at the start of the packet. + * @param pkt Decoded ITM packet, or null for an operation-only callback. + * @param size Number of raw packet bytes. + * @param data Raw packet bytes. + */ + void rawPacketForRoute(const TraceRouteIdentity& route, ocsd_datapath_op_t op, ocsd_trc_index_t index_sop, + const ItmTrcPacket* pkt, std::uint32_t size, const std::uint8_t* data) override; private: + /** @brief Returns the SINGLE route, or null for a formatted collector. */ + const TraceRouteIdentity* singleRoute() const noexcept; + /** @brief Returns the deterministic route for an input-wide diagnostic. */ + const TraceRouteIdentity& defaultRoute() const noexcept; + /** @brief Resolves a generic callback while retaining the fixed SINGLE binding. */ + const TraceRouteIdentity* callbackRouteForChannel(std::uint8_t channel) const noexcept; + /** @brief Tests whether an explicit packet route belongs to this collector. */ + bool containsRoute(const TraceRouteIdentity& route) const noexcept; + /** @brief Converts one raw packet callback after its route has been resolved. */ + void appendRawPacket(const TraceRouteIdentity& route, ocsd_datapath_op_t op, ocsd_trc_index_t index_sop, + const ItmTrcPacket* pkt); /** @brief Appends a hardware synchronization element. */ - void appendSync(ocsd_trc_index_t index); + void appendSync(ocsd_trc_index_t index, const TraceRouteIdentity& route); /** @brief Appends a hardware overflow element. */ - void appendOverflow(ocsd_trc_index_t index); + void appendOverflow(ocsd_trc_index_t index, const TraceRouteIdentity& route); /** @brief Converts an OpenCSD global timestamp callback. */ - void appendGlobalTimestamp(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem); + void appendGlobalTimestamp(ocsd_trc_index_t index, const OcsdTraceElement& elem, const TraceRouteIdentity& route); /** @brief Converts an OpenCSD error packet callback. */ - void appendError(ocsd_trc_index_t index, const ItmTrcPacket& pkt); + void appendError(ocsd_trc_index_t index, const ItmTrcPacket& pkt, const TraceRouteIdentity& route); /** @brief Converts an ITM software packet callback. */ - void appendSoftware(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem); + void appendSoftware(ocsd_trc_index_t index, const OcsdTraceElement& elem, const TraceRouteIdentity& route); /** @brief Converts a DWT hardware packet callback. */ - void appendDwt(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem); + void appendDwt(ocsd_trc_index_t index, const OcsdTraceElement& elem, const TraceRouteIdentity& route); /** @brief Converts an OpenCSD local timestamp callback. */ - void appendTimestamp(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem); + void appendTimestamp(ocsd_trc_index_t index, const OcsdTraceElement& elem, const TraceRouteIdentity& route); /** @brief Maps the OpenCSD timestamp type to its semantic relation. */ static LocalTimestampRelation timestampRelation(swt_itm_type type); /** @brief Buffers or commits one element according to transaction state. */ - void appendElement(OpenCsdTraceElement element); + void appendElement(OpenCsdTraceElement element, const TraceRouteIdentity& route); + /** @brief Builds and appends one ordinary or logger-reported diagnostic. */ + void appendDecodeErrorImpl(const TraceRouteIdentity& route, ocsd_trc_index_t index, const std::string& message, + TraceIssueCode issueCode, bool discontinuity, TraceIssueSeverity severity, + std::optional callbackOrder, bool reportedDiagnostic); /** @brief Emits one committed element while deferring sink exceptions. */ void appendCommitted(OpenCsdTraceElement element); + /** @brief Adds transaction-only ordering metadata without exposing it downstream. */ + struct BufferedElement { + OpenCsdTraceElement element; + std::uint64_t callbackOrder = 0U; + bool reportedDiagnostic = false; + }; + + std::optional m_singleRoute; + std::map m_routesByChannel; OpenCsdTraceElementSink& m_elementSink; bool m_transactionActive = false; - std::vector m_transactionElements; + std::uint64_t m_nextTransactionOrder = 1U; + std::vector m_transactionElements; std::exception_ptr m_outputError; }; 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/decode/OpenCsdTreeSession.cpp b/tools/ctrace/src/decode/OpenCsdTreeSession.cpp new file mode 100644 index 000000000..615a9096c --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdTreeSession.cpp @@ -0,0 +1,203 @@ +/* + * 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_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_raw_in_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" + +#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); }, + }; +} + +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()) +{ +} + +OpenCsdTreeSession::OpenCsdTreeSession(ocsd_dcd_tree_src_t sourceType, std::uint32_t formatterFlags, + ITraceErrorLog& errorLogger, ITrcGenElemIn& elementOutput, + const TreeLifecycle& lifecycle) + : m_sourceType(validateSourceType(sourceType)), + 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(m_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) +{ + 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(); + 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"); +} + +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"); +} + +ocsd_datapath_resp_t OpenCsdTreeSession::resetDecoder(std::uint8_t channel, ocsd_trc_index_t index) +{ + validateChannel(channel); + 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 decoder component is not initialized"); + ITrcDataIn* packetProcessor = nullptr; + OpenCsdSessionValidation::requireSuccess(manager->getDataInputI(component, &packetProcessor), + "failed to resolve OpenCSD decoder input"); + OpenCsdSessionValidation::requireObject(packetProcessor, "OpenCSD decoder input is not initialized"); + return packetProcessor->TraceDataIn(OCSD_OP_RESET, index, 0U, nullptr, nullptr); +} + +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) +{ + 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..17cab01e3 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdTreeSession.h @@ -0,0 +1,154 @@ +/* + * 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 ITrcRawFrameIn; +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 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 Resets one decoder pair without resetting the DecodeTree frontend. + * @param channel OpenCSD transport channel used to resolve the decoder element. + * @param index Raw input offset associated with the recovery boundary. + * @return Data-path response from the route's packet processor. + * @throws OpenCsdTreeSessionError If the route cannot be resolved or reset. + */ + ocsd_datapath_resp_t resetDecoder(std::uint8_t channel, ocsd_trc_index_t index); + /** @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(); + /** @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; +}; + +#endif // CTRACE_SRC_DECODE_OPENCSDTREESESSION_H diff --git a/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp b/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp index bc01c8182..e7ec2c110 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) @@ -36,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: @@ -76,40 +89,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/CoreSightFormatter.h b/tools/ctrace/src/model/CoreSightFormatter.h new file mode 100644 index 000000000..15466cd65 --- /dev/null +++ b/tools/ctrace/src/model/CoreSightFormatter.h @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_MODEL_CORESIGHTFORMATTER_H +#define CTRACE_SRC_MODEL_CORESIGHTFORMATTER_H + +#include + +/** @brief Defines the raw-memory framing required by the CoreSight formatter frontend. */ +namespace CoreSightFormatter { + +inline constexpr std::uint32_t kMemoryAlignedFrameSize = 16U; + +} // namespace CoreSightFormatter + +#endif // CTRACE_SRC_MODEL_CORESIGHTFORMATTER_H diff --git a/tools/ctrace/src/model/TraceEvent.h b/tools/ctrace/src/model/TraceEvent.h index a4c86a040..71bd93f41 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 @@ -51,6 +53,7 @@ enum class TraceIssueCode { OpenCsdNoProgress, OpenCsdWaitTimeout, OpenCsdInitializationError, + OpenCsdFormattedInputError, }; /** @brief Contains one decoded ITM software stimulus event. */ @@ -269,8 +272,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..abea7f9be 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 @@ -58,7 +60,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,14 +69,33 @@ 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()); } 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) @@ -87,177 +108,213 @@ 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; - 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); - if (inserted) { - continue; - } - const auto& first = *found->second; - const auto sameMetadata = first.label == source.label && first.address == source.address && - first.dataType == source.dataType && first.dataSize == source.dataSize && - first.addressError == source.addressError && - first.dataTypeError == source.dataTypeError && - first.dataSizeError == source.dataSizeError; - const auto indistinguishableProcessors = - first.traceBusId == source.traceBusId && first.processorName != source.processorName; - if ((sameMetadata && !indistinguishableProcessors) || !reported.insert(key).second) { - continue; - } + std::map, const CtraceRunSourceMeta*> sources; + std::set> reported; + for (const auto& route : ctraceRunMeta.routes()) { + for (const auto& source : route.sources) { + if (!routeMatchesSelection(source, selection)) { + continue; + } + const auto key = std::make_tuple(source.route.id, source.type, source.source); + const auto [found, inserted] = sources.emplace(key, &source); + if (inserted) { + continue; + } + const auto& first = *found->second; + const auto sameMetadata = first.label == source.label && first.address == source.address && + first.dataType == source.dataType && first.dataSize == source.dataSize && + first.addressError == source.addressError && + first.dataTypeError == source.dataTypeError && + first.dataSizeError == source.dataSizeError; + const auto sameBinding = first.route == source.route && first.processorName == source.processorName; + if ((sameMetadata && sameBinding) || !reported.insert(key).second) { + continue; + } - valid = false; - auto context = routeContext("ctf", ctraceRunMeta, source); - 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)); - reportRequirementError( - diagnostics, - "CTF metadata cannot describe conflicting active type/source routes from different processors or Trace Bus IDs", - std::move(context)); + valid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("type", source.type); + context.emplace_back("firstProcessor", first.processorName.value_or("")); + context.emplace_back("otherProcessor", source.processorName.value_or("")); + 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; + std::vector routes; + for (const auto& route : ctraceRunMeta.routes()) { + if (selection.includesRoute(route.identity)) { + routes.push_back(&route); } - 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; - } - 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}, - }); + const auto routes = selectedCtfRoutes(ctraceRunMeta, selection); + if (routes.empty()) { + return CtfMetadataTopology{}; + } + + const auto legacy = ctraceRunMeta.routes().size() == 1U && + !ctraceRunMeta.routes().front().identity.traceBusId.has_value() && + ctraceRunMeta.traceFormat() != TraceRunFormat::Formatted; + + 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)); + } } - if (!ctraceRunMeta.timestampClockErrors().empty()) { + if (!valid) { return std::nullopt; } - 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; - } + + 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, routes.front()->processorName, CtfClockDomainId{0U}}); + return topology; + } + + 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, route->processorName, domainId}); + } + return std::optional{std::move(topology)}; +} + +/** @brief Reports one deferred trace-run field error for a selected DWT source. */ +static bool reportCtfDwtFieldError(const CtraceRunMeta& ctraceRunMeta, const CtraceRunSourceMeta& source, + const std::optional& error, const char* message, + DiagnosticSink& diagnostics) +{ + if (!error.has_value()) { + return false; + } + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("error", *error); + reportRequirementError(diagnostics, message, std::move(context)); + return true; +} + +/** @brief Reports every deferred trace-run metadata error for one DWT source. */ +static bool validateCtfDwtParsedMetadata(const CtraceRunMeta& ctraceRunMeta, const CtraceRunSourceMeta& source, + DiagnosticSink& diagnostics) +{ + bool valid = true; + if (reportCtfDwtFieldError(ctraceRunMeta, source, source.addressError, + "CTF output cannot use the configured ctrace-run address", diagnostics)) { + valid = false; + } + if (reportCtfDwtFieldError(ctraceRunMeta, source, source.dataTypeError, + "CTF output cannot use the configured ctrace-run data-type", diagnostics)) { + valid = false; + } + if (reportCtfDwtFieldError(ctraceRunMeta, source, source.dataSizeError, + "CTF output cannot use the configured ctrace-run size", diagnostics)) { + valid = false; + } + return valid; +} + +/** @brief Validates the resolved comparator, type, and size of one DWT source. */ +static bool validateCtfDwtShape(const CtraceRunMeta& ctraceRunMeta, const CtraceRunSourceMeta& source, + DiagnosticSink& diagnostics) +{ + bool valid = true; + if (source.source > 3U) { + valid = false; + reportRequirementError(diagnostics, "CTF output requires DWT comparator sources between 0 and 3", + routeContext("ctf", ctraceRunMeta, source)); + } + + const auto validType = TraceRunSchema::isDwtDataType(source.dataType); + const auto* valueVariant = CtfSchema::valueVariantForTraceRunType(source.dataType, source.dataSize); + if (!validType) { + valid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("dataType", source.dataType); reportRequirementError(diagnostics, - "CTF output requires timestamps.clock from an active ctrace-setup; no default is assumed", - { - {"backend", "ctf"}, - {"config", ctraceRunMeta.configPath()}, - }); - return std::nullopt; + "CTF output cannot use ctrace-run data-type '" + source.dataType + "'; " + + std::string(CtfSchema::ValueTypeRequirements), + std::move(context)); } - if (*ctraceRunMeta.timestampClockHz() == 0U) { + if (!TraceRunSchema::isDwtDataSize(source.dataSize) || (validType && valueVariant == nullptr)) { + valid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("dataType", source.dataType); + context.emplace_back("dataSize", std::to_string(source.dataSize)); reportRequirementError(diagnostics, - "CTF output requires timestamps.clock to be greater than zero", - { - {"backend", "ctf"}, - {"config", ctraceRunMeta.configPath()}, - }); - return std::nullopt; + "CTF output cannot use ctrace-run size " + std::to_string(source.dataSize) + + " with data-type '" + source.dataType + "'; " + + std::string(CtfSchema::ValueTypeRequirements), + std::move(context)); } - return ctraceRunMeta.timestampClockHz(); + return valid; } -/** @brief Resolves and validates the clock used by a CTF output. */ -static std::optional resolveCtfClock(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, - DiagnosticSink& diagnostics) +/** @brief Validates the resolved address range of one DWT source. */ +static bool validateCtfDwtAddressRange(const CtraceRunMeta& ctraceRunMeta, const CtraceRunSourceMeta& source, + DiagnosticSink& diagnostics) { - const auto selected = resolveSelectedCtfClock(ctraceRunMeta, selection, diagnostics); - if (!selected.valid) { - return std::nullopt; + const auto* valueVariant = CtfSchema::valueVariantForTraceRunType(source.dataType, source.dataSize); + if (!source.address.has_value() || valueVariant == nullptr) { + return true; } - if (selected.hasRoutes) { - return selected.clockHz; + const auto extent = source.dataSize - 1U; + if (*source.address <= std::numeric_limits::max() - extent) { + return true; } - return resolveDefaultCtfClock(ctraceRunMeta, selection, diagnostics); + + 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 false; +} + +/** @brief Validates all CTF requirements for one selected DWT source. */ +static bool validateCtfDwtSource(const CtraceRunMeta& ctraceRunMeta, const CtraceRunSourceMeta& source, + DiagnosticSink& diagnostics) +{ + if (!validateCtfDwtParsedMetadata(ctraceRunMeta, source, diagnostics)) { + return false; + } + const auto shapeValid = validateCtfDwtShape(ctraceRunMeta, source, diagnostics); + const auto addressValid = validateCtfDwtAddressRange(ctraceRunMeta, source, diagnostics); + return shapeValid && addressValid; } /** @brief Validates address, data type, and size metadata for selected DWT routes. */ @@ -265,90 +322,57 @@ static bool validateCtfDwtMetadata(const CtraceRunMeta& ctraceRunMeta, const Tra DiagnosticSink& diagnostics) { bool valid = true; - for (const auto& source : ctraceRunMeta.sources()) { - if (source.type != "dwt" || !routeMatchesSelection(source, selection)) { - continue; - } - bool sourceValid = true; - if (source.addressError.has_value()) { - valid = false; - sourceValid = false; - auto context = routeContext("ctf", ctraceRunMeta, source); - context.emplace_back("error", *source.addressError); - reportRequirementError(diagnostics, "CTF output cannot use the configured ctrace-run address", - std::move(context)); - } - if (source.dataTypeError.has_value()) { - valid = false; - sourceValid = false; - auto context = routeContext("ctf", ctraceRunMeta, source); - context.emplace_back("error", *source.dataTypeError); - reportRequirementError(diagnostics, "CTF output cannot use the configured ctrace-run data-type", - std::move(context)); - } - if (source.dataSizeError.has_value()) { - valid = false; - sourceValid = false; - auto context = routeContext("ctf", ctraceRunMeta, source); - context.emplace_back("error", *source.dataSizeError); - reportRequirementError(diagnostics, "CTF output cannot use the configured ctrace-run size", - std::move(context)); - } - if (!sourceValid) { - continue; - } - const auto validType = TraceRunSchema::isDwtDataType(source.dataType); - const auto* valueVariant = CtfSchema::valueVariantForTraceRunType(source.dataType, source.dataSize); - if (!validType) { - valid = false; - auto context = routeContext("ctf", ctraceRunMeta, source); - context.emplace_back("dataType", source.dataType); - reportRequirementError(diagnostics, - "CTF output cannot use ctrace-run data-type '" + source.dataType + "'; " + - std::string(CtfSchema::ValueTypeRequirements), - std::move(context)); - } - if (!TraceRunSchema::isDwtDataSize(source.dataSize) || (validType && valueVariant == nullptr)) { - valid = false; - auto context = routeContext("ctf", ctraceRunMeta, source); - context.emplace_back("dataType", source.dataType); - context.emplace_back("dataSize", std::to_string(source.dataSize)); - reportRequirementError(diagnostics, - "CTF output cannot use ctrace-run size " + std::to_string(source.dataSize) + - " with data-type '" + source.dataType + "'; " + - std::string(CtfSchema::ValueTypeRequirements), - std::move(context)); + for (const auto& route : ctraceRunMeta.routes()) { + for (const auto& source : route.sources) { + if (source.type != "dwt" || !routeMatchesSelection(source, selection)) { + continue; + } + if (!validateCtfDwtSource(ctraceRunMeta, source, diagnostics)) { + valid = false; + } } } 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; - 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) { - continue; - } + std::set> resolvedKeys; + std::vector sources; + for (const auto& route : ctraceRunMeta.routes()) { + for (const auto& source : route.sources) { + if ((source.type != "itm" && source.type != "dwt") || (source.type == "itm" && source.source == 0U) || + !routeMatchesSelection(source, selection) || + !resolvedKeys.emplace(source.type, source.source, source.route.id).second) { + continue; + } - sources.push_back({ - route.type, - route.source, - route.traceBusId, - route.label, - route.address, - route.dataType, - static_cast(route.dataSize), - }); + sources.push_back({ + source.type, + source.source, + source.route, + source.label, + source.address, + source.dataType, + static_cast(source.dataSize), + }); + } } 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; @@ -377,16 +401,13 @@ 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), + paths.ctf, paths.traceCompassXml, request.selection, std::move(*metadata), resolveCtfRoutes(ctraceRunMeta), }; } } diff --git a/tools/ctrace/src/output/TraceOutput.h b/tools/ctrace/src/output/TraceOutput.h index 294979180..4ced6a6ee 100644 --- a/tools/ctrace/src/output/TraceOutput.h +++ b/tools/ctrace/src/output/TraceOutput.h @@ -42,16 +42,93 @@ class TraceOutput { } /** @brief Prepares a new final output target before the first event. */ - virtual void start() {} + void start(); /** @brief Flushes and completes the active output target after the last event. */ - virtual void stop() {} + void stop(); /** @brief Discards an incomplete active output without committing partial data. */ - virtual void abort() = 0; + void abort(); /** * @brief Writes one event synchronously in decode order. * @param event Decoded event whose lifetime extends through this call. */ - virtual void writeEvent(const TraceEvent& event) = 0; + void writeEvent(const TraceEvent& event); + +protected: + /** @brief Validates and prepares targets before this output owns incomplete artifacts. */ + virtual void prepareOutput() = 0; + /** @brief Opens backend resources after the output becomes active. */ + virtual void startOutput() = 0; + /** @brief Flushes and commits backend resources while the output remains active. */ + virtual void stopOutput() = 0; + /** @brief Releases backend resources and removes incomplete artifacts. */ + virtual void abortOutput() = 0; + /** @brief Writes one event to an active backend. */ + virtual void writeOutput(const TraceEvent& event) = 0; + + /** @brief Aborts an active output while suppressing every cleanup exception. */ + void abortNoexcept() noexcept; + +private: + bool m_active = false; }; +inline void TraceOutput::start() +{ + abort(); + prepareOutput(); + m_active = true; + try { + startOutput(); + } catch (...) { + abort(); + throw; + } +} + +inline void TraceOutput::stop() +{ + if (!m_active) { + return; + } + try { + stopOutput(); + } catch (...) { + abort(); + throw; + } + m_active = false; +} + +inline void TraceOutput::abort() +{ + if (!m_active) { + return; + } + abortOutput(); + m_active = false; +} + +inline void TraceOutput::writeEvent(const TraceEvent& event) +{ + if (!m_active) { + return; + } + try { + writeOutput(event); + } catch (...) { + abort(); + throw; + } +} + +inline void TraceOutput::abortNoexcept() noexcept +{ + try { + abort(); + } catch (...) { + // Destruction cannot report cleanup failures safely. + (void)0; + } +} + #endif // CTRACE_SRC_OUTPUT_TRACEOUTPUT_H diff --git a/tools/ctrace/src/output/TraceOutputConfig.h b/tools/ctrace/src/output/TraceOutputConfig.h index da0c9e60c..62e6a679b 100644 --- a/tools/ctrace/src/output/TraceOutputConfig.h +++ b/tools/ctrace/src/output/TraceOutputConfig.h @@ -8,6 +8,8 @@ #ifndef CTRACE_SRC_OUTPUT_TRACEOUTPUTCONFIG_H #define CTRACE_SRC_OUTPUT_TRACEOUTPUTCONFIG_H +#include "ctf/CtfMetadataModel.h" +#include "TraceRoute.h" #include "TraceSelection.h" #include @@ -24,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; - std::uint8_t traceBusId = 0U; - 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; @@ -45,20 +36,20 @@ 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) + TraceSelection selection, CtfMetadataTopology metadata, std::vector routes = {}) : 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)) { } std::filesystem::path outputDirectory; std::filesystem::path traceCompassXmlPath; - std::uint64_t coreClockHz = 0; TraceSelection selection; - std::vector sources; + CtfMetadataTopology metadata; + std::vector routes; }; #endif // CTRACE_SRC_OUTPUT_TRACEOUTPUTCONFIG_H diff --git a/tools/ctrace/src/output/csv/CsvFileOutput.cpp b/tools/ctrace/src/output/csv/CsvFileOutput.cpp index e2a409bda..3679197f7 100644 --- a/tools/ctrace/src/output/csv/CsvFileOutput.cpp +++ b/tools/ctrace/src/output/csv/CsvFileOutput.cpp @@ -97,11 +97,7 @@ CsvFileOutput::CsvFileOutput(std::filesystem::path outputFile, TraceSelection se CsvFileOutput::~CsvFileOutput() { - try { - CsvFileOutput::abort(); - } catch (...) { - (void)0; - } + abortNoexcept(); } std::string_view CsvFileOutput::backendName() const noexcept @@ -114,27 +110,27 @@ std::string CsvFileOutput::targetPath() const return m_outputFile.string(); } -void CsvFileOutput::start() +void CsvFileOutput::prepareOutput() { - abort(); const auto& outputPath = m_outputFile; removeExistingCsv(outputPath); createParentDirectory(outputPath); - m_active = true; - m_stream = m_streamFactory(outputPath); +} + +void CsvFileOutput::startOutput() +{ + m_stream = m_streamFactory(m_outputFile); if (m_stream == nullptr || !m_stream->output()) { - abort(); - throw std::runtime_error("Failed to open CSV output " + outputPath.string()); + throw std::runtime_error("Failed to open CSV output " + m_outputFile.string()); } m_stream->output() << CsvRowMapper::header() << "\n"; if (!m_stream->output()) { - abort(); throw std::runtime_error("Failed to write CSV output " + m_outputFile.string()); } } -void CsvFileOutput::stop() +void CsvFileOutput::stopOutput() { if (m_stream != nullptr) { m_stream->close(); @@ -142,26 +138,18 @@ void CsvFileOutput::stop() const auto failed = m_stream != nullptr && !m_stream->output(); m_stream.reset(); if (failed) { - abort(); throw std::runtime_error("Failed to write CSV output " + m_outputFile.string()); } - m_active = false; } -void CsvFileOutput::abort() +void CsvFileOutput::abortOutput() { m_stream.reset(); - if (m_active) { - removeExistingCsv(m_outputFile); - m_active = false; - } + removeExistingCsv(m_outputFile); } -void CsvFileOutput::writeEvent(const TraceEvent& event) +void CsvFileOutput::writeOutput(const TraceEvent& event) { - if (m_stream == nullptr) { - return; - } if (!traceEventSelectedForOutput(event, m_selection)) { return; } diff --git a/tools/ctrace/src/output/csv/CsvFileOutput.h b/tools/ctrace/src/output/csv/CsvFileOutput.h index 570714510..7e7daceed 100644 --- a/tools/ctrace/src/output/csv/CsvFileOutput.h +++ b/tools/ctrace/src/output/csv/CsvFileOutput.h @@ -57,28 +57,31 @@ class CsvFileOutput final : public TraceOutput { /** @brief Closes an active stream without throwing. */ ~CsvFileOutput() override; - /** @brief Creates the target file and writes its header. */ - void start() override; + /** @brief Returns the CSV backend name. */ + std::string_view backendName() const noexcept override; + /** @brief Returns the CSV target file path. */ + std::string targetPath() const override; + +protected: + /** @brief Validates and prepares the CSV target path. */ + void prepareOutput() override; + /** @brief Opens the target file and writes its CSV header. */ + void startOutput() override; /** @brief Flushes and closes the completed CSV file. */ - void stop() override; + void stopOutput() override; /** @brief Closes and removes an incomplete CSV file. */ - void abort() override; + void abortOutput() override; /** * @brief Writes one selected event as a CSV row. * @param event Event evaluated against the configured selection. */ - void writeEvent(const TraceEvent& event) override; - /** @brief Returns the CSV backend name. */ - std::string_view backendName() const noexcept override; - /** @brief Returns the CSV target file path. */ - std::string targetPath() const override; + void writeOutput(const TraceEvent& event) override; private: std::filesystem::path m_outputFile; TraceSelection m_selection; StreamFactory m_streamFactory; std::unique_ptr m_stream; - bool m_active = false; }; #endif // CTRACE_SRC_OUTPUT_CSV_CSVFILEOUTPUT_H diff --git a/tools/ctrace/src/output/csv/CsvRowMapper.cpp b/tools/ctrace/src/output/csv/CsvRowMapper.cpp index 8a97fb930..01e4b5df1 100644 --- a/tools/ctrace/src/output/csv/CsvRowMapper.cpp +++ b/tools/ctrace/src/output/csv/CsvRowMapper.cpp @@ -18,6 +18,7 @@ #include #include #include +#include /** @brief Identifies columns in the stable ctrace CSV schema. */ enum class CsvColumn : std::size_t { @@ -132,20 +133,95 @@ static std::string_view exceptionActionCsvValue(ExceptionAction action) return "0x0"; } +/** @brief Writes one ITM software packet to the CSV event columns. */ +static void writePayloadColumns(CsvRow& row, const SoftwareTraceEvent& event) +{ + row[column(CsvColumn::Source)] = std::to_string(event.channel); + row[column(CsvColumn::Value)] = hexValue(event.value, event.size); +} + +/** @brief Writes one DWT data packet to the CSV event columns. */ +static void writePayloadColumns(CsvRow& row, const DwtDataTraceEvent& event) +{ + row[column(CsvColumn::Source)] = std::to_string(event.comparator); + row[column(CsvColumn::Value)] = hexValue(event.value, event.size); + writeDwtAddressFragment(row, CsvColumn::Pc, event.pc); + writeDwtAddressFragment(row, CsvColumn::Address, event.address); +} + +/** @brief Writes one DWT address packet to the CSV event columns. */ +static void writePayloadColumns(CsvRow& row, const DwtAddressTraceEvent& event) +{ + row[column(CsvColumn::Source)] = std::to_string(event.comparator); + writeDwtAddressFragment(row, CsvColumn::Pc, dwtAddressPc(event)); + writeDwtAddressFragment(row, CsvColumn::Address, dwtDataAddress(event)); +} + +/** @brief Writes one comparator-only DWT match to the CSV event columns. */ +static void writePayloadColumns(CsvRow& row, const DwtMatchTraceEvent& event) +{ + row[column(CsvColumn::Source)] = std::to_string(event.comparator); +} + +/** @brief Writes one exception transition to the CSV event columns. */ +static void writePayloadColumns(CsvRow& row, const ExceptionTraceEvent& event) +{ + row[column(CsvColumn::Source)] = std::to_string(event.number); + row[column(CsvColumn::Value)] = exceptionActionCsvValue(event.action); +} + /** @brief Writes one DWT event-counter packet to the CSV event columns. */ -static void writeDwtEvent(CsvRow& row, const DwtEventTraceEvent& event) +static void writePayloadColumns(CsvRow& row, const DwtEventTraceEvent& event) { row[column(CsvColumn::Source)] = "0"; row[column(CsvColumn::Value)] = hexValue(event.counterMask, 1U); } /** @brief Writes one PMU trace-on-overflow packet to the CSV event columns. */ -static void writePmuEvent(CsvRow& row, const PmuTraceEvent& event) +static void writePayloadColumns(CsvRow& row, const PmuTraceEvent& event) { row[column(CsvColumn::Source)] = "3"; row[column(CsvColumn::Value)] = hexValue(event.overflowMask, 1U); } +/** @brief Writes one periodic PC sample to the CSV event columns. */ +static void writePayloadColumns(CsvRow& row, const PcSampleTraceEvent& event) +{ + if (!event.sleeping) { + row[column(CsvColumn::Pc)] = hexValue(event.pc, 4U); + } +} + +/** @brief Leaves local timestamp control packets without payload-specific CSV columns. */ +static void writePayloadColumns(CsvRow&, const LocalTimestampTraceEvent&) +{ +} + +/** @brief Writes one global timestamp to the CSV cycle column. */ +static void writePayloadColumns(CsvRow& row, const GlobalTimestampTraceEvent& event) +{ + row[column(CsvColumn::Cycles)] = std::to_string(event.value); +} + +/** @brief Writes one overflow diagnostic to the CSV note column. */ +static void writePayloadColumns(CsvRow& row, const OverflowTraceEvent& event) +{ + row[column(CsvColumn::Note)] = event.message.empty() + ? "overflow: new timestamp segment; time across boundary may be unreliable" + : event.message; +} + +/** @brief Leaves synchronization control packets without payload-specific CSV columns. */ +static void writePayloadColumns(CsvRow&, const SyncTraceEvent&) +{ +} + +/** @brief Writes one retained decoder issue to the CSV note column. */ +static void writePayloadColumns(CsvRow& row, const TraceIssueEvent& event) +{ + row[column(CsvColumn::Note)] = event.message; +} + /** @brief Maps one semantic trace event to all CSV columns. */ static CsvRow eventToCsvRow(const TraceEvent& event) { @@ -153,52 +229,18 @@ 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); } - if (const auto* software = traceEventPayload(event)) { - row[column(CsvColumn::Source)] = std::to_string(software->channel); - row[column(CsvColumn::Value)] = hexValue(software->value, software->size); - } else if (const auto* data = traceEventPayload(event)) { - row[column(CsvColumn::Source)] = std::to_string(data->comparator); - row[column(CsvColumn::Value)] = hexValue(data->value, data->size); - writeDwtAddressFragment(row, CsvColumn::Pc, data->pc); - writeDwtAddressFragment(row, CsvColumn::Address, data->address); - } else if (const auto* address = traceEventPayload(event)) { - row[column(CsvColumn::Source)] = std::to_string(address->comparator); - writeDwtAddressFragment(row, CsvColumn::Pc, dwtAddressPc(*address)); - writeDwtAddressFragment(row, CsvColumn::Address, dwtDataAddress(*address)); - } else if (const auto* match = traceEventPayload(event)) { - row[column(CsvColumn::Source)] = std::to_string(match->comparator); - } else if (const auto* exception = traceEventPayload(event)) { - row[column(CsvColumn::Source)] = std::to_string(exception->number); - row[column(CsvColumn::Value)] = exceptionActionCsvValue(exception->action); - } else if (const auto* counter = traceEventPayload(event)) { - writeDwtEvent(row, *counter); - } else if (const auto* counter = traceEventPayload(event)) { - writePmuEvent(row, *counter); - } else if (const auto* sample = traceEventPayload(event)) { - if (!sample->sleeping) { - row[column(CsvColumn::Pc)] = hexValue(sample->pc, 4); - } - } else if (const auto* timestamp = traceEventPayload(event)) { - row[column(CsvColumn::Cycles)] = std::to_string(timestamp->value); - } else if (const auto* overflow = traceEventPayload(event)) { - row[column(CsvColumn::Note)] = overflow->message.empty() - ? "overflow: new timestamp segment; time across boundary may be unreliable" - : overflow->message; - } else if (const auto* issue = traceEventPayload(event)) { - row[column(CsvColumn::Note)] = issue->message; - } + std::visit([&row](const auto& payload) { writePayloadColumns(row, payload); }, event.payload); return row; } - std::string CsvRowMapper::header() { return joinColumns(kCsvColumnNames); diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp index 46f8e2548..1929b34d9 100644 --- a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp @@ -8,18 +8,23 @@ #include "CtfBundleOutput.h" #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 #include #include #include +#include /** @brief Rejects empty and root-like output targets. */ static void requireOutputTarget(const std::filesystem::path& path, const char* description) @@ -69,6 +74,22 @@ static bool isAncestorPath(const std::filesystem::path& candidate, const std::fi return true; } +/** @brief Rejects an output whose nearest existing parent is not a directory. */ +static void validateOutputParent(const std::filesystem::path& path, const char* description) +{ + auto parent = normalizedAbsolutePath(path).parent_path(); + while (!parent.empty()) { + const auto status = std::filesystem::status(parent); + if (std::filesystem::exists(status)) { + if (!std::filesystem::is_directory(status)) { + throw std::runtime_error(std::string(description) + " parent is not a directory: " + parent.string()); + } + return; + } + parent = parent.parent_path(); + } +} + /** @brief Rejects CTF and Trace Compass targets that overlap unsafely. */ static void validateOutputTargets(const std::filesystem::path& ctfDirectory, const std::filesystem::path& traceCompassXml) @@ -96,6 +117,9 @@ static void removeOutputDirectory(const std::filesystem::path& path) static void validateExistingOutputTypes(const std::filesystem::path& ctfDirectory, const std::filesystem::path& traceCompassXml) { + validateOutputParent(ctfDirectory, "CTF output"); + validateOutputParent(traceCompassXml, "Trace Compass XML output"); + std::error_code ctfError; const auto ctfStatus = std::filesystem::symlink_status(ctfDirectory, ctfError); if (ctfError && ctfError != std::errc::no_such_file_or_directory) { @@ -164,26 +188,60 @@ static void removeIncompleteOutputs(const std::filesystem::path& ctfDirectory, } } +/** @brief Selects only graphical views backed by emitted records in one completed stream. */ +static TraceCompassXmlWriter::ViewMask traceCompassViews(const CtfMetadataModel& metadata, + CtfStreamClassId streamClassId) +{ + auto views = TraceCompassXmlWriter::ViewMask{0U}; + for (const auto topic : kCtfGraphicalTopics) { + if (metadata.observedGraphicalTopic(streamClassId, topic)) { + views |= TraceCompassXmlWriter::viewMask(topic); + } + } + return views; +} + +/** @brief Counts the clock domains referenced by completed CTF streams. */ +static std::size_t traceCompassClockDomainCount(const std::vector& streams) +{ + std::set clocks; + for (const auto& stream : streams) { + clocks.insert(stream.clockDomainId); + } + return clocks.size(); +} + +/** @brief Builds the Trace Compass view routes for completed CTF streams. */ +static std::vector traceCompassViewRoutes(const CtfMetadataModel& metadata) +{ + std::vector viewRoutes; + for (const auto& stream : metadata.topology().streams) { + viewRoutes.push_back({ + stream.route.traceBusId.value_or(0U), + stream.processorName.value_or(std::string{}), + traceCompassViews(metadata, stream.streamClassId), + }); + } + return viewRoutes; +} + CtfBundleOutput::CtfBundleOutput(CtfOutputConfig config, DiagnosticSink* diagnostics) : 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), + }), + m_diagnostics(diagnostics) { validateOutputTargets(m_ctfOutputDirectory, m_traceCompassXmlPath); } CtfBundleOutput::~CtfBundleOutput() { - try { - CtfBundleOutput::abort(); - } catch (...) { - (void)0; - } + abortNoexcept(); } std::string_view CtfBundleOutput::backendName() const noexcept @@ -196,47 +254,75 @@ std::string CtfBundleOutput::targetPath() const return m_ctfOutputDirectory.string(); } -void CtfBundleOutput::start() +void CtfBundleOutput::prepareOutput() { - abort(); validateExistingOutputTypes(m_ctfOutputDirectory, m_traceCompassXmlPath); removeOutputDirectory(m_ctfOutputDirectory); removeOutputFile(m_traceCompassXmlPath); createOutputDirectory(m_ctfOutputDirectory); - m_active = true; - try { - m_encoder.start(m_ctfOutputDirectory); - TraceCompassXmlWriter::writeFile(m_traceCompassXmlPath); - } catch (...) { - abort(); - throw; - } } -void CtfBundleOutput::stop() +void CtfBundleOutput::startOutput() +{ + const auto traceUuid = CtfUuid::randomV4(); + m_encoder.start(m_ctfOutputDirectory, traceUuid); +} + +void CtfBundleOutput::stopOutput() +{ + m_encoder.stop(); + const auto* metadata = m_encoder.completedMetadata(); + // A successful encoder stop always publishes its completed metadata model. + assert(metadata != nullptr); + finalizeTraceCompassXml(*metadata); +} + +void CtfBundleOutput::finalizeTraceCompassXml(const CtfMetadataModel& metadata) { - if (!m_active) { + const auto& streams = metadata.topology().streams; + if (streams.empty()) { + removeOutputFile(m_traceCompassXmlPath); return; } - try { - m_encoder.stop(); - m_active = false; - } catch (...) { - abort(); - throw; + + const auto clockDomainCount = traceCompassClockDomainCount(streams); + if (clockDomainCount != 1U) { + omitTraceCompassXml(clockDomainCount); + return; } + + if (metadata.isLegacySingleStreamLayout()) { + TraceCompassXmlWriter::writeLegacyFile(m_traceCompassXmlPath, + traceCompassViews(metadata, streams.front().streamClassId)); + return; + } + TraceCompassXmlWriter::writeRoutedFile(m_traceCompassXmlPath, traceCompassViewRoutes(metadata)); } -void CtfBundleOutput::abort() +void CtfBundleOutput::omitTraceCompassXml(std::size_t clockDomainCount) { - m_encoder.abort(); - if (m_active) { - removeIncompleteOutputs(m_ctfOutputDirectory, m_traceCompassXmlPath); - m_active = false; + removeOutputFile(m_traceCompassXmlPath); + if (m_diagnostics == nullptr) { + return; } + 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(clockDomainCount)}, + }, + }); +} + +void CtfBundleOutput::abortOutput() +{ + m_encoder.abort(); + removeIncompleteOutputs(m_ctfOutputDirectory, m_traceCompassXmlPath); } -void CtfBundleOutput::writeEvent(const TraceEvent& event) +void CtfBundleOutput::writeOutput(const TraceEvent& event) { m_encoder.writeEvent(event); } diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.h b/tools/ctrace/src/output/ctf/CtfBundleOutput.h index e5e058ef9..08e368c4a 100644 --- a/tools/ctrace/src/output/ctf/CtfBundleOutput.h +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.h @@ -13,6 +13,7 @@ #include "TraceOutput.h" #include "TraceOutputConfig.h" +#include #include class DiagnosticSink; @@ -29,27 +30,36 @@ class CtfBundleOutput final : public TraceOutput { /** @brief Aborts an active bundle before destruction. */ ~CtfBundleOutput() override; - /** @brief Prepares empty CTF and XML targets. */ - void start() override; - /** @brief Completes metadata, stream, and XML output. */ - void stop() override; - /** @brief Removes incomplete CTF and XML targets. */ - void abort() override; - /** - * @brief Encodes one selected semantic event. - * @param event Event evaluated and encoded by the CTF backend. - */ - void writeEvent(const TraceEvent& event) override; /** @brief Returns the CTF backend name. */ std::string_view backendName() const noexcept override; /** @brief Returns the CTF output directory path. */ std::string targetPath() const override; +protected: + /** @brief Prepares an empty CTF target and removes stale companion XML. */ + void prepareOutput() override; + /** @brief Starts the CTF encoder for the prepared target. */ + void startOutput() override; + /** @brief Completes metadata and streams, then writes XML when their clocks permit it. */ + void stopOutput() override; + /** @brief Aborts the encoder and removes incomplete CTF and XML targets. */ + void abortOutput() override; + /** + * @brief Encodes one selected semantic event. + * @param event Event evaluated and encoded by the CTF backend. + */ + void writeOutput(const TraceEvent& event) override; + private: + /** @brief Finalizes or omits the companion Trace Compass XML for completed CTF metadata. */ + void finalizeTraceCompassXml(const CtfMetadataModel& metadata); + /** @brief Removes Trace Compass XML and reports incompatible emitted clock domains. */ + void omitTraceCompassXml(std::size_t clockDomainCount); + std::filesystem::path m_ctfOutputDirectory; std::filesystem::path m_traceCompassXmlPath; CtfEncoder m_encoder; - bool m_active = false; + DiagnosticSink* m_diagnostics = nullptr; }; #endif // CTRACE_SRC_OUTPUT_CTF_CTFBUNDLEOUTPUT_H diff --git a/tools/ctrace/src/output/ctf/CtfEncoder.cpp b/tools/ctrace/src/output/ctf/CtfEncoder.cpp index 5f95076b1..a01443bf8 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" @@ -17,6 +18,7 @@ #include "TraceSelection.h" #include +#include #include #include #include @@ -25,6 +27,7 @@ #include #include #include +#include #include /** @brief Saturates an internal overflow count to the CTF field width. */ @@ -32,53 +35,39 @@ static std::uint32_t ctfOverflowCount(std::uint64_t count) { return static_cast(std::min(count, std::numeric_limits::max())); } -/** @brief Resolves the configured CTF value representation for one DWT comparator. */ -static const CtfSchema::ValueVariant& dwtValueVariant(const ResolvedTraceSource* source, std::uint32_t comparator) + +/** @brief Adapts a normalized route to the legacy CTF event-context field. */ +static std::uint8_t legacyCtfTraceBusId(const TraceRouteIdentity& route) { - static const ResolvedTraceSource 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; + return route.traceBusId.value_or(0U); } -/** @brief Tests whether two routes describe equivalent CTF source metadata. */ -static bool equivalentSourceMetadata(const ResolvedTraceSource& left, const ResolvedTraceSource& right) +/** @brief Enforces an explicit normalized route catalogue when one was supplied. */ +static void validateConfiguredRoute(const CtfEncoderConfig& config, const TraceRouteIdentity& route) { - // 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; + if (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 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) +/** @brief Resolves the configured CTF value representation for one DWT comparator. */ +static const CtfSchema::ValueVariant& dwtValueVariant(const CtfSourceDescriptor* 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; - }); - if (exact != config.sources.end() || traceBusId != 0U) { - return exact == config.sources.end() ? nullptr : &*exact; - } + static const CtfSourceDescriptor defaults; + const auto& resolved = source != nullptr ? *source : defaults; + return *CtfSchema::valueVariantForTraceRunType(resolved.dataType, resolved.dataSize); +} - 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; +/** @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) +{ + return metadata.source(route, type, source); } /** @brief Sign-extends a sample from its configured source width. */ @@ -140,12 +129,115 @@ static void writeDwtAddress(CtfStreamWriter::Record& record, const std::optional } } +/** @brief Dispatches semantic payloads to their type-specific CTF encoders. */ +struct CtfEncoder::PayloadVisitor { + CtfEncoder& encoder; + const TraceEvent& event; + bool selected; + + void operator()(const SoftwareTraceEvent& software) const + { + if (selected) { + encoder.writeSoftwareEvent(event, software); + } + } + + void operator()(const DwtDataTraceEvent& data) const + { + if (selected) { + encoder.writeDwtValueEvent(event, data); + } + } + + void operator()(const DwtAddressTraceEvent& address) const + { + if (selected) { + encoder.writeDwtAddrEvent(event, address); + } + } + + void operator()(const DwtMatchTraceEvent& match) const + { + if (selected) { + encoder.writeDwtMatchEvent(event, match); + } + } + + void operator()(const ExceptionTraceEvent& exception) const + { + if (exception.action != ExceptionAction::Unknown) { + encoder.writeExceptionEvent(event.route, exception); + } + } + + void operator()(const DwtEventTraceEvent& counters) const + { + if (selected) { + encoder.writeDwtEvent(event, counters); + } + } + + void operator()(const PmuTraceEvent& counters) const + { + if (selected) { + encoder.writePmuEvent(event, counters); + } + } + + void operator()(const PcSampleTraceEvent& sample) const + { + if (selected) { + encoder.writePcSampleEvent(event, sample); + } + } + + void operator()(const LocalTimestampTraceEvent&) const + { + encoder.streamState(event.route).localTimestampObserved = true; + } + + void operator()(const GlobalTimestampTraceEvent& timestamp) const + { + if (selected) { + encoder.writeGlobalTimestampEvent(event, timestamp); + } + } + + void operator()(const OverflowTraceEvent&) const + { + auto& routeState = encoder.streamState(event.route); + if (event.quality.has_value()) { + routeState.overflowCount = std::max(routeState.overflowCount, event.quality->overflowCount); + } else { + ++routeState.overflowCount; + } + encoder.writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::Overflow), event.route, selected); + } + + void operator()(const SyncTraceEvent&) const + { + encoder.writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::Resync), event.route, + encoder.m_config.selection.types.empty()); + } + + void operator()(const TraceIssueEvent& issue) const + { + if (issue.code == TraceIssueCode::DataLoss) { + encoder.writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss), event.route, selected); + return; + } + if (event.quality.has_value() && event.quality->overflow) { + encoder.writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss), event.route, selected); + } + if (selected) { + encoder.writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError), event.route, true); + } + } +}; + 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() @@ -153,25 +245,41 @@ 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_metadata.emplace(traceUuid, m_config.metadata); + m_completedMetadata.reset(); + m_streams.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& stream : m_metadata->topology().streams) { + validateConfiguredRoute(m_config, stream.route); + } + for (const auto& source : m_metadata->topology().sources) { + validateConfiguredRoute(m_config, source.route); } - for (const auto traceBusId : initialTraceBusIds) { - writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart), traceBusId, - m_config.selection.types.empty()); - (void)exceptionLane(traceBusId); + 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); + } } } catch (...) { abort(); @@ -185,14 +293,27 @@ 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_reportedDwtSizeMismatches.clear(); + m_exceptionLanes.clear(); m_outputDirectory.clear(); } @@ -201,105 +322,130 @@ 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); + const auto* stream = m_metadata->streamForRoute(event.route); + if (stream == nullptr) { + throw std::runtime_error( + "CTF binary output cannot encode an event route without an exact runtime stream descriptor"); + } + const auto selected = traceEventSelectedForOutput(event, m_config.selection); + if (activatesStream(event, selected)) { + 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; } } - const auto selected = traceEventSelectedForOutput(event, m_config.selection); - if (const auto* software = traceEventPayload(event)) { - if (selected) { - writeSoftwareEvent(event, *software); - } - } else if (const auto* exception = traceEventPayload(event)) { - if (exception->action != ExceptionAction::Unknown) { - writeExceptionEvent(event.traceBusId, *exception); - } - } else if (const auto* data = traceEventPayload(event)) { - if (selected) { - writeDwtValueEvent(event, *data); - } - } else if (const auto* address = traceEventPayload(event)) { - if (selected) { - writeDwtAddrEvent(event, *address); - } - } else if (const auto* match = traceEventPayload(event)) { - if (selected) { - writeDwtMatchEvent(event, *match); - } - } else if (const auto* counters = traceEventPayload(event)) { - if (selected) { - writeDwtEvent(event, *counters); - } - } else if (const auto* counters = traceEventPayload(event)) { - if (selected) { - writePmuEvent(event, *counters); - } - } else if (const auto* sample = traceEventPayload(event)) { - if (selected) { - writePcSampleEvent(event, *sample); - } - } else if (isTraceEvent(event)) { - auto& streamState = m_streamStates[event.traceBusId]; - if (event.quality.has_value()) { - streamState.overflowCount = std::max(streamState.overflowCount, event.quality->overflowCount); - } else { - ++streamState.overflowCount; - } - writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::Overflow), event.traceBusId, selected); - } else if (isTraceEvent(event)) { - m_streamStates[event.traceBusId].localTimestampObserved = true; - } else if (isTraceEvent(event)) { - writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::Resync), event.traceBusId, - m_config.selection.types.empty()); - } else if (const auto* timestamp = traceEventPayload(event)) { - if (selected) { - writeGlobalTimestampEvent(event, *timestamp); - } - } else if (const auto* issue = traceEventPayload(event)) { - if (issue->code == TraceIssueCode::DataLoss) { - writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss), event.traceBusId, selected); - } else { - if (event.quality.has_value() && event.quality->overflow) { - writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss), event.traceBusId, selected); - } - if (selected) { - writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError), event.traceBusId, true); - } - } - } + std::visit(PayloadVisitor{*this, event, selected}, event.payload); +} + +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; 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, - [&](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); + }); + const auto streamClassId = streamDescriptor(event.route).streamClassId; + if (sample.sleeping) { + m_metadata->observeGraphicalTopic(streamClassId, CtfGraphicalTopic::ProcessorState); + } +} + +std::uint64_t CtfEncoder::allocateEventTimestamp(const TraceRouteIdentity& route) +{ + // Each route-specific CtfStreamWriter applies its final monotonic clamp. + return streamState(route).eventTimestamp; +} + +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(), + m_metadata->isLegacySingleStreamLayout() ? CtfStreamWriter::EventContextLayout::Legacy + : CtfStreamWriter::EventContextLayout::RouteLabeled); + } 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()); } -std::uint64_t CtfEncoder::allocateEventTimestamp(std::uint8_t traceBusId) +void CtfEncoder::bootstrapRoute(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; + (void)ensureStreamWriter(streamDescriptor(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,50 +455,54 @@ 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, - [&](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) { - const auto* source = resolvedTraceSource(m_config, "dwt", event.traceBusId, 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 = 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, - [&](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); + }); + m_metadata->observeGraphicalTopic(streamDescriptor(event.route).streamClassId, CtfGraphicalTopic::DwtValue); } 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.traceBusId, data.comparator}).second) { + !m_reportedDwtSizeMismatches.insert({event.route.id, data.comparator}).second) { return; } @@ -362,7 +512,6 @@ 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)); m_config.diagnostics->report({ DiagnosticSink::Severity::Warning, "configured ctrace-run size does not match the decoded SWO payload size", @@ -372,119 +521,144 @@ 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, - [&](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); + }); + if (address.has_value()) { + m_metadata->observeGraphicalTopic(streamDescriptor(event.route).streamClassId, CtfGraphicalTopic::DwtAddress); + } } 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, - [&](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); + }); + m_metadata->observeGraphicalTopic(streamDescriptor(event.route).streamClassId, CtfGraphicalTopic::DwtMatch); } 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); + auto emitted = false; 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, - [&](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); + }); + emitted = true; + } + if (emitted) { + m_metadata->observeGraphicalTopic(streamDescriptor(event.route).streamClassId, CtfGraphicalTopic::DwtEvent); } } 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); + auto emitted = false; for (const auto counter : kPmuEventCounters) { if ((counters.overflowMask & pmuEventCounterBit(counter)) == 0U) { continue; } - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::PmuEvent), eventTimestamp, event.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); + }); + emitted = true; + } + if (emitted) { + m_metadata->observeGraphicalTopic(streamDescriptor(event.route).streamClassId, CtfGraphicalTopic::PmuEvent); } } 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) { - record.writeU64(timestamp.value); - record.writeU8(timestamp.clockChange ? 1U : 0U); - }); + const auto eventTimestamp = allocateEventTimestamp(event.route); + const auto traceBusId = legacyCtfTraceBusId(event.route); + 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, 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); - m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::TraceStatus), eventTimestamp, traceBusId, payloadSize, - [&](CtfStreamWriter::Record& record) { - record.writeU8(reason); - record.writeU32(ctfOverflowCount(m_streamStates[traceBusId].overflowCount)); - }); + const auto eventTimestamp = allocateEventTimestamp(route); + const auto traceBusId = legacyCtfTraceBusId(route); + streamWriter(route).writeRecord(CtfSchema::value(CtfSchema::EventId::TraceStatus), eventTimestamp, traceBusId, + payloadSize, [&](CtfStreamWriter::Record& record) { + record.writeU8(reason); + 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 +670,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 @@ -510,53 +685,56 @@ void CtfEncoder::emitExceptionRecord(std::uint8_t traceBusId, ExceptionNumber nu 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); + }); + const auto streamClassId = streamDescriptor(route).streamClassId; + m_metadata->observeException(streamClassId, number); + if (origin == CtfExceptionLaneTracker::RecordOrigin::Trace) { + m_metadata->observeGraphicalTopic(streamClassId, CtfGraphicalTopic::Exception); + } } -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; - observedExceptionNumbers.insert(lane.observedExceptionNumbers().begin(), lane.observedExceptionNumbers().end()); + std::set emittedStreamClassIds; + for (const auto& [streamClassId, stream] : m_streams) { + (void)stream; + emittedStreamClassIds.insert(streamClassId); } - CtfMetadataWriter::write(m_outputDirectory, m_stream.uuidString(), m_config.coreClockHz, m_config.sources, - { - observedExceptionNumbers.begin(), - observedExceptionNumbers.end(), - }); + CtfMetadataModel completed = m_metadata->projectToEmittedStreams(emittedStreamClassIds); + 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 265549007..660312bcb 100644 --- a/tools/ctrace/src/output/ctf/CtfEncoder.h +++ b/tools/ctrace/src/output/ctf/CtfEncoder.h @@ -9,29 +9,33 @@ #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" +#include "TraceRoute.h" #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 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. */ @@ -45,15 +49,20 @@ 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. */ 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 Dispatches one semantic payload without inflating writeEvent's lifecycle logic. */ + struct PayloadVisitor; + /** @brief Tracks timestamp and trace-quality state for one output stream. */ struct StreamState { std::uint64_t eventTimestamp = 0; @@ -62,17 +71,29 @@ 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 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. */ 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. */ 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. */ @@ -86,23 +107,25 @@ 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); CtfEncoderConfig m_config; std::filesystem::path m_outputDirectory; - CtfStreamWriter m_stream; + std::optional m_metadata; + std::optional m_completedMetadata; + std::map m_streams; bool m_recording = false; - std::map m_streamStates; - std::set> m_reportedDwtSizeMismatches; - std::map m_exceptionLanes; + 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/output/ctf/CtfExceptionLaneTracker.cpp b/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.cpp index 51d21d343..ead698448 100644 --- a/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.cpp +++ b/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.cpp @@ -9,9 +9,7 @@ #include "TraceEvent.h" -#include #include -#include void CtfExceptionLaneTracker::startThreadMode(const RecordEmitter& emit) { @@ -46,24 +44,19 @@ void CtfExceptionLaneTracker::consume(const ExceptionTraceEvent& event, const Re } } -const std::vector& CtfExceptionLaneTracker::observedExceptionNumbers() const -{ - return m_observedExceptionNumbers; -} - void CtfExceptionLaneTracker::setActiveContext(ExceptionNumber number, RecordAction action, RecordOrigin origin, const RecordEmitter& emit) { if (m_activeContextNumber.has_value() && *m_activeContextNumber == number) { if (action == RecordAction::Return) { - emitRecord(number, action, origin, emit); + emit(number, action, origin); } return; } if (m_activeContextNumber.has_value()) { - emitRecord(*m_activeContextNumber, RecordAction::Exit, RecordOrigin::Synthetic, emit); + emit(*m_activeContextNumber, RecordAction::Exit, RecordOrigin::Synthetic); } - emitRecord(number, action, origin, emit); + emit(number, action, origin); m_activeContextNumber = number; } @@ -72,7 +65,7 @@ void CtfExceptionLaneTracker::closeActiveContext(RecordOrigin origin, const Reco if (!m_activeContextNumber.has_value()) { return; } - emitRecord(*m_activeContextNumber, RecordAction::Exit, origin, emit); + emit(*m_activeContextNumber, RecordAction::Exit, origin); m_activeContextNumber.reset(); } @@ -81,16 +74,6 @@ void CtfExceptionLaneTracker::updateActiveContext(RecordAction action, RecordOri setActiveContext(m_contextStack.empty() ? kThreadModeNumber : m_contextStack.back().number, action, origin, emit); } -void CtfExceptionLaneTracker::emitRecord(ExceptionNumber number, RecordAction action, RecordOrigin origin, - const RecordEmitter& emit) -{ - if (std::find(m_observedExceptionNumbers.begin(), m_observedExceptionNumbers.end(), number) == - m_observedExceptionNumbers.end()) { - m_observedExceptionNumbers.push_back(number); - } - emit(number, action, origin); -} - void CtfExceptionLaneTracker::enterContext(ExceptionNumber number) { if (!m_contextStack.empty() && m_contextStack.back().state == ContextState::Running) { diff --git a/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.h b/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.h index d50e89076..1e8bc531b 100644 --- a/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.h +++ b/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.h @@ -40,8 +40,6 @@ class CtfExceptionLaneTracker { void resetForDiscontinuity(const RecordEmitter& emit); /** @brief Applies one exception transition and emits resulting lane records. */ void consume(const ExceptionTraceEvent& event, const RecordEmitter& emit); - /** @brief Returns exception numbers observed by this tracker. */ - const std::vector& observedExceptionNumbers() const; private: static constexpr ExceptionNumber kThreadModeNumber = 0; @@ -64,8 +62,6 @@ class CtfExceptionLaneTracker { void closeActiveContext(RecordOrigin origin, const RecordEmitter& emit); /** @brief Activates the context selected by the stack after an enter or return. */ void updateActiveContext(RecordAction action, RecordOrigin origin, const RecordEmitter& emit); - /** @brief Emits and records one lane transition. */ - void emitRecord(ExceptionNumber number, RecordAction action, RecordOrigin origin, const RecordEmitter& emit); /** @brief Pushes or reactivates an entered exception context. */ void enterContext(ExceptionNumber number); /** @brief Removes an exited running exception context. */ @@ -75,7 +71,6 @@ class CtfExceptionLaneTracker { std::vector m_contextStack; std::optional m_activeContextNumber; - std::vector m_observedExceptionNumbers; }; #endif // CTRACE_SRC_OUTPUT_CTF_CTFEXCEPTIONLANETRACKER_H diff --git a/tools/ctrace/src/output/ctf/CtfGraphicalTopic.h b/tools/ctrace/src/output/ctf/CtfGraphicalTopic.h new file mode 100644 index 000000000..f7b7573de --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfGraphicalTopic.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_CTF_CTFGRAPHICALTOPIC_H +#define CTRACE_SRC_OUTPUT_CTF_CTFGRAPHICALTOPIC_H + +#include +#include +#include + +/** @brief Identifies one graphical Trace Compass topic backed by emitted CTF records. */ +enum class CtfGraphicalTopic : std::uint8_t { + DwtValue, + DwtAddress, + DwtMatch, + DwtEvent, + PmuEvent, + Exception, + ProcessorState, + Count, +}; + +/** @brief Lists every graphical topic in declaration order. */ +inline constexpr std::array(CtfGraphicalTopic::Count)> kCtfGraphicalTopics{{ + CtfGraphicalTopic::DwtValue, + CtfGraphicalTopic::DwtAddress, + CtfGraphicalTopic::DwtMatch, + CtfGraphicalTopic::DwtEvent, + CtfGraphicalTopic::PmuEvent, + CtfGraphicalTopic::Exception, + CtfGraphicalTopic::ProcessorState, +}}; + +/** @brief Checks that the topic list contains every declared topic exactly once. */ +constexpr bool hasCompleteCtfGraphicalTopicList() noexcept +{ + std::array found{}; + for (const auto topic : kCtfGraphicalTopics) { + const auto index = static_cast(topic); + if (index >= found.size() || found[index]) { + return false; + } + found[index] = true; + } + for (const auto present : found) { + if (!present) { + return false; + } + } + return true; +} + +static_assert(hasCompleteCtfGraphicalTopicList(), "CTF graphical topic list must be complete and unique"); + +#endif // CTRACE_SRC_OUTPUT_CTF_CTFGRAPHICALTOPIC_H diff --git a/tools/ctrace/src/output/ctf/CtfMetadataModel.cpp b/tools/ctrace/src/output/ctf/CtfMetadataModel.cpp new file mode 100644 index 000000000..5781a5208 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfMetadataModel.cpp @@ -0,0 +1,292 @@ +/* + * 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()}; +} + +void CtfMetadataModel::observeGraphicalTopic(CtfStreamClassId streamClassId, CtfGraphicalTopic topic) +{ + if (std::none_of(m_topology.streams.begin(), m_topology.streams.end(), [&](const auto& stream) { + return stream.streamClassId == streamClassId; + })) { + throw std::runtime_error("CTF graphical-topic observation references an unknown stream class"); + } + m_observedGraphicalTopics[streamClassId].insert(topic); +} + +bool CtfMetadataModel::observedGraphicalTopic(CtfStreamClassId streamClassId, CtfGraphicalTopic topic) const +{ + const auto found = m_observedGraphicalTopics.find(streamClassId); + return found != m_observedGraphicalTopics.end() && found->second.find(topic) != found->second.end(); +} + +CtfMetadataModel CtfMetadataModel::projectToEmittedStreams(const std::set& streamClassIds) const +{ + CtfMetadataTopology topology; + std::set clockDomainIds; + std::set routeIds; + for (const auto& stream : m_topology.streams) { + if (streamClassIds.find(stream.streamClassId) != streamClassIds.end()) { + topology.streams.push_back(stream); + clockDomainIds.insert(stream.clockDomainId); + routeIds.insert(stream.route.id); + } + } + for (const auto& clock : m_topology.clockDomains) { + if (clockDomainIds.find(clock.id) != clockDomainIds.end()) { + topology.clockDomains.push_back(clock); + } + } + for (const auto& source : m_topology.sources) { + if (routeIds.find(source.route.id) != routeIds.end()) { + topology.sources.push_back(source); + } + } + + CtfMetadataModel projected(m_traceUuid, std::move(topology)); + for (const auto streamClassId : streamClassIds) { + if (std::none_of(projected.topology().streams.begin(), projected.topology().streams.end(), + [&](const auto& stream) { return stream.streamClassId == streamClassId; })) { + continue; + } + for (const auto number : observedExceptions(streamClassId)) { + projected.observeException(streamClassId, number); + } + const auto topics = m_observedGraphicalTopics.find(streamClassId); + if (topics != m_observedGraphicalTopics.end()) { + for (const auto topic : topics->second) { + projected.observeGraphicalTopic(streamClassId, topic); + } + } + } + return projected; +} + +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.clockDomainId == clock.id; +} + +void CtfMetadataModel::validate() const +{ + validateClockDomains(); + validateStreams(); + validateSources(); + validateNonLegacyClockDomains(); +} + +void CtfMetadataModel::validateClockDomains() 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"); + } + } +} + +void CtfMetadataModel::validateStreams() const +{ + 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.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"); + } + } +} + +void CtfMetadataModel::validateSources() const +{ + 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"); + } + } + } +} + +void CtfMetadataModel::validateNonLegacyClockDomains() const +{ + 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..c8adbc778 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfMetadataModel.h @@ -0,0 +1,186 @@ +/* + * 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 "CtfGraphicalTopic.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 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; + 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 Records one graphical topic backed by emitted records on a concrete stream class. */ + void observeGraphicalTopic(CtfStreamClassId streamClassId, CtfGraphicalTopic topic); + /** @brief Tests whether one graphical topic is backed by emitted records on a stream class. */ + bool observedGraphicalTopic(CtfStreamClassId streamClassId, CtfGraphicalTopic topic) const; + /** @brief Projects configured topology and observations to streams with completed packet output. */ + CtfMetadataModel projectToEmittedStreams(const std::set& streamClassIds) const; + /** @brief Tests whether this topology uses the exact legacy single-stream CTF layout. */ + bool isLegacySingleStreamLayout() const noexcept; + +private: + /** @brief Runs every topology validation group in deterministic error order. */ + void validate() const; + /** @brief Validates clock-domain identity and scalar properties. */ + void validateClockDomains() const; + /** @brief Validates stream identities and their clock-domain references. */ + void validateStreams() const; + /** @brief Validates source identities, metadata, and ordering. */ + void validateSources() const; + /** @brief Requires explicit clock UUIDs outside the exact legacy layout. */ + void validateNonLegacyClockDomains() const; + + CtfUuid m_traceUuid; + CtfMetadataTopology m_topology; + std::map> m_observedExceptions; + std::map> m_observedGraphicalTopics; +}; + +#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..7d5ded373 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& observedExceptionNumbers) +/** @brief Writes primitive TSDL type aliases shared by all metadata layouts. */ +static void writePrimitiveTypeDefinitions(std::ostream& out) { out << R"( typealias integer { size = 8; align = 8; signed = false; } := uint8_t; @@ -289,8 +285,13 @@ typealias integer { size = 16; align = 8; signed = true; byte_order = le; } := i 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; +)"; +} +/** @brief Writes fixed CMSIS enumeration declarations shared by all metadata layouts. */ +static void writeFixedCmsisEnumerationDefinitions(std::ostream& out) +{ + out << R"( typealias enum : uint8_t { "read" = )" << static_cast(CtfSchema::value(CtfSchema::DwtAccess::Read)) << R"(, @@ -326,18 +327,28 @@ typealias enum : uint8_t { typealias enum : uint8_t { )"; for (const auto counter : kDwtEventCounters) { - out << " \"" << CtfSchema::dwtEventCounterName(counter) << "\" = " - << static_cast(CtfSchema::value(counter)) << ",\n"; + 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 << " \"" << CtfSchema::pmuEventCounterName(counter) + << "\" = " << static_cast(CtfSchema::value(counter)) << ",\n"; } - out << R"(} := cmsis_pmu_event_counter_t; -typealias enum : uint8_t { + out << "} := cmsis_pmu_event_counter_t;\n"; +} + +/** @brief Writes reusable TSDL type and enumeration declarations. */ +static void writeTypeDefinitions(std::ostream& out, const MetadataSymbols& symbols, + const std::vector& observedExceptionNumbers) +{ + writePrimitiveTypeDefinitions(out); + out << "typealias integer { size = 64; align = 8; signed = false; map = clock.swo_clock.value; } := " + "swo_clock_t;\n"; + writeFixedCmsisEnumerationDefinitions(out); + out << R"(typealias enum : uint8_t { )"; std::set itmLabels; for (std::uint32_t channel = 1U; channel < 32U; ++channel) { @@ -365,25 +376,33 @@ typealias enum : uint16_t { )"; } -/** @brief Writes the packet and event context definition for the SWO stream. */ -static void writeStreamDefinition(std::ostream& out) +/** @brief Writes packet and event context definitions for one CTF stream. */ +static void writeStreamDefinition(std::ostream& out, std::uint32_t streamClassId = CtfSchema::SwoStreamId, + std::string_view timestampType = "swo_clock_t", std::string_view routeType = {}) { out << R"( stream { id = )" - << CtfSchema::SwoStreamId << R"(; + << streamClassId << R"(; event.header := struct { uint32_t id; - swo_clock_t timestamp; + )" + << timestampType << R"( timestamp; }; event.context := struct { uint8_t cmsis_trace_bus_id; - }; +)"; + if (!routeType.empty()) { + out << " " << routeType << " ctrace_route;\n"; + } + out << R"( }; packet.context := struct { uint32_t packet_size; uint32_t content_size; - swo_clock_t timestamp_begin; - swo_clock_t timestamp_end; + )" + << timestampType << R"( timestamp_begin; + )" + << timestampType << R"( timestamp_end; uint32_t events_discarded; uint32_t packet_seq_num; }; @@ -392,7 +411,8 @@ stream { } /** @brief Writes the ITM software event declaration. */ -static void writeItmEvent(std::ostream& out) +static void writeItmEvent(std::ostream& out, std::uint32_t streamClassId = CtfSchema::SwoStreamId, + std::string_view channelType = "cmsis_itm_channel_t") { out << R"( event { @@ -401,9 +421,10 @@ event { name = ")" << CtfSchema::eventName(CtfSchema::EventId::Itm) << R"("; stream_id = )" - << CtfSchema::SwoStreamId << R"(; + << streamClassId << R"(; fields := struct { - cmsis_itm_channel_t cmsis_itm_channel; + )" + << channelType << R"( cmsis_itm_channel; )" << ctfValueFields("itm") << R"( uint8_t cmsis_sample_flags; uint32_t cmsis_overflow_count; @@ -413,7 +434,8 @@ event { } /** @brief Writes the DWT value event declaration. */ -static void writeDwtValueEvent(std::ostream& out) +static void writeDwtValueEvent(std::ostream& out, std::uint32_t streamClassId = CtfSchema::SwoStreamId, + std::string_view comparatorType = "cmsis_dwt_comparator_t") { out << R"( event { @@ -422,12 +444,13 @@ event { name = ")" << CtfSchema::eventName(CtfSchema::EventId::DwtValue) << R"("; stream_id = )" - << CtfSchema::SwoStreamId << R"(; + << streamClassId << R"(; fields := struct { - cmsis_dwt_comparator_t cmsis_dwt_comparator; + )" + << comparatorType << R"( cmsis_dwt_comparator; cmsis_dwt_access_t cmsis_dwt_access; -)" << ctfValueFields("dwt") << ctfDwtAddressFields("pc") << ctfDwtAddressFields("address") - << R"( uint8_t cmsis_sample_flags; +)" << ctfValueFields("dwt") + << ctfDwtAddressFields("pc") << ctfDwtAddressFields("address") << R"( uint8_t cmsis_sample_flags; uint32_t cmsis_overflow_count; }; }; @@ -435,7 +458,8 @@ event { } /** @brief Writes the DWT address event declaration. */ -static void writeDwtAddressEvent(std::ostream& out) +static void writeDwtAddressEvent(std::ostream& out, std::uint32_t streamClassId = CtfSchema::SwoStreamId, + std::string_view comparatorType = "cmsis_dwt_comparator_t") { out << R"( event { @@ -444,11 +468,12 @@ event { name = ")" << CtfSchema::eventName(CtfSchema::EventId::DwtAddress) << R"("; stream_id = )" - << CtfSchema::SwoStreamId << R"(; + << streamClassId << R"(; fields := struct { - cmsis_dwt_comparator_t cmsis_dwt_comparator; -)" << ctfDwtAddressFields("pc") << ctfDwtAddressFields("address") - << R"( uint8_t cmsis_sample_flags; + )" + << comparatorType << R"( cmsis_dwt_comparator; +)" << ctfDwtAddressFields("pc") + << ctfDwtAddressFields("address") << R"( uint8_t cmsis_sample_flags; uint32_t cmsis_overflow_count; }; }; @@ -456,7 +481,8 @@ event { } /** @brief Writes the comparator-only DWT match event declaration. */ -static void writeDwtMatchEvent(std::ostream& out) +static void writeDwtMatchEvent(std::ostream& out, std::uint32_t streamClassId = CtfSchema::SwoStreamId, + std::string_view comparatorType = "cmsis_dwt_comparator_t") { out << R"( event { @@ -465,9 +491,10 @@ event { name = ")" << CtfSchema::eventName(CtfSchema::EventId::DwtMatch) << R"("; stream_id = )" - << CtfSchema::SwoStreamId << R"(; + << streamClassId << R"(; fields := struct { - cmsis_dwt_comparator_t cmsis_dwt_comparator; + )" + << comparatorType << R"( cmsis_dwt_comparator; uint8_t cmsis_sample_flags; uint32_t cmsis_overflow_count; }; @@ -476,7 +503,7 @@ event { } /** @brief Writes the DWT event-counter declaration. */ -static void writeDwtEvent(std::ostream& out) +static void writeDwtEvent(std::ostream& out, std::uint32_t streamClassId = CtfSchema::SwoStreamId) { out << R"( event { @@ -485,7 +512,7 @@ event { name = ")" << CtfSchema::eventName(CtfSchema::EventId::DwtEvent) << R"("; stream_id = )" - << CtfSchema::SwoStreamId << R"(; + << streamClassId << R"(; fields := struct { cmsis_dwt_event_counter_t cmsis_dwt_event_counter; uint8_t cmsis_sample_flags; @@ -496,7 +523,7 @@ event { } /** @brief Writes the programmable PMU event-counter declaration. */ -static void writePmuEvent(std::ostream& out) +static void writePmuEvent(std::ostream& out, std::uint32_t streamClassId = CtfSchema::SwoStreamId) { out << R"( event { @@ -505,7 +532,7 @@ event { name = ")" << CtfSchema::eventName(CtfSchema::EventId::PmuEvent) << R"("; stream_id = )" - << CtfSchema::SwoStreamId << R"(; + << streamClassId << R"(; fields := struct { cmsis_pmu_event_counter_t cmsis_pmu_event_counter; uint8_t cmsis_sample_flags; @@ -516,7 +543,7 @@ event { } /** @brief Writes the periodic PC-sample event declaration. */ -static void writePcSampleEvent(std::ostream& out) +static void writePcSampleEvent(std::ostream& out, std::uint32_t streamClassId = CtfSchema::SwoStreamId) { out << R"( event { @@ -525,7 +552,7 @@ event { name = ")" << CtfSchema::eventName(CtfSchema::EventId::PcSample) << R"("; stream_id = )" - << CtfSchema::SwoStreamId << R"(; + << streamClassId << R"(; fields := struct { uint8_t cmsis_pc_sample_state; uint32_t cmsis_pc[cmsis_pc_sample_state]; @@ -537,7 +564,8 @@ event { } /** @brief Writes status, exception, and global timestamp declarations. */ -static void writeStatusEvents(std::ostream& out) +static void writeStatusEvents(std::ostream& out, std::uint32_t streamClassId = CtfSchema::SwoStreamId, + std::string_view exceptionType = "cmsis_exception_number_t") { out << R"( event { @@ -546,7 +574,7 @@ event { name = ")" << CtfSchema::eventName(CtfSchema::EventId::TraceStatus) << R"("; stream_id = )" - << CtfSchema::SwoStreamId << R"(; + << streamClassId << R"(; fields := struct { cmsis_trace_status_reason_t cmsis_trace_status_reason; uint32_t cmsis_overflow_count; @@ -559,9 +587,10 @@ event { name = ")" << CtfSchema::eventName(CtfSchema::EventId::Exception) << R"("; stream_id = )" - << CtfSchema::SwoStreamId << R"(; + << streamClassId << R"(; fields := struct { - cmsis_exception_number_t cmsis_exception_number; + )" + << exceptionType << R"( cmsis_exception_number; cmsis_exception_action_t cmsis_exception_action; uint16_t cmsis_exception_number_value; cmsis_exception_origin_t cmsis_exception_origin; @@ -574,7 +603,7 @@ event { name = ")" << CtfSchema::eventName(CtfSchema::EventId::GlobalTimestamp) << R"("; stream_id = )" - << CtfSchema::SwoStreamId << R"(; + << streamClassId << R"(; fields := struct { uint64_t cmsis_global_timestamp; uint8_t cmsis_clock_change; @@ -583,28 +612,180 @@ event { )"; } -void CtfMetadataWriter::write(const std::filesystem::path& outputDir, const std::string& uuidString, - std::uint64_t coreClockHz, 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 Returns the Trace Compass route label carried by one stream context. */ +static std::string streamRouteLabel(const CtfStreamDescriptor& stream) +{ + if (stream.processorName.has_value() && !stream.processorName->empty()) { + return *stream.processorName; + } + return 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)); + if (stream.processorName.has_value() && !stream.processorName->empty()) { + out << " " << prefix << "_processor_name = " << tsdlString(*stream.processorName) << ";\n"; + } + 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) +{ + writePrimitiveTypeDefinitions(out); + 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"; + } + writeFixedCmsisEnumerationDefinitions(out); +} + +/** @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); + const auto traceBusId = static_cast(stream.route.traceBusId.value_or(0U)); + out << "typealias enum : uint8_t {\n" + << " " << tsdlString(streamRouteLabel(stream)) << " = " << traceBusId << ",\n" + << "} := " << prefix << "_route_t;\n" + << "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", prefix + "_route_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); + std::ofstream out(metadataPath, std::ios::out | std::ios::binary | std::ios::trunc); if (!out) { 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..82013b8ba 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,7 @@ 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(); -} +constexpr std::size_t kRouteLabelContextSize = 1U; CtfStreamWriter::Record::Record(std::vector& buffer, std::size_t offset, std::size_t endOffset) : m_buffer(buffer), @@ -89,22 +74,16 @@ 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, EventContextLayout eventContextLayout) { 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_eventContextLayout = eventContextLayout; m_packetBuffer.assign(kPacketSizeBytes, 0U); beginPacket(); @@ -146,7 +125,8 @@ void CtfStreamWriter::writeRecord(std::uint32_t eventId, std::uint64_t timestamp if (!m_open) { return; } - const auto totalSize = kEventPrefixSize + payloadSize; + const auto routeContextSize = m_eventContextLayout == EventContextLayout::RouteLabeled ? kRouteLabelContextSize : 0U; + const auto totalSize = kEventPrefixSize + routeContextSize + payloadSize; if (totalSize > kPacketSizeBytes - kPacketOverhead) { throw std::invalid_argument("CTF record does not fit into a packet"); } @@ -160,6 +140,9 @@ void CtfStreamWriter::writeRecord(std::uint32_t eventId, std::uint64_t timestamp record.writeU32(eventId); record.writeU64(timestamp); record.writeU8(traceBusId); + if (m_eventContextLayout == EventContextLayout::RouteLabeled) { + record.writeU8(traceBusId); + } writePayload(record); if (record.m_offset != recordEnd) { throw std::logic_error("CTF record payload is shorter than its declared size"); @@ -176,11 +159,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 +176,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..13ac4ae3d 100644 --- a/tools/ctrace/src/output/ctf/CtfStreamWriter.h +++ b/tools/ctrace/src/output/ctf/CtfStreamWriter.h @@ -8,19 +8,26 @@ #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. */ class CtfStreamWriter final { public: + /** @brief Selects the event-context schema encoded by one stream. */ + enum class EventContextLayout { + Legacy, + RouteLabeled, + }; + /** @brief Provides bounded little-endian writes into one reserved record payload. */ class Record final { public: @@ -60,20 +67,18 @@ 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, + EventContextLayout eventContextLayout = EventContextLayout::Legacy); /** @brief Flushes the final packet and closes the stream. */ void close(); - /** @brief Closes and removes an incomplete stream without throwing. */ + /** @brief Closes an incomplete stream and discards buffered writer state without throwing. */ void abort() noexcept; /** @brief Appends one timestamped CTF event record; its size is needed up front for packet rollover and bounds. */ 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 +97,8 @@ 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; + EventContextLayout m_eventContextLayout = EventContextLayout::Legacy; 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/src/output/ctf/TraceCompassXmlWriter.cpp b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp index f1111a2d0..27d3a1012 100644 --- a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp +++ b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp @@ -8,6 +8,7 @@ #include "TraceCompassXmlWriter.h" #include "CtfSchema.h" +#include "TraceStreamId.h" #include #include @@ -15,10 +16,12 @@ #include #include #include +#include #include #include #include #include +#include constexpr const char* kTraceCompassAnalysisVersionPlaceholder = "__SWO_ANALYSIS_VERSION__"; // Stack depth keeps overlapping visual pulses active until their last scheduled pop. @@ -50,9 +53,18 @@ static std::string withTraceCompassAnalysisVersion(std::string xml) return xml; } +/** @brief Renders route path components while generating a formatted-stream analysis. */ +static std::string statePathPrefix(bool routePrefixed) +{ + return routePrefixed + ? " \n" + " \n" + : ""; +} + /** @brief Generates Trace Compass value handlers for one CTF event route. */ static std::string valueHandlers(CtfSchema::EventId eventId, const char* prefix, const char* routeField, - const char* valueAttribute) + const char* valueAttribute, bool routePrefixed) { std::ostringstream handlers; for (const auto& arm : CtfSchema::ValueVariants) { @@ -66,7 +78,9 @@ static std::string valueHandlers(CtfSchema::EventId eventId, const char* prefix, - @@ -84,7 +98,8 @@ static std::string valueHandlers(CtfSchema::EventId eventId, const char* prefix, /** @brief Generates visible pulses for one family of event counters. */ template static std::string eventCounterHandlers(CtfSchema::EventId eventId, std::string_view field, - const std::array& counters, CounterName counterName) + const std::array& counters, CounterName counterName, + bool routePrefixed) { std::ostringstream handlers; for (const auto counter : counters) { @@ -100,7 +115,8 @@ static std::string eventCounterHandlers(CtfSchema::EventId eventId, std::string_ - @@ -117,7 +133,8 @@ static std::string eventCounterHandlers(CtfSchema::EventId eventId, std::string_ - @@ -134,17 +151,21 @@ static std::string eventCounterHandlers(CtfSchema::EventId eventId, std::string_ } /** @brief Generates one visible pulse for every comparator-only DWT match. */ -static std::string dwtMatchHandler() +static std::string dwtMatchHandler(bool routePrefixed) { std::ostringstream handler; handler << R"( - - @@ -158,7 +179,7 @@ static std::string dwtMatchHandler() } /** @brief Generates one DWT address handler for each encoded data-address width. */ -static std::string dwtAddressHandlers() +static std::string dwtAddressHandlers(bool routePrefixed) { std::ostringstream handlers; static_assert(CtfSchema::DwtAddressVariants.front().tag == CtfSchema::DwtAddressTag::None); @@ -173,7 +194,9 @@ static std::string dwtAddressHandlers() - @@ -186,49 +209,26 @@ static std::string dwtAddressHandlers() return handlers.str(); } -/** @brief Generates the Trace Compass state-provider definition. */ -static std::string stateProviderXml() +/** @brief Returns whether a selected graphical view requires its corresponding event handler. */ +static bool includesView(TraceCompassXmlWriter::ViewMask views, TraceCompassXmlWriter::View view) { - // Numeric time-graph states are exposed as TSP style keys; string states are - // serialized without a style and appear as gaps in compatible clients. - std::ostringstream xml; - xml << R"( - )"; - return xml.str(); } -/** @brief Generates the Trace Compass time-graph view definitions. */ -static std::string viewsXml() +/** @brief Writes state resets for trace discontinuities. */ +static void writeDiscontinuityStateHandler(std::ostream& xml, bool routePrefixed, bool exceptionView, + bool processorStateView) { + if (!exceptionView && !processorStateView) { + return; + } + + std::ostringstream stateChanges; + if (processorStateView) { + writeProcessorDiscontinuityStateChange(stateChanges, routePrefixed); + } + if (exceptionView) { + writeExceptionDiscontinuityStateChanges(stateChanges, routePrefixed); + } + writeStateHandler(xml, CtfSchema::EventId::TraceStatus, stateChanges.str()); +} + +/** @brief Generates the Trace Compass state-provider definition. */ +static std::string stateProviderXml(bool routePrefixed, TraceCompassXmlWriter::ViewMask views) +{ + // Numeric time-graph states are exposed as TSP style keys; string states are + // serialized without a style and appear as gaps in compatible clients. std::ostringstream xml; - xml << R"( - - - - - - - - - \n"; +} + +/** @brief Writes the DWT event-counter view. */ +static void writeDwtEventView(std::ostream& xml, const TraceCompassXmlWriter::ViewRoute* route) +{ + writeEventCounterView(xml, route, "arm.cmsis.swo.tg.dwt_event", "DWT Event Counters", + CtfSchema::EventId::DwtEvent, kDwtEventCounters); +} + +/** @brief Writes the PMU event-counter view. */ +static void writePmuEventView(std::ostream& xml, const TraceCompassXmlWriter::ViewRoute* route) +{ + writeEventCounterView(xml, route, "arm.cmsis.swo.tg.pmu_event", "PMU Event Counters", + CtfSchema::EventId::PmuEvent, kPmuEventCounters); +} + +/** @brief Writes the exception timeline view. */ +static void writeExceptionView(std::ostream& xml, const TraceCompassXmlWriter::ViewRoute* route) +{ + xml << " \n" + << " \n"; +} + +/** @brief Writes the processor-state timeline view. */ +static void writeProcessorStateView(std::ostream& xml, const TraceCompassXmlWriter::ViewRoute* route) +{ + xml << " \n" + << " \n"; +} + +/** @brief Generates only graphical Trace Compass views, one per route for generalized CTF. */ +static std::string viewsXml(bool routePrefixed, + const std::vector& routes, + TraceCompassXmlWriter::ViewMask legacyViews) +{ + std::ostringstream xml; + writeSelectedViews(xml, routePrefixed, routes, legacyViews, TraceCompassXmlWriter::View::DwtValue, + writeDwtValueView); + writeSelectedViews(xml, routePrefixed, routes, legacyViews, TraceCompassXmlWriter::View::DwtAddress, + writeDwtAddressView); + writeSelectedViews(xml, routePrefixed, routes, legacyViews, TraceCompassXmlWriter::View::DwtMatch, + writeDwtMatchView); + writeSelectedViews(xml, routePrefixed, routes, legacyViews, TraceCompassXmlWriter::View::DwtEvent, + writeDwtEventView); + writeSelectedViews(xml, routePrefixed, routes, legacyViews, TraceCompassXmlWriter::View::PmuEvent, + writePmuEventView); + writeSelectedViews(xml, routePrefixed, routes, legacyViews, TraceCompassXmlWriter::View::Exception, + writeExceptionView); + writeSelectedViews(xml, routePrefixed, routes, legacyViews, TraceCompassXmlWriter::View::ProcessorState, + writeProcessorStateView); return xml.str(); } /** @brief Assembles the complete versioned Trace Compass analysis XML. */ -static std::string traceCompassXml() +static std::string traceCompassXml(bool routePrefixed, + const std::vector& routes, + TraceCompassXmlWriter::ViewMask legacyViews) { std::ostringstream xml; xml << R"( )"; - xml << stateProviderXml(); - xml << viewsXml(); + auto activeViews = legacyViews; + if (routePrefixed) { + activeViews = 0U; + for (const auto& route : routes) { + activeViews |= route.views; + } + } + xml << stateProviderXml(routePrefixed, activeViews); + xml << viewsXml(routePrefixed, routes, legacyViews); xml << R"( )"; return withTraceCompassAnalysisVersion(xml.str()); } -void TraceCompassXmlWriter::writeFile(const std::filesystem::path& filePath) +static void writeXmlFile(const std::filesystem::path& filePath, bool routePrefixed, + const std::vector& routes, + TraceCompassXmlWriter::ViewMask legacyViews) { + if ((legacyViews & ~TraceCompassXmlWriter::AllViews) != 0U) { + throw std::invalid_argument("Trace Compass XML contains an unsupported legacy view selection"); + } + if (routePrefixed) { + if (routes.empty()) { + throw std::invalid_argument("route-prefixed Trace Compass XML requires at least one view route"); + } + std::array(CoreSight::kMaxAtbTraceId) + 1U> seenIds{}; + for (const auto& route : routes) { + if ((route.views & ~TraceCompassXmlWriter::AllViews) != 0U) { + throw std::invalid_argument("Trace Compass XML contains an unsupported route view selection"); + } + if (!CoreSight::isAtbTraceId(route.traceBusId)) { + throw std::invalid_argument("route-prefixed Trace Compass XML requires Trace Bus IDs between 1 and 111"); + } + if (seenIds[route.traceBusId]) { + throw std::invalid_argument("route-prefixed Trace Compass XML requires unique Trace Bus IDs"); + } + seenIds[route.traceBusId] = true; + } + } if (!filePath.parent_path().empty()) { std::filesystem::create_directories(filePath.parent_path()); } - std::ofstream out(filePath, std::ios::out | std::ios::trunc); + std::ofstream out(filePath, std::ios::out | std::ios::binary | std::ios::trunc); if (!out) { throw std::runtime_error("Failed to write Trace Compass XML " + filePath.string()); } - out << traceCompassXml(); + out << traceCompassXml(routePrefixed, routes, legacyViews); out.close(); if (!out) { throw std::runtime_error("Failed to write Trace Compass XML " + filePath.string()); } } + +void TraceCompassXmlWriter::writeLegacyFile(const std::filesystem::path& filePath, ViewMask views) +{ + writeXmlFile(filePath, false, {}, views); +} + +void TraceCompassXmlWriter::writeRoutedFile(const std::filesystem::path& filePath, + const std::vector& routes) +{ + writeXmlFile(filePath, true, routes, 0U); +} diff --git a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.h b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.h index f168d91a2..9b91c2ff8 100644 --- a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.h +++ b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.h @@ -8,17 +8,50 @@ #ifndef CTRACE_SRC_OUTPUT_CTF_TRACECOMPASSXMLWRITER_H #define CTRACE_SRC_OUTPUT_CTF_TRACECOMPASSXMLWRITER_H +#include "CtfGraphicalTopic.h" + +#include #include +#include +#include +#include /** @brief Writes the Trace Compass analysis definition accompanying CTF output. */ class TraceCompassXmlWriter final { public: - /** @brief Writes the complete analysis definition to a file. */ - static void writeFile(const std::filesystem::path& path); + /** @brief Identifies one graphical analysis block that can be exposed in Trace Compass. */ + using View = CtfGraphicalTopic; + + /** @brief Stores a set of graphical analysis blocks. */ + using ViewMask = std::uint32_t; + + static_assert(static_cast(View::Count) < std::numeric_limits::digits, + "Trace Compass view mask must represent every graphical topic"); + + /** @brief Converts one graphical view to its set bit. */ + static constexpr ViewMask viewMask(View view) noexcept + { + return ViewMask{1U} << static_cast(view); + } + + /** @brief Enables every supported graphical analysis block. */ + static constexpr ViewMask AllViews = (ViewMask{1U} << static_cast(View::Count)) - 1U; + + /** @brief Identifies one visible route without exposing its architectural ID in the label. */ + struct ViewRoute { + std::uint8_t traceBusId = 0U; + std::string label; + ViewMask views = AllViews; + }; + + /** @brief Writes a legacy single-stream analysis definition to a file. */ + static void writeLegacyFile(const std::filesystem::path& path, ViewMask views = AllViews); + /** @brief Writes a route-prefixed multi-stream analysis definition to a file. */ + static void writeRoutedFile(const std::filesystem::path& path, const std::vector& routes); private: /** @brief Prevents construction of this stateless XML utility. */ TraceCompassXmlWriter() = delete; }; -#endif // CTRACE_SRC_OUTPUT_CTF_TRACECOMPASSXMLWRITER_H +#endif // CTRACE_SRC_OUTPUT_CTF_TRACECOMPASSXMLWRITER_H diff --git a/tools/ctrace/src/tracerun/CtraceRunMeta.cpp b/tools/ctrace/src/tracerun/CtraceRunMeta.cpp index 69d6eb5ef..a067ae1ae 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,97 @@ struct ProcessorMeta { std::optional timestampClockHz; std::optional timestampClockError; std::optional timestampPrescaler; + bool itmEnableConflict = false; std::optional itmEnableMask; }; +/** @brief Tests whether a feature leaf ends in one non-empty decimal index. */ +static bool hasDecimalSuffix(const std::string_view leaf, const std::string_view prefix) +{ + if (leaf.size() <= prefix.size() || leaf.substr(0U, prefix.size()) != prefix) { + return false; + } + 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 type accepts one processor-scoped feature leaf. */ +static bool isProcessorScopedFeature(const std::string_view type, const std::string_view leaf) +{ + if (type == "itm") { + return leaf == "itm" || leaf == "timestamps"; + } + if (type == "dwt") { + return leaf == "timestamps" || leaf == "synchronization" || hasDecimalSuffix(leaf, "data#"); + } + if (type == "exception") { + return leaf == "exceptions"; + } + if (type == "event" || type == "pmu") { + return hasDecimalSuffix(leaf, "events#"); + } + if (type == "pcsample") { + return leaf == "pcsampling"; + } + if (type == "overflow") { + return leaf == "overflow"; + } + return type == "global_ts" && leaf == "timesync"; +} + +/** @brief Returns the single non-leading separator of one `[pname/]feature` path. */ +static std::optional processorFeatureSeparator(const std::string_view path) +{ + const auto separator = path.find('/'); + if (separator == std::string_view::npos || separator == 0U) { + return std::nullopt; + } + if (path.find('/', separator + 1U) != std::string_view::npos) { + return std::nullopt; + } + return separator; +} + +/** @brief Returns the feature leaf selected by one ctrace reference path. */ +static std::string_view referenceLeaf(const std::string_view path) +{ + const auto separator = path.rfind('/'); + return separator == std::string_view::npos ? path : path.substr(separator + 1U); +} + +/** @brief Derives a processor name from a one-segment `[pname/]feature` reference path. */ +static std::optional referencePathProcessorName(const TraceRunReference& reference) +{ + const auto separator = processorFeatureSeparator(reference.ctraceRef); + if (!separator.has_value()) { + return std::nullopt; + } + + const auto leaf = std::string_view(reference.ctraceRef).substr(*separator + 1U); + if (!isProcessorScopedFeature(reference.type, leaf)) { + 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 +126,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 +157,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 +184,46 @@ static std::string configError(const TraceRunConfig& config, std::size_t line, c return location + ": " + message; } +/** @brief Preserves reader locations while locating programmatically supplied setup errors. */ +static std::string itmEnableError(const TraceRunConfig& config, const TraceRunSetup& setup) +{ + const auto& error = *setup.itm->enableError; + const auto pathLocation = config.path + ':'; + const auto lineLocation = config.path + '('; + if (!config.path.empty() && + (error.compare(0U, pathLocation.size(), pathLocation) == 0 || + error.compare(0U, lineLocation.size(), lineLocation) == 0)) { + return error; + } + return configError(config, setup.line, error); +} + +/** @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,172 +268,270 @@ 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. */ -static ProcessorIdentity processorIdentity(const TraceRunConfig& config, std::vector& warnings) -{ - // Active setups define the authoritative processor set when present. +/** @brief Stores one usable reference and its already validated processor name. */ +struct ProcessorReferenceEvidence { + const TraceRunReference* reference = nullptr; + std::optional name; +}; + +/** @brief Indexes processor evidence before resolving SINGLE trace identity rules. */ +struct ProcessorIdentityEvidence { std::set setupNames; - std::set> uniqueSetupNames; bool unnamedSetup = false; - std::size_t setupCount = 0U; - const TraceRunSetup* singleActiveSetup = nullptr; + std::vector references; + std::set referenceNames; + bool unnamedReference = false; + + std::size_t setupCount() const + { + return !setupNames.empty() ? setupNames.size() : (unnamedSetup ? 1U : 0U); + } +}; + +/** @brief Collects and validates all setup and reference processor evidence once. */ +static ProcessorIdentityEvidence collectProcessorIdentityEvidence(const TraceRunConfig& config) +{ + ProcessorIdentityEvidence evidence; 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); + evidence.setupNames.insert(*name); } else { - unnamedSetup = true; + evidence.unnamedSetup = true; } } - // 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); + evidence.references.push_back({&reference, name}); if (name.has_value()) { - referenceNames.insert(*name); + evidence.referenceNames.insert(*name); } else { - unnamedReference = true; + evidence.unnamedReference = true; } } + return evidence; +} - // Reconcile references according to the number and naming of active setups. - if (setupCount > 1U) { - if (unnamedSetup) { - throw std::runtime_error(config.path + - ": pname is required for every ctrace-setup in a multi-processor configuration"); +/** @brief Resolves identity when multiple named setup processors are active. */ +static ProcessorIdentity resolveMultiSetupProcessorIdentity(const TraceRunConfig& config, + const ProcessorIdentityEvidence& evidence, + std::vector& warnings) +{ + if (evidence.unnamedSetup) { + throw std::runtime_error(config.path + + ": pname is required for every ctrace-setup in a multi-processor configuration"); + } + + std::set matchingReferenceNames; + for (const auto& binding : evidence.references) { + if (!binding.name.has_value()) { + continue; } - for (const auto& reference : config.references) { - if (!isUsableStreamBinding(reference)) { - continue; - } - const auto name = TraceRunSchema::normalizedProcessorName(reference.processorName); - if (!name.has_value()) { + if (evidence.setupNames.find(*binding.name) == evidence.setupNames.end()) { + addRootInconsistency(warnings, + "ignoring ctrace-ref pname '" + *binding.name + "' because it has no matching ctrace-setup", + warningContext(*binding.reference)); + continue; + } + matchingReferenceNames.insert(*binding.name); + } + + if (matchingReferenceNames.size() != 1U) { + for (const auto& binding : evidence.references) { + if (!binding.name.has_value()) { addRootInconsistency(warnings, "ignoring ctrace-ref without pname because multiple ctrace-setup processors are active", - warningContext(reference)); - } else if (setupNames.find(*name) == setupNames.end()) { - addRootInconsistency(warnings, "ignoring ctrace-ref pname '" + *name + - "' because it has no matching ctrace-setup", - warningContext(reference)); + warningContext(*binding.reference)); } } - return {true, std::nullopt, true, setupNames}; + return {true, std::nullopt, true, evidence.setupNames}; } - if (setupCount == 1U) { - const auto setupName = TraceRunSchema::normalizedProcessorName(singleActiveSetup->processorName); - if (setupName.has_value()) { - for (const auto& reference : config.references) { - if (!isUsableStreamBinding(reference)) { - continue; - } - const auto name = TraceRunSchema::normalizedProcessorName(reference.processorName); - if (name.has_value() && *name != *setupName) { - addRootInconsistency(warnings, "ignoring ctrace-ref pname '" + *name + - "' because it does not match ctrace-setup pname '" + *setupName + "'", - warningContext(reference)); - } - } - return {false, setupName, true, setupNames}; + const auto selectedName = *matchingReferenceNames.begin(); + std::set selectedStreams; + for (const auto& binding : evidence.references) { + const auto& reference = *binding.reference; + if (binding.name.has_value() && *binding.name == selectedName && reference.stream.has_value()) { + selectedStreams.insert(*reference.stream); } - 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())}}); + } + for (const auto& binding : evidence.references) { + if (binding.name.has_value()) { + continue; + } + const auto& reference = *binding.reference; + 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, evidence.setupNames, selectedStreams}; +} + +/** @brief Resolves identity when exactly one setup processor group is active. */ +static ProcessorIdentity resolveSingleSetupProcessorIdentity(const TraceRunConfig& config, + const ProcessorIdentityEvidence& evidence, + std::vector& warnings) +{ + if (!evidence.setupNames.empty()) { + const auto setupName = *evidence.setupNames.begin(); + for (const auto& binding : evidence.references) { + if (binding.name.has_value() && *binding.name != setupName) { + addRootInconsistency(warnings, + "ignoring ctrace-ref pname '" + *binding.name + + "' because it does not match ctrace-setup pname '" + setupName + "'", + warningContext(*binding.reference)); + } } - return { - false, - referenceNames.size() == 1U ? std::optional(*referenceNames.begin()) : std::nullopt, - true, - {}, - }; + return {false, setupName, true, evidence.setupNames}; } - if (referenceNames.size() > 1U) { - if (unnamedReference) { + if (evidence.referenceNames.size() > 1U) { + throw std::runtime_error(config.path + + ": unformatted SINGLE trace requires one unambiguous processor metadata binding"); + } + const auto processorName = evidence.referenceNames.empty() + ? std::nullopt + : std::optional(*evidence.referenceNames.begin()); + return {false, processorName, true, {}}; +} + +/** @brief Resolves identity from references when no active setup constrains it. */ +static ProcessorIdentity resolveReferenceProcessorIdentity(const TraceRunConfig& config, + const ProcessorIdentityEvidence& evidence) +{ + if (evidence.referenceNames.size() > 1U) { + if (evidence.unnamedReference) { throw std::runtime_error(config.path + ": pname is required for every ctrace-ref in a multi-processor configuration"); } return {true, std::nullopt}; } - return { - false, - referenceNames.empty() ? std::nullopt : std::optional(*referenceNames.begin()), - false, - {}, - }; + const auto processorName = evidence.referenceNames.empty() + ? std::nullopt + : std::optional(*evidence.referenceNames.begin()); + return {false, processorName, false, {}}; } -/** @brief Resolves the data setup referenced by one DWT route. */ -static const TraceRunDataSetup* referencedDataSetup(const TraceRunConfig& config, const TraceRunReference& reference) +/** @brief Resolves the unambiguous processor identity of a trace-run file. */ +static ProcessorIdentity processorIdentity(const TraceRunConfig& config, std::vector& warnings) { - const auto index = reference.dataSetupIndex; - if (!index.has_value()) { - return nullptr; + const auto evidence = collectProcessorIdentityEvidence(config); + const auto setupCount = evidence.setupCount(); + if (setupCount > 1U) { + return resolveMultiSetupProcessorIdentity(config, evidence, warnings); } + if (setupCount == 1U) { + return resolveSingleSetupProcessorIdentity(config, evidence, warnings); + } + return resolveReferenceProcessorIdentity(config, evidence); +} + +/** @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; } - const auto* candidate = *index < setup.data.size() ? &setup.data[*index] : nullptr; - if (candidate != nullptr) { - return candidate; + 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; } + 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; + } + } + } + if (resolved.has_value()) { + resolved->size = effectiveSize; + } + if (conflict) { + resolved->size.reset(); + resolved->sizeError = "conflicting active ctrace-setup data.size values"; } - return nullptr; + 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) : nullptr; CtraceRunSourceMeta meta; meta.type = reference.type; - meta.processorName = processorIdentity.canonicalName(reference.processorName); - meta.traceBusId = static_cast(reference.stream.value_or(0U)); + 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.source = source; meta.label = reference.label; if (reference.type != "dwt") { @@ -296,7 +547,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; } @@ -316,20 +567,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; } @@ -338,296 +597,847 @@ 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 Tests whether processor candidates supply different explicit ITM masks. */ +static bool hasDistinctItmEnableMasks(const std::vector& processors) +{ + std::optional first; + for (const auto& processor : processors) { + if (!processor.itmEnableMask.has_value()) { + continue; + } + if (first.has_value() && first != processor.itmEnableMask) { + return true; + } + first = processor.itmEnableMask; + } + return false; +} -/** @brief Builds warning context for one resolved stream binding. */ -static std::vector> warningContext(const ResolvedStreamBinding& binding) +/** @brief Retains a common clock error or describes ambiguous SINGLE clock candidates. */ +static std::optional commonTimestampClockError(const std::vector& processors) { - 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)); + 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 Tests whether a ctrace reference uses the specified `[pname/]feature` path form. */ +static bool hasFeaturePath(const TraceRunReference& reference, const std::string_view& leaf) +{ + if (reference.ctraceRef == leaf) { + return true; } - if (binding.processorName.has_value()) { - context.emplace_back("pname", *binding.processorName); + const auto separator = processorFeatureSeparator(reference.ctraceRef); + if (!separator.has_value()) { + return false; } - return context; + return std::string_view(reference.ctraceRef).substr(*separator + 1U) == leaf; } -/** @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 Tests whether a reference path denotes the authoritative processor ITM anchor. */ +static bool hasProcessorItmPath(const TraceRunReference& reference) { - std::vector bindings; - for (const auto& reference : config.references) { - if (!isUsableStreamBinding(reference) || !processorIdentity.accepts(reference)) { - continue; - } - const auto processorName = processorIdentity.canonicalName(reference.processorName); - bindings.push_back({ - reference.line, - static_cast(reference.stream.value_or(0U)), - processorName, - reference.ctraceRef, - findProcessor(processors, processorName), - }); + 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 the fixed feature names that may establish a formatted route without an ITM anchor. */ +static bool isNamedFormattedRouteFallback(const std::string_view type, const std::string_view leaf) +{ + if (type == "itm") { + return leaf == "timestamps"; } - return bindings; + if (type == "exception") { + return leaf == "exceptions"; + } + if (type == "pcsample") { + return leaf == "pcsampling"; + } + if (type == "event" || type == "pmu") { + return hasDecimalSuffix(leaf, "events#"); + } + return false; } -static std::map -buildTimestampsByTraceBusId(const std::vector& bindings, - std::vector& warnings) +/** @brief Tests whether one reference is permitted to establish a formatted ITM route without an anchor. */ +static bool isFormattedRouteFallback(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 leaf = referenceLeaf(reference.ctraceRef); + if (!hasFeaturePath(reference, leaf)) { + return false; + } + if (reference.type == "dwt") { + if (leaf == "timestamps" || leaf == "synchronization") { + return true; } + return reference.dataSetupIndex.has_value() && hasDecimalSuffix(leaf, "data#"); + } + return isNamedFormattedRouteFallback(reference.type, leaf); +} + +/** @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 Indexes active setup fragments by their optional processor identity. */ +struct ActiveSetupIndex { + std::vector fragments; + std::set namedProcessors; + bool hasUnnamedProcessor = false; - 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)) { + 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; } - addRootInconsistency(warnings, - "ignoring conflicting ctrace-setup timestamps.clock assignment for CoreSight Trace Bus ID " + - std::to_string(binding.traceBusId), - warningContext(binding)); + 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 result; + return index; } -static std::map -buildTimestampPrescalersByTraceBusId(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) { - 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)); + 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")); } - return result; + 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")); } -/** @brief Returns the common ITM enable mask across relevant processors. */ -static std::optional commonItmEnableMask(const std::vector& processors) +/** @brief Tests whether a setup feature path resolves one reference within the same fragment. */ +static bool setupContainsReference(const TraceRunSetup& setup, const TraceRunReference& reference) { - std::optional common; - for (const auto& processor : processors) { - if (!processor.itmEnableMask.has_value()) { - return std::nullopt; + 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 (common.has_value() && common != processor.itmEnableMask) { - return std::nullopt; + 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; + } } - common = processor.itmEnableMask; } - return common; + 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 -buildItmEnableMasksByTraceBusId(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> 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)); + 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))); } } +} + +/** @brief Validates all routing-relevant fields retained from one formatted reference. */ +static void validateFormattedReference(const TraceRunConfig& config, const TraceRunReference& reference) +{ + 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 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::uint8_t traceBusId, + std::map& boundRoutes) +{ + if (!route.processorName.has_value()) { + return; + } + 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")); + } +} - std::map result; - for (const auto& [traceBusId, enableMask] : candidates) { - if (enableMask.has_value()) { - result.emplace(traceBusId, *enableMask); +/** @brief Adds compatible evidence to one formatted route or rejects an ID-to-processor conflict. */ +static CtraceRunRoute& 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, CtraceRunRoute{}); + auto& route = found->second; + if (inserted) { + route.processorName = processorName; + } else if (route.processorName.has_value() && processorName.has_value() && 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 (!route.processorName.has_value() && processorName.has_value()) { + route.processorName = processorName; + } + registerBoundRoute(config, reference, route, traceBusId, boundRoutes); + return route; +} + +/** @brief Resolves setup and source metadata for the formatted routes of one trace run. */ +class FormattedRouteMetadata final { +public: + /** @brief Binds the immutable input model and warning collector. */ + FormattedRouteMetadata(const TraceRunConfig& config, const ActiveSetupIndex& setups, + std::vector& warnings) + : m_config(config), + m_setups(setups), + m_warnings(warnings) + { + } + + /** @brief Applies compatible active setup fragments to one normalized route. */ + void applySetup(CtraceRunRoute& route) const + { + bool enableMaskConflict = false; + std::optional clockHz; + std::optional clockError; + std::optional prescaler; + std::optional enableMask; + + for (const auto* setup : setupFragments(route)) { + 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( + m_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( + m_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"); + } + if (setup->itm.has_value()) { + if (setup->itm->enableError.has_value()) { + throw std::runtime_error(itmEnableError(m_config, *setup)); + } + const auto candidateMask = setup->itm->enableMask; + if (!candidateMask.has_value()) { + continue; + } + if (enableMask.has_value() && *enableMask != *candidateMask) { + if (!enableMaskConflict) { + addRootInconsistency( + m_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; + } + } + + route.timestampPrescaler = prescaler.value_or(TraceRunSchema::kDefaultTimestampPrescaler); + if (!enableMaskConflict) { + route.itmEnableMask = enableMask; + } + route.timestampClockHz = clockHz; + route.timestampClockError = clockError; + } + + /** @brief Converts one validated reference into route-local source metadata. */ + CtraceRunSourceMeta source(const TraceRunReference& reference, std::uint32_t source, + const CtraceRunRoute& route) const + { + ProcessorIdentity identity; + identity.multipleProcessors = true; + auto boundReference = reference; + boundReference.processorName = route.processorName; + auto meta = sourceMeta(m_config, boundReference, source, identity); + meta.processorName = route.processorName; + meta.route = route.identity; + return meta; + } + +private: + /** @brief Returns setup fragments that unambiguously supply metadata for one route. */ + std::vector setupFragments(const CtraceRunRoute& route) const + { + std::vector matches; + if (route.processorName.has_value()) { + for (const auto* setup : m_setups.fragments) { + if (TraceRunSchema::normalizedProcessorName(setup->processorName) == route.processorName) { + matches.push_back(setup); + } + } + const auto unnamedCanBind = + m_setups.hasUnnamedProcessor && m_setups.namedProcessors.size() <= 1U && + (m_setups.namedProcessors.empty() || + m_setups.namedProcessors.find(*route.processorName) != m_setups.namedProcessors.end()); + if (!unnamedCanBind) { + return matches; + } + } else if (m_setups.processorGroupCount() != 1U || !m_setups.hasUnnamedProcessor) { + return matches; + } + + for (const auto* setup : m_setups.fragments) { + if (!TraceRunSchema::normalizedProcessorName(setup->processorName).has_value()) { + matches.push_back(setup); + } } + return matches; } - return result; + + const TraceRunConfig& m_config; + const ActiveSetupIndex& m_setups; + std::vector& m_warnings; +}; + +/** @brief Finds the established route described by resolved streamless processor evidence. */ +static std::optional streamlessRouteId( + const std::optional& processorName, const std::map& states, + const std::map& boundRoutes) +{ + if (processorName.has_value()) { + const auto bound = boundRoutes.find(*processorName); + if (bound != boundRoutes.end()) { + return bound->second; + } + if (states.size() != 1U || states.begin()->second.processorName.has_value()) { + return std::nullopt; + } + } + 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); + if (!route.processorName.has_value() && processorName.has_value()) { + route.processorName = processorName; + } + registerBoundRoute(config, reference, route, traceBusId, boundRoutes); +} + +/** @brief Retains formatted route state and the final route selected for every reference. */ +struct FormattedRouteBindings { + std::map routes; + std::map processorRoutes; + std::vector> referenceRoutes; +}; + +/** @brief Validates processor constraints imposed by active formatted setup fragments. */ +static void validateFormattedSetupProcessors(const TraceRunConfig& config, const ActiveSetupIndex& setups) +{ + 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; } -CtraceRunMeta CtraceRunMeta::fromConfig(const TraceRunConfig& config) +/** @brief Validates all routing-relevant formatted references before building shared state. */ +static void validateFormattedReferences(const TraceRunConfig& config) { - CtraceRunMeta ctraceRunMeta; - ctraceRunMeta.m_configPath = config.path; + for (const auto& reference : config.references) { + validateFormattedReference(config, reference); + } +} +/** @brief Registers authoritative formatted ITM route anchors. */ +static void registerFormattedRouteAnchors(const TraceRunConfig& config, const ActiveSetupIndex& setups, + FormattedRouteBindings& bindings) +{ for (const auto& reference : config.references) { - if (!TraceRunSchema::hasConsumedRouteShape(reference) && !TraceRunSchema::contributesStreamBinding(reference)) { - continue; + if (isProcessorItmAnchor(reference)) { + mergeFormattedRoute(config, reference, formattedProcessorName(config, setups, reference), bindings.routes, + bindings.processorRoutes); } - const auto problem = TraceRunSchema::referenceProblem(reference); - if (problem != ReferenceProblem::None && reference.error.empty()) { - throw std::runtime_error(referenceProblemMessage(config, reference, problem)); + } +} + +/** @brief Registers compatible formatted route fallbacks after every anchor. */ +static void registerFormattedRouteFallbacks(const TraceRunConfig& config, const ActiveSetupIndex& setups, + FormattedRouteBindings& bindings) +{ + for (const auto& reference : config.references) { + if (isFormattedRouteFallback(reference) && reference.stream.has_value()) { + mergeFormattedRoute(config, reference, formattedProcessorName(config, setups, reference), bindings.routes, + bindings.processorRoutes); } } - for (const auto& setup : config.setups) { - if (!consumesSetup(config, setup) || !setup.timestamps.has_value() || - !setup.timestamps->timestampPrescaler.has_value() || - TraceRunSchema::isTimestampPrescaler(*setup.timestamps->timestampPrescaler)) { +} + +/** @brief Rejects an empty or ambiguous formatted route set after anchor discovery. */ +static void validateFormattedRouteSet(const TraceRunConfig& config, const ActiveSetupIndex& setups, + const FormattedRouteBindings& bindings) +{ + if (setups.namedProcessors.empty() && setups.hasUnnamedProcessor && bindings.routes.size() > 1U) { + throw std::runtime_error(config.path + + ": one unnamed ctrace-setup processor cannot bind multiple formatted ITM routes"); + } + + if (bindings.routes.empty()) { + throw std::runtime_error(config.path + + ": formatted trace input requires an ITM route anchor or supported feature fallback"); + } +} + +/** @brief Resolves every compatible formatted reference to one established route. */ +static void bindFormattedReferences(const TraceRunConfig& config, const ActiveSetupIndex& setups, + FormattedRouteBindings& bindings) +{ + bindings.referenceRoutes.resize(config.references.size()); + for (std::size_t index = 0U; index < config.references.size(); ++index) { + const auto& reference = config.references[index]; + if (reference.stream.has_value()) { + const auto traceBusId = static_cast(*reference.stream); + if (bindings.routes.find(traceBusId) == bindings.routes.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), bindings.routes, + bindings.processorRoutes); + bindings.referenceRoutes[index] = traceBusId; + continue; + } + if (!describesFormattedRoute(reference)) { continue; } - throw std::runtime_error(configError(config, setup.timestamps->line, - setup.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")); - } - const auto identity = processorIdentity(config, ctraceRunMeta.m_warnings); - std::vector processors; + const auto processorName = formattedProcessorName(config, setups, reference); + const auto routeId = streamlessRouteId(processorName, bindings.routes, bindings.processorRoutes); + 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, bindings.routes, bindings.processorRoutes); + } - for (const auto& setup : config.setups) { - if (!consumesSetup(config, setup)) { + // Match materialization against the final processor bindings after every reference has been merged. + for (std::size_t index = 0U; index < config.references.size(); ++index) { + const auto& reference = config.references[index]; + if (reference.stream.has_value() || !describesFormattedRoute(reference)) { continue; } - const auto processorName = identity.canonicalName(setup.processorName); - auto& processor = processorMeta(processors, processorName); - if (setup.timestamps.has_value()) { - processor.timestampsEnabled = true; - processor.timestampClockHz = setup.timestamps->clockHz; - processor.timestampClockError = setup.timestamps->clockError; - processor.timestampPrescaler = - setup.timestamps->timestampPrescaler.value_or(TraceRunSchema::kDefaultTimestampPrescaler); - if (setup.timestamps->clockError.has_value()) { - ctraceRunMeta.m_timestampClockErrors.push_back(*setup.timestamps->clockError); + const auto processorName = formattedProcessorName(config, setups, reference); + bindings.referenceRoutes[index] = streamlessRouteId(processorName, bindings.routes, bindings.processorRoutes); + } +} + +/** @brief Materializes stable identities and source metadata for resolved formatted routes. */ +static std::vector materializeFormattedRoutes(const TraceRunConfig& config, + const ActiveSetupIndex& setups, + std::vector& warnings, + FormattedRouteBindings bindings) +{ + const FormattedRouteMetadata routeMetadata(config, setups, warnings); + std::vector routes; + routes.reserve(bindings.routes.size()); + std::uint32_t routeOrdinal = 0U; + for (auto& [traceBusId, route] : bindings.routes) { + route.identity = {TraceRouteId{routeOrdinal}, traceBusId}; + ++routeOrdinal; + routeMetadata.applySetup(route); + + for (std::size_t index = 0U; index < config.references.size(); ++index) { + const auto& reference = config.references[index]; + const auto routeId = bindings.referenceRoutes[index]; + if (!routeId.has_value() || *routeId != traceBusId || !TraceRunSchema::isUsableReference(reference)) { + continue; + } + for (const auto source : reference.sources) { + route.sources.push_back(routeMetadata.source(reference, source, route)); } } - if (setup.itm.has_value()) { - processor.itmEnableMask = setup.itm->enableMask; - } + routes.push_back(std::move(route)); } + return routes; +} +/** @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); + validateFormattedSetupProcessors(config, setups); + validateFormattedReferences(config); + + FormattedRouteBindings bindings; + // Anchors are authoritative, so register all of them before compatibility fallbacks. + registerFormattedRouteAnchors(config, setups, bindings); + registerFormattedRouteFallbacks(config, setups, bindings); + validateFormattedRouteSet(config, setups, bindings); + bindFormattedReferences(config, setups, bindings); + return materializeFormattedRoutes(config, setups, warnings, std::move(bindings)); +} + +/** @brief Validates routing-relevant fields retained from unformatted references. */ +static void validateUnformattedReferences(const TraceRunConfig& config) +{ for (const auto& reference : config.references) { - if (isUsableStreamBinding(reference) && identity.accepts(reference)) { - (void)processorMeta(processors, identity.canonicalName(reference.processorName)); + const auto problem = TraceRunSchema::referenceProblem(reference); + if (problem == ReferenceProblem::InvalidStream) { + throw std::runtime_error(referenceProblemMessage(config, reference, problem)); } - if (!TraceRunSchema::isUsableReference(reference) || !identity.accepts(reference)) { + if (!TraceRunSchema::hasConsumedRouteShape(reference) && !TraceRunSchema::contributesStreamBinding(reference)) { continue; } - for (const auto source : reference.sources) { - 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); - ctraceRunMeta.m_itmEnableMask = commonItmEnableMask(processors); - ctraceRunMeta.m_itmEnableMasksByTraceBusId = - buildItmEnableMasksByTraceBusId(streamBindings, ctraceRunMeta.m_warnings); - ctraceRunMeta.m_processorCount = processors.size(); - ctraceRunMeta.m_distinctProcessorPrescalers = containsDistinctProcessorPrescalers(processors); + if (problem != ReferenceProblem::None && !isDiscardableSourceProblem(reference, problem)) { + throw std::runtime_error(referenceProblemMessage(config, reference, problem)); + } + } +} - return ctraceRunMeta; +/** @brief Tests whether one setup contributes to the selected unformatted processor identity. */ +static bool isSelectedUnformattedSetup(const TraceRunConfig& config, const ProcessorIdentity& identity, + const TraceRunSetup& setup) +{ + return identity.acceptsSetup(setup) && consumesSetup(config, setup); } -const std::string& CtraceRunMeta::configPath() const +/** @brief Validates selected setup scalars before merging unformatted metadata. */ +static void validateUnformattedSetups(const TraceRunConfig& config, const ProcessorIdentity& identity) { - return m_configPath; + for (const auto& setup : config.setups) { + if (!isSelectedUnformattedSetup(config, identity, setup)) { + continue; + } + if (setup.timestamps.has_value() && setup.timestamps->timestampPrescaler.has_value() && + !TraceRunSchema::isTimestampPrescaler(*setup.timestamps->timestampPrescaler)) { + throw std::runtime_error( + configError(config, setup.timestamps->line, + setup.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 (setup.itm.has_value() && setup.itm->enableError.has_value()) { + throw std::runtime_error(itmEnableError(config, setup)); + } + } } -const std::optional& CtraceRunMeta::timestampClockHz() const +/** @brief Merges one selected setup's timestamp metadata into its processor accumulator. */ +static void mergeUnformattedTimestamps(const TraceRunConfig& config, const TraceRunSetup& setup, + ProcessorMeta& processor) { - return m_timestampClockHz; + if (!setup.timestamps.has_value()) { + return; + } + 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.timestampPrescaler = prescaler; } -const std::map& CtraceRunMeta::timestampsByTraceBusId() const +/** @brief Merges one selected setup's ITM mask into its processor accumulator. */ +static void mergeUnformattedItm(const TraceRunSetup& setup, const std::optional& processorName, + ProcessorMeta& processor, std::vector& warnings) { - return m_timestampsByTraceBusId; + if (!setup.itm.has_value() || !setup.itm->enableMask.has_value() || processor.itmEnableConflict) { + return; + } + if (processor.itmEnableMask.has_value() && processor.itmEnableMask != setup.itm->enableMask) { + processor.itmEnableConflict = true; + processor.itmEnableMask.reset(); + addRootInconsistency(warnings, + "ignoring conflicting ctrace-setup itm.enable values for unformatted SINGLE trace", + {{"pname", processorName.value_or("")}}); + return; + } + processor.itmEnableMask = setup.itm->enableMask; } -const std::optional& CtraceRunMeta::timestampPrescaler() const +/** @brief Accumulates metadata from all setups selected for an unformatted input. */ +static std::vector unformattedProcessors(const TraceRunConfig& config, const ProcessorIdentity& identity, + std::vector& warnings) { - return m_timestampPrescaler; + std::vector processors; + for (const auto& setup : config.setups) { + if (!isSelectedUnformattedSetup(config, identity, setup)) { + continue; + } + const auto processorName = identity.canonicalName(setup.processorName); + auto& processor = processorMeta(processors, processorName); + mergeUnformattedTimestamps(config, setup, processor); + mergeUnformattedItm(setup, processorName, processor, warnings); + } + return processors; } -const std::map& CtraceRunMeta::timestampPrescalersByTraceBusId() const +/** @brief Collects usable sources and reference-only processor candidates for an unformatted input. */ +static std::vector unformattedSources(const TraceRunConfig& config, + const ProcessorIdentity& identity, + std::vector& processors) { - return m_timestampPrescalersByTraceBusId; + std::vector sources; + for (const auto& reference : config.references) { + if (isUsableProcessorBinding(reference) && identity.accepts(reference)) { + (void)processorMeta(processors, identity.canonicalReferenceName(reference)); + } + if (!TraceRunSchema::isUsableReference(reference) || !identity.accepts(reference)) { + continue; + } + for (const auto source : reference.sources) { + sources.push_back(sourceMeta(config, reference, source, identity)); + } + } + return sources; } -const std::optional& CtraceRunMeta::itmEnableMask() const +/** @brief Materializes the one synthetic route used for unformatted SINGLE input. */ +static CtraceRunRoute makeUnformattedRoute(const TraceRunConfig& config, const std::vector& processors, + std::vector sources, + std::vector& warnings) { - return m_itmEnableMask; + const auto timestampPrescaler = commonTimestampPrescaler(processors); + if (!processors.empty() && !timestampPrescaler.has_value()) { + throw std::runtime_error( + config.path + ": unformatted SINGLE trace cannot choose between different timestamps.itm-prescaler values"); + } + const auto timestampClockHz = commonTimestampClock(processors); + const auto itmEnableMask = commonItmEnableMask(processors); + if (hasDistinctItmEnableMasks(processors)) { + addRootInconsistency( + warnings, + "ignoring different ctrace-setup itm.enable values across processor candidates for unformatted SINGLE trace"); + } + const auto clockError = commonTimestampClockError(processors); + + CtraceRunRoute syntheticRoute; + syntheticRoute.sources = std::move(sources); + syntheticRoute.timestampClockHz = timestampClockHz; + syntheticRoute.timestampPrescaler = + timestampPrescaler.value_or(TraceRunSchema::kDefaultTimestampPrescaler); + syntheticRoute.itmEnableMask = itmEnableMask; + syntheticRoute.timestampClockError = clockError; + if (processors.size() == 1U) { + syntheticRoute.processorName = processors.front().name; + syntheticRoute.timestampClockHz = processors.front().timestampClockHz; + syntheticRoute.timestampClockError = processors.front().timestampClockError; + syntheticRoute.timestampPrescaler = + processors.front().timestampPrescaler.value_or(TraceRunSchema::kDefaultTimestampPrescaler); + syntheticRoute.itmEnableMask = processors.front().itmEnableMask; + } + return syntheticRoute; } -const std::map& CtraceRunMeta::itmEnableMasksByTraceBusId() const +/** @brief Builds the synthetic route and warnings for an unformatted SINGLE input. */ +static CtraceRunRoute unformattedRoute(const TraceRunConfig& config, std::vector& warnings) { - return m_itmEnableMasksByTraceBusId; + validateUnformattedReferences(config); + const auto identity = processorIdentity(config, warnings); + validateUnformattedSetups(config, identity); + auto processors = unformattedProcessors(config, identity, warnings); + auto sources = unformattedSources(config, identity, processors); + return makeUnformattedRoute(config, processors, std::move(sources), warnings); } -const std::vector& CtraceRunMeta::timestampClockErrors() const +CtraceRunMeta CtraceRunMeta::fromConfig(const TraceRunConfig& config) { - return m_timestampClockErrors; + CtraceRunMeta ctraceRunMeta; + ctraceRunMeta.m_configPath = config.path; + ctraceRunMeta.m_traceFormat = config.traceFormat; + validateDisabledReferences(config); + + if (TraceRunSchema::effectiveTraceFormat(config.traceFormat) == TraceRunFormat::Formatted) { + ctraceRunMeta.m_routes = formattedRoutes(config, ctraceRunMeta.m_warnings); + } else { + ctraceRunMeta.m_routes.push_back(unformattedRoute(config, ctraceRunMeta.m_warnings)); + } + + return ctraceRunMeta; } -bool CtraceRunMeta::hasDistinctProcessorPrescalers() const +const std::string& CtraceRunMeta::configPath() const { - return m_distinctProcessorPrescalers; + return m_configPath; } -std::size_t CtraceRunMeta::processorCount() const +const std::optional& CtraceRunMeta::traceFormat() const { - return m_processorCount; + return m_traceFormat; } -const std::vector& CtraceRunMeta::sources() const +const std::vector& CtraceRunMeta::routes() const { - return m_sources; + return m_routes; } const std::vector& CtraceRunMeta::warnings() const diff --git a/tools/ctrace/src/tracerun/CtraceRunMeta.h b/tools/ctrace/src/tracerun/CtraceRunMeta.h index d6e1992d2..70c681fef 100644 --- a/tools/ctrace/src/tracerun/CtraceRunMeta.h +++ b/tools/ctrace/src/tracerun/CtraceRunMeta.h @@ -8,21 +8,22 @@ #ifndef CTRACE_SRC_TRACERUN_CTRACERUNMETA_H #define CTRACE_SRC_TRACERUN_CTRACERUNMETA_H -#include +#include "TraceRoute.h" + #include -#include #include #include #include #include struct TraceRunConfig; +enum class TraceRunFormat; /** @brief Stores normalized metadata for one trace source route. */ 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; @@ -33,70 +34,59 @@ struct CtraceRunSourceMeta { std::optional dataSizeError; }; -/** @brief Stores normalized timestamp metadata for one processor stream. */ -struct CtraceRunTimestampMeta { - std::optional processorName; - std::optional clockHz; - std::optional clockError; -}; - /** @brief Describes one non-fatal inconsistency between ctrace-setup and ctrace-refs. */ struct CtraceRunWarning { std::string message; std::vector> context; }; +/** @brief Describes one normalized ITM route and its processor metadata. */ +struct CtraceRunRoute { + TraceRouteIdentity identity; + std::optional processorName; + std::optional timestampClockHz; + std::optional timestampClockError; + std::uint32_t timestampPrescaler = 1U; + std::optional itmEnableMask; + std::vector sources; +}; + /** @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. - * @return Metadata indexed for decoder and output consumption. - * - * Ambiguous processor-wide values remain absent while per-stream values and - * validation errors are retained for diagnostics and output planning. + * @return Metadata containing the canonical decoder and output routes. */ static CtraceRunMeta fromConfig(const TraceRunConfig& config); /** @brief Returns the source trace-run configuration path. */ const std::string& configPath() const; - /** @brief Returns the unambiguous timestamp clock, if available. */ - const std::optional& timestampClockHz() const; - /** @brief Returns timestamp metadata indexed by Trace Bus ID. */ - const std::map& timestampsByTraceBusId() const; - /** @brief Returns the unambiguous ITM timestamp prescaler, if available. */ - const std::optional& timestampPrescaler() const; - /** @brief Returns timestamp prescalers indexed by Trace Bus ID. */ - const std::map& timestampPrescalersByTraceBusId() const; - /** @brief Returns the unambiguous ITM stimulus enable mask, if available. */ - const std::optional& itmEnableMask() const; - /** @brief Returns ITM stimulus enable masks indexed by Trace Bus ID. */ - const std::map& itmEnableMasksByTraceBusId() const; - /** @brief Returns clock validation errors retained for output planning. */ - const std::vector& timestampClockErrors() const; - /** @brief Reports whether processor-specific timestamp prescalers differ. */ - bool hasDistinctProcessorPrescalers() const; - /** @brief Returns the number of processors represented by the configuration. */ - std::size_t processorCount() const; - /** @brief Returns all normalized source routes. */ - const std::vector& sources() const; + /** @brief Returns the optional global byte-format declaration used during normalization. */ + const std::optional& traceFormat() const; + /** @brief Returns the normalized protocol-route catalogue. */ + const std::vector& routes() const; /** @brief Returns non-fatal inconsistencies ignored during normalization. */ const std::vector& warnings() const; private: + /** @brief Restricts construction to normalized instances returned by fromConfig(). */ + CtraceRunMeta() = default; + std::string m_configPath; - std::optional m_timestampClockHz; - std::map m_timestampsByTraceBusId; - std::optional m_timestampPrescaler; - std::map m_timestampPrescalersByTraceBusId; - std::optional m_itmEnableMask; - std::map m_itmEnableMasksByTraceBusId; - std::vector m_timestampClockErrors; - std::size_t m_processorCount = 0; - bool m_distinctProcessorPrescalers = false; - std::vector m_sources; + std::optional m_traceFormat; + std::vector m_routes; 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/TraceRunDiscovery.cpp b/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp index 30e903637..3d9269909 100644 --- a/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp +++ b/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp @@ -7,9 +7,12 @@ #include "TraceRunDiscovery.h" +#include "CoreSightFormatter.h" + #include #include #include +#include #include #include #include @@ -20,16 +23,72 @@ constexpr std::string_view ConfigSuffix = ".ctrace-run.yml"; +TraceRunInputDescriptor::TraceRunInputDescriptor(std::filesystem::path path, TraceRunFormat format, + CtraceRunMeta metadata, std::ifstream stream) + : m_path(std::move(path)), + m_format(format), + m_metadata(std::move(metadata)), + m_stream(std::move(stream)) +{ +} + +const std::filesystem::path& TraceRunInputDescriptor::path() const noexcept +{ + return m_path; +} + +TraceRunFormat TraceRunInputDescriptor::format() const noexcept +{ + return m_format; +} + +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 +185,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 +203,58 @@ 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 % CoreSightFormatter::kMemoryAlignedFrameSize != 0U) { + throw std::runtime_error("formatted raw trace input size must be a multiple of " + + std::to_string(CoreSightFormatter::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, format, std::move(metadata), std::move(readable)); +} diff --git a/tools/ctrace/src/tracerun/TraceRunDiscovery.h b/tools/ctrace/src/tracerun/TraceRunDiscovery.h index 25fe46c48..ae56265ba 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,49 @@ struct TraceRunRawInput { std::string channel; }; +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 effective global byte format. */ + TraceRunFormat format() 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, TraceRunFormat format, CtraceRunMeta metadata, + std::ifstream stream); + + std::filesystem::path m_path; + TraceRunFormat m_format = TraceRunFormat::Unformatted; + 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 +88,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/src/tracerun/YmlTraceRunConfigReader.cpp b/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp index e495aefc3..03a378486 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 = errorMessage(path, itmNode, "'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 = errorMessage(path, enableNode, "'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/data/.gitattributes b/tools/ctrace/test/data/.gitattributes index 63719c3ba..6bc417808 100644 --- a/tools/ctrace/test/data/.gitattributes +++ b/tools/ctrace/test/data/.gitattributes @@ -3,3 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 *.csv text eol=lf +*.py 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..b5e35b031 --- /dev/null +++ b/tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.SWO.traceanalysis.xml @@ -0,0 +1,111 @@ + + + + + + + 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 000000000..5db046c79 Binary files /dev/null and b/tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.ctf/stream_0 differ diff --git a/tools/ctrace/test/data/README.md b/tools/ctrace/test/data/README.md index 66dfb7ff1..7127f6477 100644 --- a/tools/ctrace/test/data/README.md +++ b/tools/ctrace/test/data/README.md @@ -10,24 +10,80 @@ by the reader and executable-level tests. Generated CSV/CTF outputs stay in the build tree and are not versioned, except for reference output used by an exact comparison test. +## Fixture integrity + +The [fixture manifest](../integration/src/ValidateFixtureIntegrity.cmake) is +the canonical SHA-256 and size inventory for checked-in fixtures, including +fixture-local provenance documents. `CtraceFixtureIntegrity` checks that the +inventory is complete and the reconstructed TB capture contains 256 frames. +Update the manifest in the same review as a fixture change. Inputs generated +at test runtime are defined and checked by the integration tests, not listed +in this manifest. + +## Blinky reference outputs + The Blinky fixture is stored under the generic `Blinky+Arm` target name. It was captured from a CMSIS project with CMSIS-Debugger 1.4.0 and pyTS 0.1.0, as recorded in the accompanying `ctrace-run` file. It contains SWO and TB input. The integration test compares the generated SWO CSV byte-for-byte with its -reference and verifies that TB is reported as a trace channel that is not -implemented yet. +reference and verifies that the coexisting TB input is excluded by the legacy +undeclared-format selection contract. The Blinky YAML, SWO capture, and TB capture are approved ctrace test assets and may be redistributed as part of Open-CMSIS-Pack/devtools. The reference CSV is derived from the SWO capture and is covered by the same approval and the repository-wide Apache-2.0 license terms. -The approved Blinky fixture set is identified by these SHA-256 values: +The `Blinky+Arm/expected` directory freezes the legacy SWO CTF and Trace Compass +output. The integration test adds the captured CM7 clock of 480 MHz to its +working copy of the legacy YAML, normalizes platform-dependent generated CRLF +line endings to LF while rejecting bare carriage returns, validates the +generated RFC 4122 UUID, and normalizes only that trace UUID to zero in the +metadata and packet headers before the byte-for-byte comparison. + +## Formatted multi-source inputs + +[TB-Trace](TB-Trace/README.md) reconstructs a memory-aligned CoreSight formatter +capture from the approved Blinky hardware payload. Its local README documents +all transformations, the manually added ctrace-private `trace-format` field, +deterministic regeneration, and independent deformatting/counterchecks. Its +Python tools validate formatter-ID and payload counters and are test-only. + +[formatted-synthetic](formatted-synthetic/README.md) complements that payload +with deterministic packet-family coverage on two routes: one authoritative +processor-ITM anchor and one constrained current-pyTS fallback. The integration +test generates its 128-byte raw input; only the YAML and documentation are +checked in. The local README records the exact routes, packet sequence, +generated raw hash, and test matrix. -- SWO capture: `f2de14241242697fa0948f1878850cce81575c404233c5c135aa68fc582dc72c` -- TB capture: `b0fccabe1a326ffe9fadf12d5c3a205d87628985e5e75a99da23c97d7f33d13b` -- Derived CSV: `6138cc60deee8bc16a8a889a6d9156ed76f389c4831afafc5125e4a0d00074cc` -- Trace-run YAML: `c9816183dde98ded93e57afd44312fb3026e3efdd1681f745bc03f7426713563` +## Generated negative and recovery inputs + +The integration test also creates focused formatted inputs as byte literals in +[CtraceIntegTests.cpp](../integration/src/CtraceIntegTests.cpp). They are +hand-authored from the CoreSight memory-aligned formatter and ITM packet +encodings; they are not hardware captures and make no claim about pyTS or +pyOCD producer output: + +- `Partial.TB.raw` is 15 arbitrary bytes and exists only to prove alignment + preflight before output creation. +- `Mixed.TB.raw` is two frames containing clean ID-1 ITM software packets and + two opaque ID-42 runs; it proves one warning and no guessed decoder/output + for an unsupported normal formatter ID. +- `Invalid.TB.raw` is one ID-1 frame containing ITM hardware sync followed by + reserved header `0x04`; it proves an unresolved route-local loss interval at + end of input. +- `Recovery.TB.raw` is three frames interleaving IDs 1 and 2. ID 2 contains a + reserved header, continues into the next frame without a repeated formatter + ID marker, then resynchronizes; it proves that reset and rollback stay local + while ID 1 and the deformatter retain state. +- `Unassigned.TB.raw` is one all-zero frame with payload before any formatter + source ID; it proves that an input-wide deformatter error aborts all outputs. + +These generated files exist only in each test's build-tree working directory. +Their canonical representation and expected semantics are the reviewed source +literals and assertions, so there are no separate fixture hashes or generators. + +## Other decoder fixtures The `Arm-reset` fixture is an approved excerpt of an Arm target capture. It starts at the hardware ITM sync immediately before an MCU-reset discontinuity @@ -37,18 +93,12 @@ sync, and continues decoding DWT events. The bounded excerpt keeps Debug tests portable across CI platforms. The trace-run YAML retains only metadata needed by the test. -- SWO capture: `8c7ba2b90e42188517c7b793e8b7dd4030fa5455b7a38a2de15d8ca2b47995c9` -- Trace-run YAML: `372e3bf3986fd6860dee5046920cbe129db6fd298c3e22468b3e374c09b8cf52` - The `trace-event` fixture combines two packet-aligned excerpts from an Arm Cortex-M7 SWO capture. The first excerpt contains mixed architectural DWT event counters. An explicit overflow and hardware sync separate it from a second excerpt dominated by `SLEEPCNT`. The integration test verifies CSV packet preservation and bitwise CTF expansion across the boundary. -- Raw capture excerpt: `97807dad2f69b1274df8960d3459426d1da4a6892d05e7623f3e16f06c5d85c8` -- Trace-run YAML: `a7b924d89854ac85e2751d1297ec78783fa12cb3fa54f5638691dd48d546a34e` - The `trace-match` fixture is completely synthetic. It was generated from the Armv8-M ITM and DWT packet definitions and was not captured from real hardware. It contains a hardware synchronization packet followed by one Data Trace Match @@ -56,10 +106,9 @@ packet for each comparator 0 through 3 and local timestamps. The integration test verifies the generated CSV rows, CTF records, labels, and Trace Compass timeline configuration. -- Generated raw trace: `5cffb5803675dc02ecd5ed4939a42c660ad7cabd3542b8ca1506230e20d14a50` -- Generated trace-run YAML: `b40c10634b8ba335b14b75f0026758ad84dd68aaf68f0a1bbfd2a5745756c5e8` +## Reader and entry-point inputs `trace-run` contains only the small current-schema inputs needed by executable tests. Reader unit tests cover only the fields consumed by ctrace. A C++ -entry-point test creates a reviewable eight-byte ITM stream below the build tree +entry-point test creates a reviewable 13-byte ITM stream below the build tree and verifies all output formats without an external fixture generator. diff --git a/tools/ctrace/test/data/TB-Trace/Blinky+Arm.TB.raw b/tools/ctrace/test/data/TB-Trace/Blinky+Arm.TB.raw new file mode 100644 index 000000000..515ba22f2 Binary files /dev/null and b/tools/ctrace/test/data/TB-Trace/Blinky+Arm.TB.raw differ diff --git a/tools/ctrace/test/data/TB-Trace/Blinky+Arm.ctrace-run.yml b/tools/ctrace/test/data/TB-Trace/Blinky+Arm.ctrace-run.yml new file mode 100644 index 000000000..783c86ae6 --- /dev/null +++ b/tools/ctrace/test/data/TB-Trace/Blinky+Arm.ctrace-run.yml @@ -0,0 +1,150 @@ +# Reconstructed from real STM32H747I-EVAL trace-buffer data. The formatter +# stream IDs and leading ITM synchronization packets are part of this fixture. +ctrace-run: + generated-by: ctrace reconstructed test fixture + trace-format: formatted + ctrace-setup: + - pname: CM4 + timestamps: + clock: 240000000 + itm-prescaler: 1 + data: + exceptions: + events: + itm: + enable: 0x00000000 + pcsampling: + period: 16384 + - pname: CM7 + timestamps: + clock: 480000000 + itm-prescaler: 1 + data: + - location: Blinky_cm7|osRtxInfo.kernel.tick + access: W + output: PC+value + size: 4 + exceptions: + events: + itm: + enable: 0x00000000 + pcsampling: + period: 16384 + synchronization: + DWT: 16M + ctrace-refs: + - ctrace-ref: CM4/timestamps + type: dwt + pname: CM4 + stream: 1 + regs: + - name: ITM_TCR + value: 0x00000003 + mask: 0x00000303 + - ctrace-ref: CM4/exceptions + type: exception + pname: CM4 + stream: 1 + regs: + - name: DWT_CTRL + value: 0x00010000 + mask: 0x00010000 + - name: ITM_TCR + value: 0x00000009 + mask: 0x00000009 + - ctrace-ref: CM4/itm + type: itm + pname: CM4 + stream: 1 + regs: + - name: ITM_TER0 + value: 0x00000000 + - name: ITM_TPR + value: 0x00000000 + mask: 0x0000000f + - name: ITM_TCR + value: 0x00010001 + mask: 0x007f0001 + - ctrace-ref: CM4/pcsampling + type: pcsample + pname: CM4 + stream: 1 + regs: + - name: DWT_CTRL + value: 0x0000121f + mask: 0x0000121f + - name: ITM_TCR + value: 0x00000009 + mask: 0x00000009 + - ctrace-ref: CM7/timestamps + type: dwt + pname: CM7 + stream: 2 + regs: + - name: ITM_TCR + value: 0x00000003 + mask: 0x00000303 + - ctrace-ref: CM7/data#0 + type: dwt + pname: CM7 + address: 0x24000024 + size: 4 + data-type: unsigned + source: 0 + stream: 2 + regs: + - name: DWT_COMP0 + value: 0x24000024 + - name: DWT_MASK0 + value: 0x00000002 + - name: DWT_FUNCTION0 + value: 0x0000000f + - name: ITM_TCR + value: 0x00000009 + mask: 0x00000009 + - ctrace-ref: CM7/exceptions + type: exception + pname: CM7 + stream: 2 + regs: + - name: DWT_CTRL + value: 0x00010000 + mask: 0x00010000 + - name: ITM_TCR + value: 0x00000009 + mask: 0x00000009 + - ctrace-ref: CM7/itm + type: itm + pname: CM7 + stream: 2 + regs: + - name: ITM_TER0 + value: 0x00000000 + - name: ITM_TPR + value: 0x00000000 + mask: 0x0000000f + - name: ITM_TCR + value: 0x00020001 + mask: 0x007f0001 + - ctrace-ref: CM7/pcsampling + type: pcsample + pname: CM7 + stream: 2 + regs: + - name: DWT_CTRL + value: 0x0000121f + mask: 0x0000121f + - name: ITM_TCR + value: 0x00000009 + mask: 0x00000009 + - ctrace-ref: CM7/synchronization + type: dwt + pname: CM7 + stream: 2 + regs: + - name: DWT_CTRL + value: 0x00000400 + mask: 0x00000c00 + - name: ITM_TCR + value: 0x00000005 + mask: 0x00000005 diff --git a/tools/ctrace/test/data/TB-Trace/README.md b/tools/ctrace/test/data/TB-Trace/README.md new file mode 100644 index 000000000..e56a782c5 --- /dev/null +++ b/tools/ctrace/test/data/TB-Trace/README.md @@ -0,0 +1,105 @@ +# Reconstructed multi-source Trace Bus fixture + +`Blinky+Arm.TB.raw` is a 4096-byte, memory-aligned CoreSight formatter capture reconstructed from the real hardware +capture in `../Blinky+Arm/Blinky+Arm.TB.raw`. It is intended for development and integration testing of formatted +multi-source input. It does not contain instruction trace. + +Canonical SHA-256 values: + +- source capture: `b0fccabe1a326ffe9fadf12d5c3a205d87628985e5e75a99da23c97d7f33d13b`; +- reconstructed capture: `aab49e56a07783b984fa7c6faeea101a51141423e66ba043dbd8d30702012639`; +- reconstruction tool: `8ce6ca54cedc216c04a03587b8388003a8ab0563e6c79c39ebf436d9bfcd0050`; +- analysis helper: `0ce65b99a2c51b2978cf0b790c653f5172715a1fa86521da8088fbd851ef9347`. + +The reconstruction preserves the order of all usable hardware payload bytes and their original formatter +interleaving. It makes the following deliberate changes: + +- drops four bytes that preceded the first formatter ID in the source capture; +- maps CM4 to Trace Bus ID 1 and CM7 to Trace Bus ID 2; +- prepends one valid ITM hardware synchronization packet to each source; +- removes the redundant terminal CM7 synchronization packet from the source capture; +- regenerates memory-aligned formatter frames and fills the remaining capacity with ID 0 NULL data. + +The resulting formatter stream has these properties: + +| Trace Bus ID | Processor | Payload bytes | Initial ITM sync | +| :--- | :--- | ---: | :--- | +| 0 | NULL | 7 | n/a | +| 1 | CM4 | 1488 | offset 0 | +| 2 | CM7 | 2093 | offset 0 | + +There are 256 formatter frames and 252 ID changes. No payload byte precedes the first formatter ID. + +The recorded independent-deformatter and current single-stream-decoder countercheck produced the following semantic +CSV rows without decoder errors: + +| Trace Bus ID | PC samples | Exception rows | DWT data rows | Total | +| :--- | ---: | ---: | ---: | ---: | +| 1 / CM4 | 129 | 84 | 0 | 213 | +| 2 / CM7 | 121 | 165 | 26 | 312 | + +`Blinky+Arm.ctrace-run.yml` follows the current per-processor setup and generated-reference structure. The +ctrace-private provisional root-level `trace-format: formatted` field selects CoreSight frame decoding. Ctrace +internally defaults formatted input to 16-byte memory-aligned framing; no public `trace-framing` field is assumed or +emitted. The declared processor clocks are fixture metadata and are not encoded in the raw trace. Normative format, +framing, and explicit file-association metadata remain to be specified by CMSIS-Toolbox and emitted by the producer +that knows the effective capture configuration. + +`regenerate_tb_trace.py` performs the documented reconstruction without reading the canonical output. It validates the +source hash and structure, removes the terminal CM7 synchronization bytes even though formatter interleaving separates +them, applies the source-ID mapping, prepends the CM4 and CM7 synchronization packets in that order, deterministically +packs memory-aligned frames, and adds exactly seven ID 0 bytes to retain the 4096-byte capture size. The tool validates +the output hash and a deformat/reformat round trip before writing it. + +From the repository root, regenerate and compare the canonical capture as follows: + +```sh +work_dir="$(mktemp -d)" +python3 tools/ctrace/test/data/TB-Trace/regenerate_tb_trace.py \ + --output "$work_dir/Blinky+Arm.TB.raw" +cmp "$work_dir/Blinky+Arm.TB.raw" \ + tools/ctrace/test/data/TB-Trace/Blinky+Arm.TB.raw +sha256sum "$work_dir/Blinky+Arm.TB.raw" +``` + +The command reports 4096 bytes, 256 frames, 252 formatter ID changes, payload lengths 7/1488/2093 for IDs 0/1/2, +and reconstructed SHA-256 `aab49e56a07783b984fa7c6faeea101a51141423e66ba043dbd8d30702012639`. + +`split_tb_trace.py` is the independent analysis helper. It deformats the fixture into one raw ITM file per valid Trace +Bus ID, allowing the unformatted decoder path to remain an implementation-independent semantic countercheck for the +combined formatted path: + +```sh +python3 tools/ctrace/test/data/TB-Trace/split_tb_trace.py \ + "$work_dir/Blinky+Arm.TB.raw" --output-dir "$work_dir/split" +``` + +The helper reports 4096 input bytes, 256 frames, 252 ID changes, no unassigned bytes, ID 0 with 7 bytes, ID 1 with +1488 bytes, and ID 2 with 2093 bytes. Both valid streams report their first ITM synchronization at offset 0. It +deliberately does not copy `Blinky+Arm.ctrace-run.yml`: that configuration describes the combined formatted input and +would be incorrect beside a demultiplexed unformatted stream. + +For a semantic countercheck, set `CTRACE` to a freshly built ctrace executable and give each demultiplexed stream a +minimal unformatted trace-run configuration: + +```sh +CTRACE=build/tools/ctrace//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. + +The executable integration test also decodes the canonical combined capture directly. It requires 213 CSV rows on +Trace Bus ID 1 and 312 on ID 2, no row or CTF file for ID-0 padding, one CTF stream per active ID, and two independent +clock declarations for the 240 MHz CM4 and 480 MHz CM7 routes. Because those clock domains have no specified common +origin, the valid CTF bundle deliberately has no companion Trace Compass XML and reports that limitation once. 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/data/formatted-synthetic/README.md b/tools/ctrace/test/data/formatted-synthetic/README.md new file mode 100644 index 000000000..e681dc75d --- /dev/null +++ b/tools/ctrace/test/data/formatted-synthetic/README.md @@ -0,0 +1,64 @@ + + +# Deterministic synthetic formatted fixture + +This fixture is completely synthetic. It was not captured from hardware and +does not represent output produced by pyTS, pyOCD, or another capture producer. +It exists to exercise packet families and routing cases that the reconstructed +`TB-Trace` hardware payload does not cover. + +The directory stores the canonical `Synthetic.ctrace-run.yml`. Integration +tests generate `Synthetic.TB.raw` below the build tree by calling +`syntheticFormattedCapture()` in `test/integration/src/CtraceIntegTests.cpp`. +That builder creates architectural ITM packets with +`FormattedTraceTestSupport.h` and packs them into 16-byte memory-aligned +CoreSight frames. Neither helper is part of the ctrace runtime. + +The deterministic generated capture has these properties: + +- size: 128 bytes in eight memory-aligned frames; +- formatter ID selections: three, consisting of the initial ID and two changes; +- Trace Bus ID 1: 58 payload bytes, beginning with ITM hardware sync; +- Trace Bus ID 2: 57 payload bytes, beginning with ITM hardware sync; +- formatter ID 0: two padding bytes and no semantic output; +- generated raw SHA-256: + `e8a62ad20f048385fde894ed1b869bdfb402feabf8a5e4d88283334a92674847`; +- trace-run SHA-256: + `a8370d26cd2f75264fc4f48fcdbf5fa2c9e1404c80a61c898dd30de8a44b91c6`. + +## Routes + +| Trace Bus ID | Processor | Binding evidence | Clock | Prescaler | Raw local increment | Decoded cycles | +| :--- | :--- | :--- | ---: | ---: | ---: | ---: | +| 1 | `anchored` | authoritative `anchored/itm` reference | 240 MHz | 1 | 240 | 240 | +| 2 | `fallback` | constrained current-pyTS `fallback/data#0` fallback | 480 MHz | 4 | 120 | 480 | + +Both routes therefore represent a one-microsecond local increment while +proving that prescaling happens exactly once after routing. They use different +PC, address, and global-timestamp values so incorrect cross-route state is +observable. + +## Packet coverage + +Each route contains the same packet-family sequence with route-specific values: + +- hardware synchronization; +- zero-valued ITM software payloads of widths 1, 2, and 4 on ports 1, 2, and 3; +- zero-valued DWT comparator payloads of widths 1, 2, and 4; +- a comparator-3 PC/address pair; +- an Armv8-M comparator-3 Data Trace Match packet; +- a periodic-PC-sample sleep indication; +- a DWT event-counter mask and a PMU trace-on-overflow mask; +- paired GTS1/GTS2 global timestamp packets; +- a local timestamp and an overflow packet. + +The resulting output covers ITM, DWT value/address/match, PC sample, DWT event, +PMU event, global timestamp, and trace-status CTF event families. The +reconstructed `TB-Trace` fixture independently covers real-hardware PC, +exception, and DWT-value traffic. The synthetic fixture also drives check, +CSV, CTF, and `--all`; stream/type filter combinations; absent/null clock +handling; route-normalization failures; explicit formatted SWO naming; +independent backend failures; and repeated-conversion cleanup. diff --git a/tools/ctrace/test/data/formatted-synthetic/Synthetic.ctrace-run.yml b/tools/ctrace/test/data/formatted-synthetic/Synthetic.ctrace-run.yml new file mode 100644 index 000000000..b5c07c118 --- /dev/null +++ b/tools/ctrace/test/data/formatted-synthetic/Synthetic.ctrace-run.yml @@ -0,0 +1,94 @@ +ctrace-run: + generated-by: ctrace deterministic synthetic formatted test + trace-format: formatted + ctrace-setup: + - pname: anchored + timestamps: + clock: 240000000 + itm-prescaler: 1 + data: + - size: 1 + - size: 2 + - size: 4 + - size: 4 + - pname: fallback + timestamps: + clock: 480000000 + itm-prescaler: 4 + data: + - size: 1 + - size: 2 + - size: 4 + - size: 4 + ctrace-refs: + # Authoritative processor-ITM anchor for Trace Bus ID 1. + - ctrace-ref: anchored/itm + type: itm + pname: anchored + stream: 1 + source: [1, 2, 3] + - ctrace-ref: anchored/data#0 + type: dwt + pname: anchored + stream: 1 + source: 0 + size: 1 + data-type: unsigned + - ctrace-ref: anchored/data#1 + type: dwt + pname: anchored + stream: 1 + source: 1 + size: 2 + data-type: unsigned + - ctrace-ref: anchored/data#2 + type: dwt + pname: anchored + stream: 1 + source: 2 + size: 4 + data-type: unsigned + - ctrace-ref: anchored/data#3 + type: dwt + pname: anchored + stream: 1 + source: 3 + size: 4 + data-type: unsigned + + # No fallback/itm anchor: data#0 is the permitted current-pyTS fallback + # that establishes Trace Bus ID 2. The other references describe content + # on the already established route. + - ctrace-ref: fallback/data#0 + type: dwt + pname: fallback + stream: 2 + source: 0 + size: 1 + data-type: unsigned + - ctrace-ref: fallback/data#1 + type: dwt + pname: fallback + stream: 2 + source: 1 + size: 2 + data-type: unsigned + - ctrace-ref: fallback/data#2 + type: dwt + pname: fallback + stream: 2 + source: 2 + size: 4 + data-type: unsigned + - ctrace-ref: fallback/data#3 + type: dwt + pname: fallback + stream: 2 + source: 3 + size: 4 + data-type: unsigned + - ctrace-ref: fallback/messages + type: itm + pname: fallback + stream: 2 + source: [1, 2, 3] diff --git a/tools/ctrace/test/integration/CMakeLists.txt b/tools/ctrace/test/integration/CMakeLists.txt index 7012958ff..bf885e027 100644 --- a/tools/ctrace/test/integration/CMakeLists.txt +++ b/tools/ctrace/test/integration/CMakeLists.txt @@ -41,6 +41,17 @@ set_tests_properties(CtraceIntegTests PROPERTIES TIMEOUT 120 ) +add_test( + NAME CtraceFixtureIntegrity + COMMAND "${CMAKE_COMMAND}" + "-DFIXTURE_ROOT=${PROJECT_SOURCE_DIR}/test/data" + -P "${CMAKE_CURRENT_SOURCE_DIR}/src/ValidateFixtureIntegrity.cmake" +) +set_tests_properties(CtraceFixtureIntegrity PROPERTIES + LABELS "integration" + TIMEOUT 30 +) + add_test( NAME ctrace-version COMMAND ctrace --version @@ -91,3 +102,26 @@ set_tests_properties(ctrace-windows-manifest PROPERTIES LABELS "integration" TIMEOUT 30 ) + +if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND NOT CMAKE_CROSSCOMPILING) + find_program(BABELTRACE2_EXECUTABLE NAMES babeltrace2) + if(BABELTRACE2_EXECUTABLE) + add_test( + NAME CtraceBabeltrace2Consumer + COMMAND "${CMAKE_COMMAND}" + "-DCTRACE_EXECUTABLE=$" + "-DBABELTRACE2_EXECUTABLE=${BABELTRACE2_EXECUTABLE}" + "-DFIXTURE_DIRECTORY=${PROJECT_SOURCE_DIR}/test/data/TB-Trace" + "-DBUILD_ROOT=${CMAKE_BINARY_DIR}" + "-DTEST_WORK_DIRECTORY=${CMAKE_CURRENT_BINARY_DIR}/babeltrace2-consumer" + -P "${CMAKE_CURRENT_SOURCE_DIR}/src/ValidateBabeltrace2Consumer.cmake" + ) + set_tests_properties(CtraceBabeltrace2Consumer PROPERTIES + ENVIRONMENT "LC_ALL=C" + LABELS "linux-consumer" + TIMEOUT 120 + ) + else() + message(STATUS "babeltrace2 not found; CtraceBabeltrace2Consumer is not registered") + endif() +endif() diff --git a/tools/ctrace/test/integration/README.md b/tools/ctrace/test/integration/README.md index a890fc895..722ae5e76 100644 --- a/tools/ctrace/test/integration/README.md +++ b/tools/ctrace/test/integration/README.md @@ -11,5 +11,117 @@ workflows with fixtures from `test/data` and writes generated output only under the CMake build directory. Small CTest smoke tests separately cover the platform executable and Windows manifest. -Fixture provenance and the scenarios covered by each capture are documented in -the [test-data README](../data/README.md). +Fixture provenance and the scenarios covered by each checked-in capture and +inline-generated formatted input are documented in the +[test-data README](../data/README.md). + +## Babeltrace consumer gate + +`CtraceBabeltrace2Consumer` is a separately labelled native-Linux CTest. The CI +jobs install Ubuntu package revision `2.0.5-3build2`, and the test itself +requires the consumer to report exactly Babeltrace `2.0.5`. CI invokes the +`linux-consumer` label with `--no-tests=error`, so a missing registration is a +failure rather than a silent skip. + +The test generates the two-clock `TB-Trace` CTF bundle and reads each stream in +an isolated metadata-plus-one-stream directory. It verifies that Babeltrace +scales a 25,729-tick stream-1 sample at 240 MHz to `0.000107204` seconds and a +7,603-tick stream-2 sample at 480 MHz to `0.000015839` seconds. It then verifies +that Babeltrace's default whole-bundle mux rejects the two distinct clock UUIDs +instead of inventing a global event order. + +## Trace Compass acceptance + +The data-driven XML output was accepted on 2026-09-11 against Trace Compass +Server `0.17.0`, build `202609101143`, and TSP `0.6.0`. The server bundle +manifest identifies source commit +`b626f3c61f8d0dac15451c7663aa36d9bf3db33e`; the relevant installed bundles +were TMF Core `10.2.0`, CTF Core `5.1.0`, TMF CTF Core `5.0.2`, and XML Core +`4.3.2`. This is a dated acceptance record, not a pinned runtime dependency or +general compatibility matrix. The identity query was: + +```sh +curl -sS http://127.0.0.1:8080/tsp/api/identifier +``` + +Every scenario ran in isolation. Its XML configuration, trace, and experiment +were removed before the next scenario, and the server was restarted to clear +the XML analysis registry that persists beyond the REST `DELETE`. Indexing and +analysis requests were repeated until their response status was `COMPLETED`. + +### Single-clock DWT match + +The generated `trace-match.ctf` and `trace-match.SWO.traceanalysis.xml` from +`ConvertsDwtMatchAcrossCsvAndCtf` were registered through: + +```http +POST /tsp/api/config/types/org.eclipse.tracecompass.tmf.core.config.xmlsourcetype/configs +POST /tsp/api/traces +POST /tsp/api/experiments +``` + +Experiment `55d09123-c1a6-3736-89b4-d38201b70fcb` exposed exactly seven events +from 0 through 10,000 ns. `GET /tsp/api/experiments//outputs` exposed +exactly one generated graphical provider: + +```text +arm.cmsis.swo.tg.dwt_match.v1 +SWO Trace Analysis: DWT Match +``` + +The synthetic exception bootstrap remained available in the event table but +did not create an exception view. The following semantic queries returned four +`DWT_MATCH` children and active states beginning at 1,000, 3,000, 6,000, and +10,000 ns: + +```http +POST /tsp/api/experiments//outputs/timeGraph/arm.cmsis.swo.tg.dwt_match.v1/tree +{"parameters":{}} + +POST /tsp/api/experiments//outputs/timeGraph/arm.cmsis.swo.tg.dwt_match.v1/states +{"parameters":{"requested_timerange":{"start":0,"end":10999,"nbTimes":100},"requested_items":[1,2,3,4]}} +``` + +### Single-source CM4 + +The reconstructed TB fixture was copied to a temporary directory and converted +with the release executable: + +```sh +ctrace --target Blinky+Arm --all --stream 1 +``` + +Its CTF and XML were loaded as experiment +`39f5a594-7467-31fb-a83e-75004f2d0076`. It exposed exactly 244 events from 0 +through 8,844,454 ns and only this generated graphical provider: + +```text +arm.cmsis.swo.tg.exception.stream1.v1 +SWO Trace Analysis: EXCEPTION - CM4 +``` + +The visible name contained the resolved processor name but no numeric trace ID; +ordinary PC samples remained event-table data. Event-table index 3 was the +25,729-tick sample scaled to 107,204 ns on `stream_1`, with context +`[cmsis_trace_bus_id=1, ctrace_route=CM4]`. The exception tree contained +`Thread Mode`, `Exception Return`, and `SysTick`; querying the three +server-assigned child IDs returned exactly 29 states for each lane: + +```http +POST /tsp/api/experiments//outputs/timeGraph/arm.cmsis.swo.tg.exception.stream1.v1/states +{"parameters":{"requested_timerange":{"start":0,"end":8844454,"nbTimes":1000},"requested_items":[6,7,8]}} +``` + +### Multi-clock TB trace + +The complete CTF bundle from `ConvertsReconstructedFormattedTraceBusFixture` +was loaded without an XML configuration. Experiment +`788ec3a1-8d07-375f-8fa2-24f30ea41281` exposed 614 events: 244 on +`stream_1`/CM4 and 370 on `stream_2`/CM7. The event contexts were +`[cmsis_trace_bus_id=1, ctrace_route=CM4]` and +`[cmsis_trace_bus_id=2, ctrace_route=CM7]`. + +No companion `Blinky+Arm.TB.traceanalysis.xml` existed, the server's XML +configuration list was empty, and its output list contained no +`arm.cmsis.swo.*` provider. Trace Compass therefore imported all event-table +data without constructing an invalid cross-clock graphical timeline. diff --git a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp index 4372647ad..931287bef 100644 --- a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp +++ b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp @@ -7,6 +7,8 @@ #include "CtraceMain.h" #include "CtfTestSupport.h" +#include "FormattedTraceTestSupport.h" +#include "TestSupport.h" #include @@ -16,14 +18,14 @@ #include #include #include -#include +#include #include -#include -#include +#include #include #include #include #include +#include #include namespace { @@ -79,46 +81,211 @@ class CtraceIntegTests : public testing::Test { std::filesystem::path m_workDirectory; }; -void writeFile(const std::filesystem::path& path, const std::string& contents = {}) +void expectNonEmptyFile(const std::filesystem::path& path) +{ + ASSERT_TRUE(std::filesystem::is_regular_file(path)) << path; + EXPECT_GT(std::filesystem::file_size(path), 0U) << path; +} + +void expectContains(std::string_view text, std::string_view expected) { - std::ofstream output(path, std::ios::binary | std::ios::trunc); - ASSERT_TRUE(output) << path; - output.write(contents.data(), static_cast(contents.size())); - ASSERT_TRUE(output) << path; + EXPECT_NE(std::string_view::npos, text.find(expected)) << expected << "\n" << text; } -std::string readTextFile(const std::filesystem::path& path) +void expectNotContains(std::string_view text, std::string_view unexpected) { - std::ifstream input(path, std::ios::binary); - if (!input) { - throw std::runtime_error("failed to read test file: " + path.string()); + EXPECT_EQ(std::string_view::npos, text.find(unexpected)) << unexpected << "\n" << text; +} + +void appendBytes(std::vector& destination, const std::vector& source) +{ + destination.insert(destination.end(), source.begin(), source.end()); +} + +/** @brief Builds one deterministic ITM byte stream covering formatted-output packet families. */ +std::vector syntheticFormattedRoute(std::uint32_t timestampIncrement, std::uint64_t globalTimestamp, + std::uint32_t pc, std::uint16_t address) +{ + using namespace FormattedTraceTestSupport; + + auto bytes = itmHardwareSync(); + std::uint8_t channel = 1U; + for (const auto width : {1U, 2U, 4U}) { + appendBytes(bytes, itmSoftwarePacket(channel++, static_cast(width), 0U)); } - return {std::istreambuf_iterator(input), std::istreambuf_iterator()}; + appendBytes(bytes, itmHardwarePacket(16U, 1U, 0U)); + appendBytes(bytes, itmHardwarePacket(18U, 2U, 0U)); + appendBytes(bytes, itmHardwarePacket(20U, 4U, 0U)); + appendBytes(bytes, itmHardwarePacket(14U, 4U, pc)); + appendBytes(bytes, itmHardwarePacket(15U, 2U, address)); + appendBytes(bytes, itmHardwarePacket(14U, 1U, 1U)); + appendBytes(bytes, itmHardwarePacket(2U, 1U, 0U)); + appendBytes(bytes, itmHardwarePacket(0U, 1U, 0x21U)); + appendBytes(bytes, itmHardwarePacket(3U, 1U, 0x81U)); + appendBytes(bytes, itmGlobalTimestampPacket(globalTimestamp)); + appendBytes(bytes, itmLocalTimestampPacket(timestampIncrement)); + appendBytes(bytes, itmOverflowPacket()); + return bytes; } -std::vector readBinaryFile(const std::filesystem::path& path) +constexpr std::uint64_t kAnchoredGlobalTimestamp = 0x1020304c00f23456ULL; +constexpr std::uint64_t kFallbackGlobalTimestamp = 0x2030405000123456ULL; + +std::size_t countCsvStreamRows(std::string_view csv, std::string_view stream); + +/** @brief Builds the common two-route CoreSight-formatted integration capture. */ +std::vector syntheticFormattedCapture() { - std::ifstream input(path, std::ios::binary); - if (!input) { - throw std::runtime_error("failed to read binary test file: " + path.string()); + const auto anchored = syntheticFormattedRoute(240U, kAnchoredGlobalTimestamp, 0x08001000U, 0x1000U); + const auto fallback = syntheticFormattedRoute(120U, kFallbackGlobalTimestamp, 0x08002000U, 0x2000U); + return FormattedTraceTestSupport::memoryAlignedFrames({{1U, anchored}, {2U, fallback}}); +} + +/** @brief Writes one independent test case using the common synthetic formatted capture. */ +void writeSyntheticFormattedFixture(const std::filesystem::path& directory, const std::string& traceRun, + std::string_view rawChannel = "TB") +{ + std::error_code error; + std::filesystem::create_directories(directory, error); + ASSERT_FALSE(error) << directory << ": " << error.message(); + writeTestFile(directory / "Synthetic.ctrace-run.yml", traceRun); + const auto raw = syntheticFormattedCapture(); + ASSERT_FALSE(raw.empty()); + ASSERT_EQ(raw.size() % 16U, 0U); + writeTestFile(directory / ("Synthetic." + std::string(rawChannel) + ".raw"), + {reinterpret_cast(raw.data()), raw.size()}); +} + +/** @brief Replaces every required occurrence in deterministic fixture text. */ +void replaceFixtureText(std::string& text, std::string_view from, std::string_view to) +{ + std::size_t replacements = 0U; + for (auto position = text.find(from); position != std::string::npos; + position = text.find(from, position + to.size())) { + text.replace(position, from.size(), to); + ++replacements; } - return {std::istreambuf_iterator(input), std::istreambuf_iterator()}; + ASSERT_GT(replacements, 0U) << from; } -void expectNonEmptyFile(const std::filesystem::path& path) +void expectSyntheticCsvRoute(std::string_view csv, std::uint8_t stream, std::uint64_t cycles, std::uint32_t pc, + std::uint16_t address, std::uint64_t globalTimestamp) { - ASSERT_TRUE(std::filesystem::is_regular_file(path)) << path; - EXPECT_GT(std::filesystem::file_size(path), 0U) << path; + const auto prefix = std::to_string(cycles) + "," + std::to_string(stream) + ","; + for (const auto& expected : { + prefix + "itm,1,0x00,,,\n", + prefix + "itm,2,0x0000,,,\n", + prefix + "itm,3,0x00000000,,,\n", + prefix + "dwt,0,0x00,,,\n", + prefix + "dwt,1,0x0000,,,\n", + prefix + "dwt,2,0x00000000,,,\n", + prefix + "pcsample,,,,,\n", + prefix + "event,0,0x21,,,\n", + prefix + "pmu,3,0x81,,,\n", + std::to_string(globalTimestamp) + "," + std::to_string(stream) + ",global_ts,,,,,\n", + }) { + expectContains(csv, expected); + } + + std::ostringstream addressRow; + addressRow << prefix << "dwt,3,,0x" << std::hex << std::setfill('0') << std::setw(8) << pc << ",0x" << std::setw(4) + << address << ",\n"; + expectContains(csv, addressRow.str()); + expectContains(csv, prefix + "dwt,3,,,,\n"); + expectContains(csv, + prefix + "overflow,,,,,overflow: new timestamp segment; time across boundary may be unreliable\n"); } -void expectContains(std::string_view text, std::string_view expected) +void expectSyntheticCtfRoute(const std::filesystem::path& streamPath, std::uint8_t traceBusId, + std::uint64_t payloadTimestamp) { - EXPECT_NE(std::string_view::npos, text.find(expected)) << expected << "\n" << text; + const auto records = CtfTestSupport::readCtfRecords(streamPath, CtfStreamWriter::EventContextLayout::RouteLabeled); + ASSERT_FALSE(records.empty()); + + std::array eventIds{}; + std::array statusReasons{}; + for (const auto& record : records) { + ASSERT_EQ(record.traceBusId, traceBusId); + ASSERT_LT(record.id, eventIds.size()); + eventIds[record.id] = true; + if (record.id == CtfSchema::value(CtfSchema::EventId::TraceStatus)) { + ASSERT_FALSE(record.payload.empty()); + ASSERT_LT(record.payload.front(), statusReasons.size()); + statusReasons[record.payload.front()] = true; + } + if (record.id == CtfSchema::value(CtfSchema::EventId::PcSample)) { + ASSERT_EQ(record.payload.size(), 6U); + EXPECT_EQ(record.payload.front(), CtfSchema::value(CtfSchema::PcSampleState::Sleep)); + } + if (record.id == CtfSchema::value(CtfSchema::EventId::Itm) || + record.id == CtfSchema::value(CtfSchema::EventId::DwtValue) || + record.id == CtfSchema::value(CtfSchema::EventId::DwtAddress) || + record.id == CtfSchema::value(CtfSchema::EventId::DwtMatch) || + record.id == CtfSchema::value(CtfSchema::EventId::PcSample) || + record.id == CtfSchema::value(CtfSchema::EventId::DwtEvent) || + record.id == CtfSchema::value(CtfSchema::EventId::PmuEvent)) { + EXPECT_EQ(record.timestamp, payloadTimestamp); + } + } + + for (const auto id : + {CtfSchema::EventId::Itm, CtfSchema::EventId::DwtValue, CtfSchema::EventId::DwtAddress, + CtfSchema::EventId::TraceStatus, CtfSchema::EventId::GlobalTimestamp, CtfSchema::EventId::PcSample, + CtfSchema::EventId::DwtEvent, CtfSchema::EventId::PmuEvent, CtfSchema::EventId::DwtMatch}) { + EXPECT_TRUE(eventIds[CtfSchema::value(id)]) << CtfSchema::eventName(id); + } + for (const auto reason : {CtfSchema::TraceStatusReason::TraceStart, CtfSchema::TraceStatusReason::Resync, + CtfSchema::TraceStatusReason::Overflow}) { + EXPECT_TRUE(statusReasons[CtfSchema::value(reason)]); + } } -void expectNotContains(std::string_view text, std::string_view unexpected) +/** @brief Verifies the complete semantic CSV content of the common synthetic capture. */ +void expectCompleteSyntheticCsv(const std::filesystem::path& csvPath) { - EXPECT_EQ(std::string_view::npos, text.find(unexpected)) << unexpected << "\n" << text; + const auto csv = readTestTextFile(csvPath); + expectSyntheticCsvRoute(csv, 1U, 240U, 0x08001000U, 0x1000U, kAnchoredGlobalTimestamp); + expectSyntheticCsvRoute(csv, 2U, 480U, 0x08002000U, 0x2000U, kFallbackGlobalTimestamp); + EXPECT_EQ(countCsvStreamRows(csv, "1"), 13U); + EXPECT_EQ(countCsvStreamRows(csv, "2"), 13U); + EXPECT_EQ(countCsvStreamRows(csv, "0"), 0U); +} + +/** @brief Requires all CTF records to use, and to cover, exactly the listed event families. */ +void expectOnlyCtfEventIds(const std::filesystem::path& streamPath, + std::initializer_list expectedIds) +{ + const auto records = CtfTestSupport::readCtfRecords(streamPath, CtfStreamWriter::EventContextLayout::RouteLabeled); + ASSERT_FALSE(records.empty()); + for (const auto& record : records) { + EXPECT_TRUE(std::any_of(expectedIds.begin(), expectedIds.end(), [&](const auto id) { + return record.id == CtfSchema::value(id); + })) << record.id; + } + for (const auto id : expectedIds) { + EXPECT_TRUE(std::any_of(records.begin(), records.end(), [&](const auto& record) { + return record.id == CtfSchema::value(id); + })) << CtfSchema::eventName(id); + } +} + +/** @brief Verifies the complete output artifact set for the multi-clock synthetic fixture. */ +void expectSyntheticArtifacts(const std::filesystem::path& directory, bool csvExpected, bool ctfExpected) +{ + EXPECT_EQ(std::filesystem::is_regular_file(directory / "Synthetic.TB.csv"), csvExpected); + EXPECT_EQ(std::filesystem::is_directory(directory / "Synthetic.ctf"), ctfExpected); + EXPECT_FALSE(std::filesystem::exists(directory / "Synthetic.TB.traceanalysis.xml")); + if (!ctfExpected) { + return; + } + + std::vector files; + for (const auto& entry : std::filesystem::directory_iterator(directory / "Synthetic.ctf")) { + ASSERT_TRUE(entry.is_regular_file()) << entry.path(); + files.push_back(entry.path().filename().string()); + } + std::sort(files.begin(), files.end()); + EXPECT_EQ(files, (std::vector{"metadata", "stream_1", "stream_2"})); } std::size_t countOccurrences(std::string_view text, std::string_view value) @@ -131,9 +298,144 @@ 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; + 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: + writeTestFile(workDirectory() / "Minimal.ctrace-run.yml", R"yml(ctrace-run: ctrace-setup: - timestamps: clock: 400000000 @@ -141,22 +443,253 @@ TEST_F(CtraceIntegTests, GeneratesAllOutputs) )yml"); const std::string raw{"\0\0\0\0\0\x80\x17\x34\x12\x00\x08\x09\x41", 13U}; - writeFile(workDirectory() / "Minimal.SWO.raw", raw); + writeTestFile(workDirectory() / "Minimal.SWO.raw", raw); const auto result = run({"ctrace", workDirectory().string(), "--target", "Minimal", "--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() / "Minimal.SWO.csv")); + readTestTextFile(workDirectory() / "Minimal.SWO.csv")); expectNonEmptyFile(workDirectory() / "Minimal.ctf" / "metadata"); expectNonEmptyFile(workDirectory() / "Minimal.ctf" / "stream_0"); expectNonEmptyFile(workDirectory() / "Minimal.SWO.traceanalysis.xml"); } +TEST_F(CtraceIntegTests, DecodesExplicitUnformattedNamedTraceBuffer) +{ + writeTestFile(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}; + writeTestFile(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", + readTestTextFile(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, ExcludesUnformattedRawSwoWithoutRequiringClock) +{ + writeTestFile(workDirectory() / "Filtered.ctrace-run.yml", R"yml(ctrace-run: + ctrace-setup: + - timestamps: + itm-prescaler: 1 + ctrace-refs: [] +)yml"); + + const std::string raw{"\0\0\0\0\0\x80\x17\x34\x12\x00\x08\x09\x41", 13U}; + writeTestFile(workDirectory() / "Filtered.SWO.raw", raw); + + const auto result = run({"ctrace", workDirectory().string(), "--target", "Filtered", "--ctf", "--stream", "1"}); + EXPECT_EQ(0, result.exitCode) << result.stderrText; + expectNotContains(result.stderrText, "timestamps.clock"); + + const auto ctfDirectory = workDirectory() / "Filtered.ctf"; + const auto metadata = readTestTextFile(ctfDirectory / "metadata"); + EXPECT_FALSE(metadata.empty()); + expectNotContains(metadata, "\nclock {"); + expectNotContains(metadata, "\nstream {"); + expectNotContains(metadata, "\nevent {"); + EXPECT_FALSE(std::filesystem::exists(ctfDirectory / "stream_0")); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Filtered.SWO.traceanalysis.xml")); +} + +TEST_F(CtraceIntegTests, RejectsPartialFormattedFrameBeforeCreatingArtifacts) +{ + writeTestFile(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"); + writeTestFile(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, SkipsUnsupportedFormattedSourceOnceAndKeepsConfiguredRoute) +{ + writeTestFile(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, + }}; + writeTestFile(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", + readTestTextFile(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, PublishesOutputsWithUnresolvedFormattedRouteRecovery) +{ + writeTestFile(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, + }}; + writeTestFile(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"); + expectContains(result.stderrText, "could not be decoded before the next hardware ITM sync"); + EXPECT_EQ("cycles,stream,type,source,value,pc,address,note\n" + "0,1,error,,,,,OpenCSD detected an invalid ITM packet header at raw offset 6.\n" + "0,1,error,,,,,OpenCSD discarded 10 raw bytes for this ITM route; no later hardware sync before end of " + "input; timestamp 0 .. unknown.\n", + readTestTextFile(workDirectory() / "Invalid.TB.csv")); + expectNonEmptyFile(workDirectory() / "Invalid.ctf" / "metadata"); + expectNonEmptyFile(workDirectory() / "Invalid.ctf" / "stream_1"); + expectNonEmptyFile(workDirectory() / "Invalid.TB.traceanalysis.xml"); +} + +TEST_F(CtraceIntegTests, RecoversOneFormattedRouteWithoutLosingInterleavedOutput) +{ + writeTestFile(workDirectory() / "Recovery.ctrace-run.yml", R"yml(ctrace-run: + trace-format: formatted + ctrace-setup: + - pname: first + timestamps: + clock: 400000000 + - pname: second + timestamps: + clock: 400000000 + ctrace-refs: + - ctrace-ref: first/itm + type: itm + pname: first + stream: 1 + - ctrace-ref: second/itm + type: itm + pname: second + stream: 2 +)yml"); + // Route 2 has a reserved ITM header in frame 2. Frame 3 starts with + // continuation data for the same formatter ID; a tree reset would lose it. + constexpr std::array raw{{ + 0x03U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x09U, 0x05U, 0x41U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x10U, + 0x80U, 0x04U, 0x00U, 0x58U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x10U, 0x43U, 0x18U, 0x44U, 0x20U, 0xe2U, + 0x44U, 0x29U, 0x03U, 0x46U, 0x10U, 0x42U, 0x01U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x07U, + }}; + writeTestFile(workDirectory() / "Recovery.TB.raw", + {reinterpret_cast(raw.data()), static_cast(raw.size())}); + + const auto result = run({"ctrace", workDirectory().string(), "--target", "Recovery", "--all"}); + EXPECT_EQ(1, result.exitCode); + expectContains(result.stderrText, "invalid ITM packet header at raw offset 17"); + const auto csv = readTestTextFile(workDirectory() / "Recovery.TB.csv"); + for (const auto expected : {",1,itm,1,0x41", ",1,itm,2,0x42", ",2,itm,2,0x43", ",2,itm,3,0x44", ",2,itm,4,0x45", + ",2,itm,5,0x46", ",2,error"}) { + expectContains(csv, expected); + } + expectNotContains(csv, ",2,itm,0,0x58"); + expectNotContains(csv, ",1,error"); + expectNonEmptyFile(workDirectory() / "Recovery.ctf" / "metadata"); + expectNonEmptyFile(workDirectory() / "Recovery.ctf" / "stream_1"); + expectNonEmptyFile(workDirectory() / "Recovery.ctf" / "stream_2"); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Recovery.TB.traceanalysis.xml")); +} + +TEST_F(CtraceIntegTests, AbortsAllOutputsOnFormattedDataBeforeFirstSourceId) +{ + writeTestFile(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"); + writeTestFile(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: + writeTestFile(workDirectory() / "Events.ctrace-run.yml", R"yml(ctrace-run: ctrace-setup: - timestamps: clock: 400000000 @@ -164,17 +697,17 @@ TEST_F(CtraceIntegTests, ExpandsDwtEventCountersAcrossCsvAndCtf) )yml"); const std::string raw{"\0\0\0\0\0\x80\x05\x21\x09\x41", 10U}; - writeFile(workDirectory() / "Events.SWO.raw", raw); + writeTestFile(workDirectory() / "Events.SWO.raw", raw); const auto result = run({"ctrace", workDirectory().string(), "--target", "Events", "--all"}); EXPECT_EQ(0, result.exitCode) << result.stderrText; EXPECT_EQ("cycles,stream,type,source,value,pc,address,note\n" "0,,event,0,0x21,,,\n" "0,,itm,1,0x41,,,\n", - readTextFile(workDirectory() / "Events.SWO.csv")); - expectContains(readTextFile(workDirectory() / "Events.ctf" / "metadata"), "name = \"DWT_EVENT\""); + readTestTextFile(workDirectory() / "Events.SWO.csv")); + expectContains(readTestTextFile(workDirectory() / "Events.ctf" / "metadata"), "name = \"DWT_EVENT\""); expectNonEmptyFile(workDirectory() / "Events.ctf" / "stream_0"); - expectContains(readTextFile(workDirectory() / "Events.SWO.traceanalysis.xml"), + expectContains(readTestTextFile(workDirectory() / "Events.SWO.traceanalysis.xml"), "