diff --git a/tools/ctrace/README.md b/tools/ctrace/README.md index 627e373c0..a0c729339 100644 --- a/tools/ctrace/README.md +++ b/tools/ctrace/README.md @@ -39,17 +39,27 @@ For `ctrace .trace --target Board --all`, the supported input produces: ```text .trace/ Board.SWO.csv - Board.ctf/ + Board.SWO.ctf/ metadata stream_0 - Board.SWO.traceanalysis.xml # only with graphical data and one retained clock domain + Board.traceanalysis.xml # graphical views from eligible completed CTF bundles ``` -Discovery requires exactly one `Board.SWO.raw`, `Board.TB.raw`, or -`Board.TB_.raw` input. Without a format declaration, SWO defaults to -unformatted ITM and TB or named-TB defaults to formatted CoreSight input. -Missing or null `trace-format` uses this channel-based default; an explicit -value overrides it for the selected input: +Discovery processes every existing `Board.SWO.raw`, `Board.TB.raw`, and `Board.TB_.raw` input independently, +one after another. `--target Board` selects the solution set and all its supported inputs. Each input gets its own +CSV and CTF bundle, using `.` as their common base name. CTF uses +this channel-qualified name even for a single input; older `Board.ctf` bundles are neither reused nor removed. +The [CTF profile](docs/ctf-format.md#files-and-common-structure) records the required specification alignment. + +One optional `Board.traceanalysis.xml` collects graphical views from all eligible CTF bundles completed for that +target in the current invocation. Each contributing bundle must retain exactly one clock domain. A multi-clock bundle +remains valid CTF but is omitted from the XML with a warning. Independent single-clock bundles can contribute together; +their clock UUIDs keep sources separate without synchronizing or rebasing their timestamps. No old per-channel XML +files are migrated or deleted. An XML failure leaves completed CSV and CTF outputs intact. + +Without a format declaration, each SWO input defaults to unformatted ITM and each TB or named-TB input defaults to +formatted CoreSight. Missing or null `trace-format` uses these channel-based defaults; an explicit value overrides +the format for every supported input in that trace-run: ```yaml ctrace-run: @@ -57,13 +67,16 @@ ctrace-run: # ctrace-setup and ctrace-refs follow here ``` -This optional, ctrace-private provisional field does not select a file or -resolve multiple candidates. The channel-based default is a heuristic, not +This optional, ctrace-private provisional field does not select a file. The channel-based default is a heuristic, not byte-content detection. 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. +Event Recorder (`Board.ER.raw`) remains unsupported and is skipped with a warning. A solution set without a supported +input reports an error. A failure in one input leaves the remaining inputs and solution sets available for processing; +the command returns non-zero if any input fails. This also applies when validating without output options. + ## Incomplete captures Formatted input accounts for payload skipped because its source ID is missing, NULL, reserved, or unconfigured, @@ -85,8 +98,9 @@ from healthy routes. Continuing past an unassigned prefix therefore does not gua A fatal OpenCSD error, including a framing error, aborts decoding and returns a non-zero status. A CSV output that has already started retains the previously committed rows and ends with a global `type=error` row. Its `note` is `decode aborted after processing N input bytes; trace is incomplete: reason`; all other fields are empty. This final -record describes the entire input, so it bypasses both `--type` and `--stream`. Partial CTF and Trace Compass XML -artifacts are still removed. A failure before CSV starts creates no CSV, and a CSV write or close failure still +record describes the entire input, so it bypasses both `--type` and `--stream`. The incomplete CTF bundle is removed +and contributes no views to the target XML; completed bundles from other inputs remain eligible. A failure before +CSV starts creates no CSV, and a CSV write or close failure still removes the unreliable file. Preserving partial CSV with this global marker is an explicit ctrace contract; the published CSV specification does not define fatal-abort handling. diff --git a/tools/ctrace/docs/architecture.md b/tools/ctrace/docs/architecture.md index e312c9106..955ad8b2e 100644 --- a/tools/ctrace/docs/architecture.md +++ b/tools/ctrace/docs/architecture.md @@ -20,17 +20,19 @@ Backend-specific representations are documented in the [CTF profile](ctf-format. PC sampling accepts a four-byte PC or the one-byte status markers `0x00` (`CPU Sleeping`) and Armv8-M `0xff` (`Trace prohibited`). Status markers preserve route, timestamp, and sample quality without a PC address. -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). +Every matching SWO, TB, and named-TB raw input is processed independently and sequentially for each selected 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. ## How it works at a glance -`ctrace` processes one solution set at a time. The [README](../README.md#trace-directory) describes how configuration, -raw input, and generated output files are grouped by their common base name. +`ctrace` processes one solution set at a time and runs a separate file job for each supported raw input in that set. +The [README](../README.md#trace-directory) describes how configuration, raw inputs, and generated outputs are grouped +by solution-set and channel names. The following path runs once per input, with fresh decoder and output state. The main in-memory path is: @@ -66,8 +68,8 @@ command line + trace-run YAML + selected SWO/TB raw file ``` The `TraceEvent` boundary is the central design point. Before it, code handles byte offsets, OpenCSD packets, decoder -recovery, and Cortex-M state. After it, code sees backend-independent events in decode order and does not depend on -OpenCSD types. +recovery, and Cortex-M state. After it, code sees backend-independent events in semantic callback order, +without depending on OpenCSD types. Formatter-skipped payload and initial ITM synchronization skips follow a separate `TraceByteSkip` path from the OpenCSD adapter through `DecodePipeline` directly to `TraceEventSink::appendByteSkip` and `DecodeConsumers`. These @@ -82,13 +84,14 @@ 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. +Input selection and route binding belong to `tracerun` and complete before each input's decoder and output backends +are constructed. Target-level XML preparation precedes these per-input jobs. The provisional, ctrace-private `trace-format` declaration describes effective capture bytes rather than target -capability or file identity. Discovery first selects exactly one SWO, TB, or named-TB input, independently of whether -the format is declared. An explicit format wins; otherwise SWO defaults to `unformatted` and TB or named-TB to -`formatted`. The selected format is resolved before `CtraceRunMeta` normalizes routes, so the decoder and route model -use the same effective format. This channel-based heuristic does not inspect capture bytes or resolve ambiguous input -selection. Framing remains an internal decoder contract. +capability or file identity. Discovery collects every matching SWO, TB, and named-TB input, independently of whether +the format is declared. Each input is preflighted and normalized separately. An explicit format applies to every input; +otherwise SWO defaults to `unformatted` and TB or named-TB to `formatted`. The format is resolved before +`CtraceRunMeta` normalizes that input's routes, so the decoder and route model use the same effective format. This +channel-based heuristic does not inspect capture bytes. 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 @@ -96,7 +99,7 @@ binding, valid IDs, and the synthetic unformatted route; backends preserve its l ## Processing state and ownership -One `DecodePipeline` is created for the selected raw file. It owns the OpenCSD adapter and Cortex-M stream decoder, so +One `DecodePipeline` is created for each 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. @@ -104,7 +107,9 @@ protocol, formatter, timestamp, and DWT state survive arbitrary file-read bounda 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. +registry use process-global state; the session restores the previously installed logger when it is destroyed. Each +file job finishes and releases its decoder before the next input starts. Route IDs and clocks remain local to that +input, so files can reuse Trace Bus IDs without sharing decoder state or output streams. 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 @@ -115,7 +120,9 @@ reference. This keeps format-specific feed and recovery policy out of the low-le 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. +the same `TraceEventSink` while keeping route-specific timestamp and DWT state apart. Events buffered for timestamp +resolution are emitted independently per route. The sink observes semantic emission order, not a global raw-input or +chronological order across routes. There is no application-wide event queue. `DecodeConsumers` forwards each event synchronously to the output lifecycle and issue reporter. @@ -160,7 +167,8 @@ diagnostic artifacts but makes the invocation fail. Routes with no received payl 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. The fatal-input output policy -retains committed CSV rows with a global abort record, but removes incomplete CTF and XML artifacts. +retains committed CSV rows with a global abort record, but removes the incomplete CTF bundle. Only completed bundles +contribute to target-level XML. ## OpenCSD diagnostic callbacks @@ -204,8 +212,8 @@ byte. Packet rollback remains separate from the fatal-input output policy descri 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) and - [`TraceRunDiscovery.cpp`](../src/tracerun/TraceRunDiscovery.cpp) to see how a solution set, YAML, and exactly one - SWO/TB input become a preflighted descriptor. + [`TraceRunDiscovery.cpp`](../src/tracerun/TraceRunDiscovery.cpp) to see how a solution set and YAML select all + matching SWO/TB inputs, each with its own preflighted descriptor and file job. 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), @@ -281,16 +289,29 @@ Output requirements are evaluated per backend and selected route. For example, m 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 decode callback order. `CtfBundleOutput` owns a bundle-local metadata model and +Each input writes separate `..csv` and `..ctf` artifacts. One optional +`.traceanalysis.xml` describes the target's eligible completed bundles. CTF always uses the channel-qualified +path, including single-input runs; existing `.ctf` bundles and old per-channel XML files are not migrated or +removed. The [CTF profile](ctf-format.md#files-and-common-structure) +records this intentional difference from the published bundle path. + +CSV writes one combined file per input in sink 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. -CTF finalization retains only emitted streams, then generates Trace Compass XML from their observed graphical topics. -Without graphical topics, it omits the XML entirely; point events remain in the CTF event table. This avoids invalid -empty analyses and invented durations. 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. +CTF finalization retains only emitted streams and exposes the completed metadata. `TraceDirectoryJob` gives these +results to a target-scoped `TraceCompassXmlOutput`; it does not discover contributions by scanning existing CTF files. +The collector prepares the target XML before the input jobs and writes it after all inputs have been attempted. +Only observed graphical topics in freshly completed bundles contribute views. Without graphical topics, it leaves no +XML; point events remain in the CTF event table. + +Each contributing bundle must retain exactly one clock domain. Multi-clock bundles remain valid CTF but are excluded +with a warning because the supported reader cannot safely order their independent streams. Separate single-clock +bundles can contribute to one XML without clock correlation or timestamp rebasing. Every clock, including legacy +`swo_clock`, has a UUID; the XML scopes state by the reader's `hostId` (that UUID), public `cmsis_trace_bus_id`, and +topic. Channel/processor labels are display-only, and the private CTF `ctrace_route` context is no longer needed by +the XML. A deterministic namespace derived from the contributing clock identities prevents analysis/view collisions +between target XML files. The legacy binary event layout is unchanged. 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 @@ -301,8 +322,11 @@ owns the active state and common write-failure cleanup; concrete backends implem The byte-skip hook defaults to no output, as required by CTF; CSV implements it independently of event filters. A successful backend can finish even if another backend fails. When `FileDecodeJob` catches `OpenCsdFatalError`, it passes a `TraceDecodeAbort` to output finalization. The default backend policy removes incomplete artifacts, as -required for CTF and XML. CSV instead appends a global abort record and closes the already committed rows. Failed +required for CTF. CSV instead appends a global abort record and closes the already committed rows. Failed CSV writes or finalization still remove the unreliable file; failures before output startup do not create one. +Target XML has an independent lifecycle: a failed input cannot remove another input's completed bundle or views, +and an XML preparation/write failure leaves completed CTF and CSV outputs intact. Preparing target XML never migrates +or removes historical per-channel XML files. ## Diagnostics and failure semantics @@ -330,9 +354,11 @@ of creating routes or clocks, and CLI Info remains visible in CTF-only mode. The source warning remains separate from byte accounting. A route-bound missing-sync Error follows ordinary output selection but always contributes to command failure; its text does not repeat the byte count already reported as Info. -An invocation-wide diagnostic sink aggregates failures while other solution sets continue where possible, then -determines the final process status. Errors are rendered as `error` even when their impact causes a non-zero exit -status. Unhandled internal ctrace failures also terminate the command after an error diagnostic. +An invocation-wide diagnostic sink aggregates failures while remaining inputs in the same set and other solution +sets continue, then determines the final process status. Errors are rendered as `error` even when their impact causes +a non-zero exit status. Unhandled internal ctrace failures also terminate the command after an error diagnostic. +An input-scoped forwarding sink adds `inputChannel` and `input` context to every file-job diagnostic while preserving +its severity and failure impact. Producer reference annotations are reported once per configuration, before file jobs. ## External dependencies diff --git a/tools/ctrace/docs/architecture.svg b/tools/ctrace/docs/architecture.svg index b0e2f364a..26c6a06ff 100644 --- a/tools/ctrace/docs/architecture.svg +++ b/tools/ctrace/docs/architecture.svg @@ -7,13 +7,17 @@ aria-labelledby="title description"> ctrace software architecture - ITM and DWT runtime data flow from command-line, trace-run YAML, and one selected raw input through a channel-based - format default or explicit override, normalized routes, mode-specific output planning, a SINGLE or formatted + ITM and DWT runtime data flow from command-line, trace-run YAML, and every matching raw input, processed sequentially + with independent state, through a channel-based format default or explicit override, 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. Native errors, bounded packet previews, and recovery details reach diagnostics and CSV. Formatter and initial synchronization skips follow a separate Info path. Fatal decode retains committed CSV rows with a global - abort record and removes incomplete CTF and XML artifacts. + abort record and removes incomplete CTF for that input. Remaining inputs continue. CSV and CTF outputs always + include the channel in their names, also for single-input runs. One target XML collects graphical views from + freshly completed single-clock bundles, scoped by clock UUID and Trace Bus ID without time correlation. + XML failures preserve CSV and CTF. Only one OpenCSD tree is live at a time. @@ -87,14 +91,14 @@ tracerun - read YAML · select exactly one input - resolve format · normalize routes - preflight · deferred output values + read YAML · collect all SWO / TB inputs + per input: format · routes · preflight + deferred output values control TraceDirectoryJob · FileDecodeJob - descriptor · output plan · 64 KiB reads + sequential file jobs · continue after input failure diagnostics @@ -121,14 +125,14 @@ OpenCSD ITM adapter SINGLE / FRAME_FORMATTED tree - one ITM decoder per route + one live tree · one ITM decoder per route cursor · transactions · route reset Cortex-M decode timestamps · DWT pairing quality state per route - ordered semantic mapping + route-local event buffering TraceEvent model @@ -159,7 +163,7 @@ OUTPUTS - + TraceOutput + TraceOutputLifecycle @@ -168,20 +172,21 @@ CSV backend - one file · callback order + <set>.<channel>.csv · emission order events + skipped-byte Info - CTF bundle + <set>.<channel>.ctf legacy stream_0 eager · formatted lazy explicit route clocks · no fallback - - Trace Compass XML - observed graphical topics only - requires one retained clock domain + + <set>.traceanalysis.xml + TraceCompassXmlOutput · completed bundles + one clock / bundle · UUID + route scopes + no clock correlation or timestamp rebasing - Fatal decode: CSV retains committed rows + global abort record; incomplete CTF / XML artifacts are removed. + Abort: retain CSV + error; remove incomplete CTF. XML uses fresh completed bundles; XML failures preserve CSV / CTF. diff --git a/tools/ctrace/docs/constraints.md b/tools/ctrace/docs/constraints.md index 74ede089f..994469e45 100644 --- a/tools/ctrace/docs/constraints.md +++ b/tools/ctrace/docs/constraints.md @@ -38,15 +38,20 @@ configuration with trace communication, separate from trace-source setup. - Optional `ctrace-run.trace-format` accepts only `unformatted` or `formatted`. An explicit value overrides the channel-based default. Missing or null selects `unformatted` for SWO and `formatted` for TB or named-TB without an - Error. Discovery resolves this effective format before route normalization. -- Exactly one existing `.SWO.raw`, `.TB.raw`, or `.TB_.raw` must be selected, independently of a - format declaration. Zero or multiple candidates fail before decoder or output construction; SWO has no priority - over coexisting TB input. 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. + Error. Discovery resolves this effective format separately for each input before route normalization. An explicit + override applies to every supported input associated with that trace-run configuration. +- Every existing `.SWO.raw`, `.TB.raw`, and `.TB_.raw` is processed independently and sequentially. + `--target` selects a solution set, including all its supported inputs. A set without an eligible input reports an error. + Each input must be a regular, readable file and is opened during its own preflight, before that input's decoder and + output backends are constructed. Target XML preparation precedes these per-input jobs. Event Recorder input remains + diagnosed and excluded from processing. +- Input preflight, route normalization, decoding, and output failures do not prevent processing the remaining inputs + or solution sets. Each input has independent decoder and output state; any failing input contributes to a non-zero + command exit status. A configuration read failure prevents processing that set but does not stop later sets. - The standardized `trace-buffer` selection belongs to solution/build-run producer configuration, not to the `*.ctrace-run.yml` file consumed by ctrace. The caller-facing selection and format/framing contract remains - follow-up work; this does not imply adding fields to `*.ctrace-run.yml`. Current discovery still requires one - unambiguous input and applies the channel-based defaults above. + follow-up work; this does not imply adding fields to `*.ctrace-run.yml`. Current discovery processes all supported + matching files and applies the channel-based defaults above. - 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. @@ -107,7 +112,8 @@ configuration with trace communication, separate from trace-source setup. outputs and healthy routes, instead of silently producing an apparently successful empty conversion. - 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 stops decoding. Outputs follow the fatal-abort policy - below: CSV retains committed rows with an abort marker; incomplete CTF and XML are removed. An incomplete packet at + below: CSV retains committed rows with an abort marker; incomplete CTF is removed and excluded from target XML. + 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 @@ -116,9 +122,15 @@ configuration with trace communication, separate from trace-source setup. ## Observable behavior and output safety -- CSV remains one combined file in decode 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. The seventh CSV column is `address`, matching the published CMSIS-Toolbox +- Each raw input has separate `..csv` and `..ctf` outputs. One optional + `.traceanalysis.xml` collects the target's eligible completed CTF views. The channel-qualified CTF path applies + even with one input; legacy `.ctf` bundles and old per-channel XML files are not reused, migrated, or removed. + Aligning the intentional CTF path change with the published + CTF specification remains [follow-up work](todo.md#inputs-and-time-correlation). +- CSV remains one combined file per input in sink callback order, not a global raw-input or chronological order across + routes. 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. The seventh CSV column is `address`, + matching the published CMSIS-Toolbox [CSV schema](https://open-cmsis-pack.github.io/cmsis-toolbox/Experimental-Features/#csv-format). - Byte-skip annotations are retained in CSV regardless of `--type` or `--stream`: `type` is `info`, `note` describes the reason, byte count, and formatter-group offset, and `stream` is the observed formatter ID when known, including @@ -133,30 +145,31 @@ configuration with trace communication, separate from trace-source setup. or data-loss boundary. Unsupported payloads remain errors; arbitrary raw `0xff` bytes are not PC-sampling markers. - 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. + `stream_0` and its binary event layout. Its `swo_clock` now has an explicit random UUID, like generalized clocks. - 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. + `ctrace_route` enum for display. The enum label is the processor name when bound and the decimal CTF stream-class ID + otherwise; XML does not depend on this private field. XML state paths use `hostId` (the bundle's sole clock UUID), + architectural `cmsis_trace_bus_id`, and topic. View paths bind those identities explicitly, including ID `0` for + legacy input. Labels use the input channel and optional processor name, never the numeric ID. Analysis/view IDs + share a namespace derived from contributing clock UUIDs, preventing cross-target registration collisions. - 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, trace-prohibited markers, 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. - If no graphical topic remains after output filtering, no companion XML is generated and stale XML is removed; + If no graphical topic remains after output filtering, no target XML is generated and stale target XML is removed; an empty analysis is not a valid Trace Compass configuration. The CTF bundle remains available. - `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. - Each formatted route has an independent CTF clock domain, even when processor labels or 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. + A multi-clock CTF bundle remains valid, but ctrace emits one Warning and excludes that bundle from target XML + because the supported reader cannot establish its combined event order. Separate single-clock bundles can all + contribute to one XML; their UUID-scoped states remain separate. Neither correlation nor timestamp rebasing is + inferred between those bundles, even when frequencies or processor labels match. - CLI decoder warnings and errors remain observable regardless of output filtering. Ordinary route-bound CSV diagnostics follow the stream filter and use the `error` type selector, including warning-severity issues. The published CSV schema defines no `warning` type or severity column. A recoverable protocol error may be published @@ -164,15 +177,21 @@ configuration with trace communication, separate from trace-source setup. - A fatal OpenCSD decode abort preserves already committed CSV rows and appends one input-wide `type=error` record with `note` set to `decode aborted after processing N input bytes; trace is incomplete: reason`. All other fields are empty. This global marker bypasses both type and stream filters; ordinary route-bound errors do not. Partial - CTF and XML artifacts are removed. This retention and global-marker policy is an explicit ctrace contract, not a + CTF artifacts are removed and contribute no XML views. This retention and global-marker policy is an explicit + ctrace contract, not a requirement of the published CSV specification. Failures before CSV startup create no CSV; CSV write or close failures still remove the unreliable file. - Structured diagnostic impact determines command failure; formatted stderr text does not. +- Target XML is prepared before processing the target's inputs and finalized after all have been attempted. It uses + only CTF metadata successfully finalized in that invocation, never stale bundles discovered on disk. Recoverable + decoder errors do not exclude an otherwise completed bundle. An XML failure does not remove completed CTF or CSV; + an input failure does not remove other inputs' contributions. No per-channel XML is generated or cleaned up. - 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. - Apart from the explicitly retained CSV after a fatal decode abort, cleanup of incomplete output artifacts is - attempted after failure, and cleanup failures are reported. Incompatible existing output filesystem types and - overlapping CTF/XML paths are rejected before replacement. + attempted after failure, and cleanup failures are reported. Incompatible existing output filesystem types are + rejected before replacement. CTF bundles and target XML use distinct sibling paths derived from the trace-run + and input names. ## Build and CI constraints diff --git a/tools/ctrace/docs/ctf-format.md b/tools/ctrace/docs/ctf-format.md index 633039b1b..f6bb4281e 100644 --- a/tools/ctrace/docs/ctf-format.md +++ b/tools/ctrace/docs/ctf-format.md @@ -24,9 +24,17 @@ cmsis_ctf_profile_version = 1 ## Files and common structure +Each raw input writes an independent `..ctf` bundle, where `` is `SWO`, `TB`, or +`TB_`. The channel is always included, even when only one input exists. Inputs are processed sequentially; +their metadata, stream IDs, clocks, and output cleanup are independent. Existing `.ctf` directories +are not reused, migrated, or removed. This intentional pathname change differs from the published +[file layout](https://open-cmsis-pack.github.io/cmsis-toolbox/Experimental-Features/#directory-and-file-structure), +which still names `.ctf`; specification alignment remains outstanding. + 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, +`swo_clock`. That clock now has an explicit UUID without changing the legacy binary layout. 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. @@ -47,15 +55,16 @@ 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. +one binary stream; independent routes are not clamped against each other. All clock domains receive random UUIDs, +including legacy `swo_clock`. Independent captures and routes have distinct identities 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. `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`. +`cmsis_stream__processor_name`. Generated XML uses the public Trace Bus ID and clock identity, not the private +`ctrace_route` display enum. The packet context has this exact field order: @@ -74,16 +83,21 @@ synchronization skips are separate byte-accounting Info annotations in CLI/CSV o identifies observed input, not a decoded CTF route or clock; this includes NULL and reserved IDs. Ctrace does not invent a CTF stream, timestamp, or `resync` event for these annotations. -The optional `..traceanalysis.xml` companion is stored next to the bundle. It is generated -only when at least one retained stream has graphical data and all retained streams reference one clock domain. -Trace Compass rejects empty analyses and cannot safely combine independent clocks. These restrictions affect only -the generated visualization; a metadata-only, point-event-only, or multi-clock CTF bundle remains valid. +One optional `.traceanalysis.xml` is stored beside the target's bundles. It collects graphical views +from eligible bundles completed in the current invocation, not from existing CTF files found on disk. Each bundle +must retain graphical data and exactly one clock domain. A multi-clock bundle is excluded with a warning; eligible +single-clock bundles still contribute. These restrictions affect only visualization; metadata-only, point-event-only, +and multi-clock CTF bundles remain valid. Historical per-channel XML files are neither migrated nor deleted. -A fatal OpenCSD decode abort removes the incomplete CTF bundle and its XML companion, including data already written -for healthy routes. If CSV output remains healthy, ctrace retains its selected rows and appends one input-wide `error` +A fatal OpenCSD decode abort removes the incomplete CTF bundle, including data already written for healthy routes, +and excludes it from target XML. If CSV output remains healthy, ctrace retains its selected rows and appends one +input-wide `error` row with the processed-byte count and abort reason. That final row has no cycle timestamp, stream, or source and bypasses type and stream filters. A recoverable route-local error instead allows normal output completion, as described -under [`TRACE_STATUS`](#trace_status-event-id-3). Both kinds of Error produce a failing command exit status. +under [`TRACE_STATUS`](#trace_status-event-id-3). Both kinds of Error produce a failing command exit status. Remaining +raw inputs are still processed, and their completed bundles and XML contributions are unaffected by another input's +failure. Target XML preparation and finalization have their own lifecycle; an XML failure leaves completed CTF and +CSV outputs intact. ## Event catalogue @@ -372,30 +386,46 @@ events as generated XML tables. The companion XML contains only graphical views - Time graphs for DWT matches, DWT and PMU counter pulses, decoded exception activity, and processor sleep. ITM values, trace-status records, Global Timestamps, ordinary PC samples, and trace-prohibited markers 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 +standard event table because they do not establish a duration. Each contributing route receives a separate graphical +view. Its visible suffix is the input channel followed by 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. When no graphical topic remains, ctrace omits the XML entirely and removes -any stale companion file; importing only the CTF bundle still provides the event table. +any stale target file; importing only the CTF bundle still provides the event table. 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. +when configured frequencies match. After lazy stream projection, a completed bundle contributes XML views only if +it retains at least one stream and exactly one referenced domain; with current planning, this normally means one +retained formatted route. Multiple retained domains exclude that bundle and produce one warning, since the supported +reader cannot establish a combined event order within it. Separate single-clock CTF bundles can contribute to the +same target XML without correlating clocks or rebasing timestamps. + +Every state path begins with `hostId` (the reader's sole clock UUID for that bundle), then +`context.cmsis_trace_bus_id`, then the topic. View entries select the concrete `//` +path. This includes legacy ID `0` and separates captures that reuse formatted ID `1` or the same processor label. +View queries accept only that concrete UUID, either bare or quoted, because supported readers can expose the TSDL +quotes as part of `hostId`; they do not wildcard the capture identity. +Separate bundles must not reuse a clock UUID: if future inputs establish shared clocks across captures, this +capture-identity contract must be revised explicitly. +Only metadata from successfully finalized bundles in the current run contributes: a preserved old bundle after an +input preflight failure is not eligible. Completed output after recoverable decoding errors remains eligible. 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. +Analysis and view identifiers share a deterministic namespace derived from the contributing clock identities. +Random clock UUIDs distinguish independent captures; the namespace prevents XML definitions for different targets +from colliding in one viewer. Display labels are not used as identity. Preparing or writing the target XML cannot +delete completed CSV/CTF output, and it never removes historical per-channel XML files. + ## Current profile boundaries - 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. + synchronization. Each bundle contributing to XML must have one domain; different contributing bundles remain + independent and their timestamps are not rebased. - 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, @@ -418,12 +448,14 @@ The primary implementation sources are [`OutputRequirements.cpp`](../src/output/ [`CtfMetadataWriter.cpp`](../src/output/ctf/CtfMetadataWriter.cpp), [`CtfEncoder.cpp`](../src/output/ctf/CtfEncoder.cpp), [`CtfStreamWriter.cpp`](../src/output/ctf/CtfStreamWriter.cpp), -[`CtfBundleOutput.cpp`](../src/output/ctf/CtfBundleOutput.cpp), and +[`CtfBundleOutput.cpp`](../src/output/ctf/CtfBundleOutput.cpp), +[`TraceCompassXmlOutput.cpp`](../src/output/ctf/TraceCompassXmlOutput.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 +topics observed in completed output create views. Target-level aggregation additionally scopes views by clock UUID +and Trace Bus ID. XML-shape changes must update focused XML tests, review and update the 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 diff --git a/tools/ctrace/docs/multi-source-design.md b/tools/ctrace/docs/multi-source-design.md index 86ddf6685..542643cd9 100644 --- a/tools/ctrace/docs/multi-source-design.md +++ b/tools/ctrace/docs/multi-source-design.md @@ -97,8 +97,9 @@ legacy CTF stream-class ID `0`. These representations do not turn it into CoreSi An observed formatter ID without a configured ITM route is diagnosed and skipped. Its presence cannot establish whether its bytes contain ITM or an instruction-trace protocol. This keeps unsupported sources from corrupting -supported routes. If simultaneous raw inputs are added later, route identity must also distinguish inputs because -different formatter domains can reuse the same Trace Bus ID. +supported routes. The original plan deferred simultaneous raw inputs and anticipated extending route identity with +input identity because different formatter domains can reuse the same Trace Bus ID. The later independent-file +processing described below keeps these domains separate through per-input jobs and outputs. ## Semantic state, output, and clocks @@ -124,11 +125,14 @@ not establish a shared origin. Global Timestamp records alone do not establish c CTF requires a valid configured frequency for each selected route. A missing or invalid clock disables CTF with an Error while CSV or validation-only decoding can continue. Backend-specific requirements and failures remain -independent. Within CTF, metadata, all stream files, and companion XML form one completion and cleanup unit. +independent. The original implementation treated CTF metadata, stream files, and companion XML as one completion +and cleanup unit. Target-level XML now has an independent lifecycle, as recorded under +[revised decisions](#decisions-revised-after-the-migration). The supported Trace Compass reader cannot safely combine independent clock declarations. Ctrace therefore emits -companion XML only when retained streams use one clock domain. Multi-clock CTF remains valid, but produces a warning -and no XML. This preserves correct per-stream timing without presenting unrelated cycle counters as one timeline. +views for a bundle only when retained streams use one clock domain. Multi-clock CTF remains valid, but produces a +warning and contributes no XML views. Separate single-clock captures can now share a target XML while retaining +UUID-scoped identities; their clocks are not correlated or rebased. ## Recovery without losing another source @@ -189,14 +193,28 @@ Reproducible commands and supported consumer versions belong to the current The following changes supersede assumptions in the original plan: -- Discovery now considers SWO, TB, and named-TB together and requires exactly one input. An absent or null format - defaults by channel: SWO is unformatted, TB is formatted. Originally, omission selected only the legacy SWO path. +- Discovery initially expanded eligibility to SWO, TB, and named-TB while retaining the one-input limit. It now + processes all matching supported inputs independently and sequentially. Each input is normalized separately: + absent or null format defaults by channel, with SWO unformatted and TB formatted; an explicit override applies + to every input in the trace-run. Originally, omission selected only the legacy SWO path. +- Input failures contribute to command failure while remaining inputs and solution sets continue. Each file has + independent decoder state and outputs, with only one OpenCSD tree live at a time. `--target` still selects a + solution set, including all its supported inputs. +- CTF bundles now always use `..ctf`, also for single-input runs. CSV retains its + channel-qualified path. Existing `.ctf` bundles are not migrated or removed; the published CTF path + still needs alignment as recorded in the [CTF profile](ctf-format.md#files-and-common-structure). - Formatter skips and initial unsynchronized ITM bytes now produce non-failing byte-count Info annotations. They carry no invented route or time; a route receiving payload without a committed hardware sync reports an Error. -- A fatal decoder abort retains committed CSV rows with an unfiltered input-wide abort record. Incomplete CTF/XML - are still removed. This replaces the original all-output cleanup policy for an otherwise healthy CSV writer. +- A fatal decoder abort retains committed CSV rows with an unfiltered input-wide abort record. Incomplete CTF + is still removed and excluded from XML. This replaces the original all-output cleanup policy for a healthy CSV writer. - Trace Compass XML now creates graphical views only for topics observed in completed output, refining the original eager legacy XML compatibility rule. +- One `.traceanalysis.xml` now collects eligible, freshly completed CTF bundles for the target. Its + lifecycle is independent of CSV/CTF, and old per-channel XML files are not migrated or deleted. Each contributing + bundle has one clock domain; multi-clock bundles are excluded with a warning. XML paths use clock UUID plus public + Trace Bus ID, not processor labels or private `ctrace_route` context. Clock-derived namespaces prevent view-ID + collisions across targets. Legacy `swo_clock` also gets a UUID without a binary layout change; independent captures + are neither synchronized nor rebased. - CSV uses the published `address` column. The plan's outstanding rename from `offset` is no longer applicable. These are current contracts, not deferred phases of the migration. Detailed behavior and output-selection exceptions diff --git a/tools/ctrace/docs/todo.md b/tools/ctrace/docs/todo.md index 7007c215e..fa01b7917 100644 --- a/tools/ctrace/docs/todo.md +++ b/tools/ctrace/docs/todo.md @@ -14,13 +14,13 @@ [devtools #2573](https://github.com/Open-CMSIS-Pack/devtools/issues/2573). Keep trace communication separate from trace-source setup and decide how to replace the temporary `trace-format` override without assuming its standardization. +- [ ] Align the published CTF bundle path with ctrace's `..ctf` naming, including single-input runs. - [ ] Support FSYNC and FSYNC+HSYNC formatted input after a public framing contract 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 -- [ ] 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. diff --git a/tools/ctrace/src/CMakeLists.txt b/tools/ctrace/src/CMakeLists.txt index a34113c4d..9f253d5d1 100644 --- a/tools/ctrace/src/CMakeLists.txt +++ b/tools/ctrace/src/CMakeLists.txt @@ -39,6 +39,7 @@ set(CTRACE_DECODE_HEADER_FILES decode/SaturatingArithmetic.h ) set(CTRACE_OUTPUT_HEADER_FILES + output/OutputPath.h output/OutputRequirements.h output/TraceOutput.h output/TraceOutputConfig.h @@ -55,6 +56,7 @@ set(CTRACE_OUTPUT_HEADER_FILES output/ctf/CtfStreamWriter.h output/ctf/CtfUuid.h output/ctf/TraceCompassXmlWriter.h + output/ctf/TraceCompassXmlOutput.h ) set(CTRACE_CONTROL_HEADER_FILES control/DecodeConsumers.h @@ -153,6 +155,7 @@ target_link_libraries(ctrace-decode PUBLIC ) add_library(ctrace-output STATIC + output/OutputPath.cpp output/csv/CsvFileOutput.cpp output/csv/CsvRowMapper.cpp output/ctf/CtfBundleOutput.cpp @@ -163,6 +166,7 @@ add_library(ctrace-output STATIC output/ctf/CtfStreamWriter.cpp output/ctf/CtfUuid.cpp output/ctf/TraceCompassXmlWriter.cpp + output/ctf/TraceCompassXmlOutput.cpp output/OutputRequirements.cpp output/TraceOutputLifecycle.cpp ${CTRACE_OUTPUT_HEADER_FILES} diff --git a/tools/ctrace/src/control/FileDecodeJob.cpp b/tools/ctrace/src/control/FileDecodeJob.cpp index a2e828313..d7961c340 100644 --- a/tools/ctrace/src/control/FileDecodeJob.cpp +++ b/tools/ctrace/src/control/FileDecodeJob.cpp @@ -145,21 +145,23 @@ static TraceOutputRequest outputRequest(const CliOptions& options) }; } -/** @brief Creates the output backends enabled by a validated plan. */ -static std::vector> createConfiguredOutputs(const TraceOutputPlan& outputPlan, - DiagnosticSink& diagnostics) +/** @brief Owns configured backends and borrows access to CTF completion metadata. */ +struct ConfiguredOutputs { + std::vector> backends; + CtfBundleOutput* ctf = nullptr; +}; + +/** @brief Creates backends and retains non-owning access to completed CTF metadata. */ +static ConfiguredOutputs createConfiguredOutputs(const TraceOutputPlan& outputPlan, DiagnosticSink& diagnostics) { - std::vector> outputs; + ConfiguredOutputs outputs; if (outputPlan.ctf.has_value()) { - outputs.push_back(std::make_unique(*outputPlan.ctf, &diagnostics)); - diagnostics.report({ - DiagnosticSink::Severity::Info, - "configured Trace Compass XML", - {{"path", outputPlan.ctf->traceCompassXmlPath.string()}}, - }); + auto ctf = std::make_unique(*outputPlan.ctf, &diagnostics); + outputs.ctf = ctf.get(); + outputs.backends.push_back(std::move(ctf)); } if (outputPlan.csv.has_value()) { - outputs.push_back(std::make_unique(outputPlan.csv->outputPath, outputPlan.csv->selection)); + outputs.backends.push_back(std::make_unique(outputPlan.csv->outputPath, outputPlan.csv->selection)); } return outputs; } @@ -246,18 +248,18 @@ FileDecodeJob::FileDecodeJob(CliOptions options, TraceRunInputDescriptor input, { } -void FileDecodeJob::run() +std::optional FileDecodeJob::run() { 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; + return std::nullopt; } reportTraceRunMeta(ctraceRunMeta, m_diagnostics); auto outputs = createConfiguredOutputs(outputPlan, m_diagnostics); - DecodeConsumers consumers(std::move(outputs), m_diagnostics, itmEnableMasks(ctraceRunMeta)); + DecodeConsumers consumers(std::move(outputs.backends), m_diagnostics, itmEnableMasks(ctraceRunMeta)); reportTimestampPrescalers(ctraceRunMeta, m_diagnostics); const auto decodeStart = std::chrono::steady_clock::now(); @@ -282,4 +284,6 @@ void FileDecodeJob::run() decodeSummary(decode, decodeEnd - decodeStart), }); consumers.finishOutputs(decodeAbort.has_value() ? &*decodeAbort : nullptr); + const auto* metadata = outputs.ctf == nullptr ? nullptr : outputs.ctf->completedMetadata(); + return metadata == nullptr ? std::nullopt : std::optional{*metadata}; } diff --git a/tools/ctrace/src/control/FileDecodeJob.h b/tools/ctrace/src/control/FileDecodeJob.h index 81a1dee3a..5b16c3791 100644 --- a/tools/ctrace/src/control/FileDecodeJob.h +++ b/tools/ctrace/src/control/FileDecodeJob.h @@ -12,6 +12,9 @@ #include "DiagnosticSink.h" #include "OpenCsdItmDecoder.h" #include "TraceRunDiscovery.h" +#include "ctf/CtfMetadataModel.h" + +#include /** @brief Decodes one raw trace file and owns its configured output lifecycle. */ class FileDecodeJob { @@ -39,8 +42,9 @@ class FileDecodeJob { * Recoverable trace corruption is reported and decoding resumes at hardware * synchronization. Setup, input, and output failures are reported through the * diagnostic sink. + * @return Completed CTF metadata, if that backend finished successfully. */ - void run(); + std::optional run(); private: CliOptions m_options; diff --git a/tools/ctrace/src/control/TraceDirectoryJob.cpp b/tools/ctrace/src/control/TraceDirectoryJob.cpp index 2e117c321..223d28dee 100644 --- a/tools/ctrace/src/control/TraceDirectoryJob.cpp +++ b/tools/ctrace/src/control/TraceDirectoryJob.cpp @@ -14,15 +14,41 @@ #include "TraceRunConfigReader.h" #include "TraceRunDiscovery.h" #include "CtraceRunMeta.h" +#include "ctf/TraceCompassXmlOutput.h" #include #include +#include #include #include #include #include #include +/** @brief Adds raw-input identity to every diagnostic without changing its severity or failure impact. */ +class InputDiagnosticSink final : public DiagnosticSink { +public: + /** @brief Binds diagnostics to one independently processed capture. */ + InputDiagnosticSink(DiagnosticSink& target, const TraceRunRawInput& input) + : m_target(target), + m_context{{"inputChannel", input.channel}, {"input", input.path.string()}} + { + } + +protected: + /** @brief Forwards the original diagnostic with the capture context prepended. */ + void write(const Event& event) override + { + auto contextual = event; + contextual.context.insert(contextual.context.begin(), m_context.begin(), m_context.end()); + m_target.report(contextual); + } + +private: + DiagnosticSink& m_target; + std::vector> m_context; +}; + /** @brief Builds diagnostic context for one trace-run reference. */ static std::vector> referenceContext(const TraceRunConfig& config, const TraceRunReference& reference) @@ -84,6 +110,32 @@ static void reportTraceRunWarnings(const CtraceRunMeta& meta, DiagnosticSink& di } } +/** @brief Reports a target-level XML failure without invalidating completed capture outputs. */ +static void reportXmlFailure(DiagnosticSink& diagnostics, const std::filesystem::path& path, + const std::exception& error) +{ + diagnostics.report({DiagnosticSink::Severity::Error, error.what(), + {{"backend", "trace-compass"}, {"path", path.string()}}}); +} + +/** @brief Prepares the shared XML independently of all per-capture output backends. */ +static std::unique_ptr prepareTraceCompassXml(OutputFormat format, + const std::filesystem::path& path, + DiagnosticSink& diagnostics) +{ + if (format != OutputFormat::Ctf && format != OutputFormat::All) { + return nullptr; + } + try { + auto xml = std::make_unique(path, diagnostics); + xml->prepare(); + return xml; + } catch (const std::exception& error) { + reportXmlFailure(diagnostics, path, error); + return nullptr; + } +} + TraceDirectoryJob::TraceDirectoryJob(CliOptions options, DiagnosticSink& diagnostics, const TraceRunConfigReader& configReader) : m_options(std::move(options)), @@ -122,7 +174,7 @@ void TraceDirectoryJob::processConfigFile(const std::filesystem::path& configFil }, }); reportConsumedReferenceDiagnostics(config, m_diagnostics); - auto input = TraceRunDiscovery::resolveInput(std::move(config), [&](const auto& rawInput) { + const auto inputs = TraceRunDiscovery::selectInputs(config, [&](const auto& rawInput) { m_diagnostics.report({ DiagnosticSink::Severity::Warning, "skipping raw trace channel excluded from active input selection", @@ -133,9 +185,7 @@ void TraceDirectoryJob::processConfigFile(const std::filesystem::path& configFil }, }); }); - reportTraceRunWarnings(input.metadata(), m_diagnostics); - FileDecodeJob fileJob(m_options, std::move(input), m_diagnostics); - fileJob.run(); + processInputs(config, inputs, configFile.parent_path() / (solutionSet + ".traceanalysis.xml")); } catch (const std::exception& error) { m_diagnostics.report({ DiagnosticSink::Severity::Error, @@ -147,3 +197,41 @@ void TraceDirectoryJob::processConfigFile(const std::filesystem::path& configFil }); } } + +void TraceDirectoryJob::processInputs(const TraceRunConfig& config, const std::vector& inputs, + const std::filesystem::path& xmlPath) +{ + auto xml = prepareTraceCompassXml(m_options.outputFormat, xmlPath, m_diagnostics); + std::vector> completed; + for (const auto& rawInput : inputs) { + auto metadata = processInput(config, rawInput); + if (xml != nullptr && metadata.has_value()) { + completed.emplace_back(rawInput.channel, std::move(*metadata)); + } + } + if (xml != nullptr) { + try { + for (const auto& [channel, metadata] : completed) { + xml->add(channel, metadata); + } + xml->finish(); + } catch (const std::exception& error) { + reportXmlFailure(m_diagnostics, xmlPath, error); + } + } +} + +std::optional TraceDirectoryJob::processInput(const TraceRunConfig& config, + const TraceRunRawInput& rawInput) +{ + InputDiagnosticSink diagnostics(m_diagnostics, rawInput); + try { + auto input = TraceRunDiscovery::resolveInput(config, rawInput); + reportTraceRunWarnings(input.metadata(), diagnostics); + FileDecodeJob fileJob(m_options, std::move(input), diagnostics); + return fileJob.run(); + } catch (const std::exception& error) { + diagnostics.report({DiagnosticSink::Severity::Error, error.what(), {{"config", config.path}}}); + } + return std::nullopt; +} diff --git a/tools/ctrace/src/control/TraceDirectoryJob.h b/tools/ctrace/src/control/TraceDirectoryJob.h index 90f52bf70..f93cef2a8 100644 --- a/tools/ctrace/src/control/TraceDirectoryJob.h +++ b/tools/ctrace/src/control/TraceDirectoryJob.h @@ -11,8 +11,14 @@ #include "CliOptions.h" #include "DiagnosticSink.h" #include "TraceRunConfigReader.h" +#include "ctf/CtfMetadataModel.h" #include +#include +#include + +struct TraceRunConfig; +struct TraceRunRawInput; /** @brief Discovers and decodes the selected trace-run configurations in a directory. */ class TraceDirectoryJob { @@ -34,8 +40,13 @@ class TraceDirectoryJob { void run(); private: - /** @brief Reads, normalizes, and decodes one selected trace-run configuration. */ + /** @brief Reads one configuration and processes all its supported raw inputs. */ void processConfigFile(const std::filesystem::path& configFile); + /** @brief Collects completed per-input CTF metadata into one target-level XML. */ + void processInputs(const TraceRunConfig& config, const std::vector& inputs, + const std::filesystem::path& xmlPath); + /** @brief Isolates one input's preflight, decoding, outputs, and contextual diagnostics. */ + std::optional processInput(const TraceRunConfig& config, const TraceRunRawInput& rawInput); CliOptions m_options; DiagnosticSink& m_diagnostics; diff --git a/tools/ctrace/src/output/OutputPath.cpp b/tools/ctrace/src/output/OutputPath.cpp new file mode 100644 index 000000000..8cf621e6d --- /dev/null +++ b/tools/ctrace/src/output/OutputPath.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OutputPath.h" + +#include +#include +#include + +void OutputPath::requireSpecificTarget(const std::filesystem::path& path, const char* description) +{ + const auto normalized = path.lexically_normal(); + if (path.empty() || normalized == normalized.root_path() || normalized.filename().empty() || + normalized.filename() == "." || normalized.filename() == "..") { + throw std::invalid_argument(std::string(description) + " must identify a specific output path"); + } +} + +void OutputPath::validateParent(const std::filesystem::path& path, const char* description) +{ + std::error_code error; + const auto absolute = std::filesystem::absolute(path, error); + auto parent = (error ? path : absolute).lexically_normal().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(); + } +} diff --git a/tools/ctrace/src/output/OutputPath.h b/tools/ctrace/src/output/OutputPath.h new file mode 100644 index 000000000..b8b35218d --- /dev/null +++ b/tools/ctrace/src/output/OutputPath.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_OUTPUTPATH_H +#define CTRACE_SRC_OUTPUT_OUTPUTPATH_H + +#include + +/** @brief Shared filesystem safety checks before replacing generated outputs. */ +namespace OutputPath { + +/** @brief Rejects empty, root-like, or directory-only target paths. */ +void requireSpecificTarget(const std::filesystem::path& path, const char* description); + +/** @brief Rejects a target whose nearest existing parent is not a directory. */ +void validateParent(const std::filesystem::path& path, const char* description); + +} // namespace OutputPath + +#endif // CTRACE_SRC_OUTPUT_OUTPUTPATH_H diff --git a/tools/ctrace/src/output/OutputRequirements.cpp b/tools/ctrace/src/output/OutputRequirements.cpp index abea7f9be..fa97ccd1f 100644 --- a/tools/ctrace/src/output/OutputRequirements.cpp +++ b/tools/ctrace/src/output/OutputRequirements.cpp @@ -28,11 +28,10 @@ #include #include -/** @brief Stores the derived CSV, CTF, and Trace Compass target paths. */ +/** @brief Stores the derived per-capture CSV and CTF target paths. */ struct OutputPaths { std::filesystem::path csv; std::filesystem::path ctf; - std::filesystem::path traceCompassXml; }; /** @brief Derives all output targets from one raw input path. */ @@ -46,14 +45,11 @@ static OutputPaths outputPaths(const std::filesystem::path& rawInputPath) const auto outputDirectory = rawInputPath.parent_path(); auto csvPath = outputDirectory / captureName; csvPath += ".csv"; - auto ctfPath = outputDirectory / solutionSetName; + auto ctfPath = outputDirectory / captureName; ctfPath += ".ctf"; - auto traceCompassXmlPath = outputDirectory / captureName; - traceCompassXmlPath += ".traceanalysis.xml"; return { std::move(csvPath), std::move(ctfPath), - std::move(traceCompassXmlPath), }; } @@ -198,7 +194,7 @@ resolveCtfTopology(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& sel CtfMetadataTopology topology; if (legacy) { topology.clockDomains.push_back( - {CtfClockDomainId{0U}, "swo_clock", std::nullopt, *routes.front()->timestampClockHz, false}); + {CtfClockDomainId{0U}, "swo_clock", CtfUuid::randomV4(), *routes.front()->timestampClockHz, false}); topology.streams.push_back( {CtfStreamClassId{0U}, routes.front()->identity, routes.front()->processorName, CtfClockDomainId{0U}}); return topology; @@ -407,7 +403,7 @@ TraceOutputPlan planTraceOutputs(const TraceOutputRequest& request, const std::f if (metadata.has_value() && validRoutes && validTypes) { metadata->sources = resolveCtfSources(ctraceRunMeta, request.selection); plan.ctf = CtfOutputConfig{ - paths.ctf, paths.traceCompassXml, request.selection, std::move(*metadata), resolveCtfRoutes(ctraceRunMeta), + paths.ctf, request.selection, std::move(*metadata), resolveCtfRoutes(ctraceRunMeta), }; } } diff --git a/tools/ctrace/src/output/TraceOutputConfig.h b/tools/ctrace/src/output/TraceOutputConfig.h index 62e6a679b..d2afff841 100644 --- a/tools/ctrace/src/output/TraceOutputConfig.h +++ b/tools/ctrace/src/output/TraceOutputConfig.h @@ -32,13 +32,12 @@ struct CsvOutputConfig { TraceSelection selection; }; -/** @brief Configures one CTF bundle and its Trace Compass analysis file. */ +/** @brief Configures one independently decoded CTF bundle. */ struct CtfOutputConfig { /** @brief Creates a complete CTF output configuration. */ - CtfOutputConfig(std::filesystem::path outputDirectory, std::filesystem::path traceCompassXmlPath, - TraceSelection selection, CtfMetadataTopology metadata, std::vector routes = {}) + CtfOutputConfig(std::filesystem::path outputDirectory, TraceSelection selection, CtfMetadataTopology metadata, + std::vector routes = {}) : outputDirectory(std::move(outputDirectory)), - traceCompassXmlPath(std::move(traceCompassXmlPath)), selection(std::move(selection)), metadata(std::move(metadata)), routes(std::move(routes)) @@ -46,7 +45,6 @@ struct CtfOutputConfig { } std::filesystem::path outputDirectory; - std::filesystem::path traceCompassXmlPath; TraceSelection selection; CtfMetadataTopology metadata; std::vector routes; diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp index 6a5d017c6..57e290dd2 100644 --- a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp @@ -7,236 +7,51 @@ #include "CtfBundleOutput.h" -#include "CtfEncoder.h" #include "CtfUuid.h" -#include "DiagnosticSink.h" -#include "TraceCompassXmlWriter.h" -#include "TraceEvent.h" -#include "TraceOutputConfig.h" +#include "OutputPath.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) -{ - const auto normalized = path.lexically_normal(); - if (path.empty() || normalized == normalized.root_path() || normalized.filename().empty() || - normalized.filename() == "." || normalized.filename() == "..") { - throw std::invalid_argument(std::string(description) + " must identify a specific output path"); - } -} - -/** @brief Returns a lexically normalized absolute path for safety comparisons. */ -static std::filesystem::path normalizedAbsolutePath(const std::filesystem::path& path) +/** @brief Verifies that an existing CTF target can safely be replaced. */ +static void validateExistingOutput(const std::filesystem::path& directory) { + OutputPath::validateParent(directory, "CTF output"); std::error_code error; - const auto absolute = std::filesystem::absolute(path, error); - return (error ? path : absolute).lexically_normal(); -} - -/** @brief Folds one ASCII character for conservative portable path comparison. */ -static char foldedPathCharacter(char character) -{ - return character >= 'A' && character <= 'Z' ? static_cast(character + ('a' - 'A')) : character; -} - -/** @brief Compares path components without relying on target-platform case rules. */ -static bool pathComponentEquals(const std::filesystem::path& lhs, const std::filesystem::path& rhs) -{ - const auto lhsText = lhs.generic_u8string(); - const auto rhsText = rhs.generic_u8string(); - return lhsText.size() == rhsText.size() && - std::equal(lhsText.begin(), lhsText.end(), rhsText.begin(), [](char lhsCharacter, char rhsCharacter) { - return foldedPathCharacter(lhsCharacter) == foldedPathCharacter(rhsCharacter); - }); -} - -/** @brief Tests whether a candidate path contains another target path. */ -static bool isAncestorPath(const std::filesystem::path& candidate, const std::filesystem::path& path) -{ - auto candidateComponent = candidate.begin(); - auto pathComponent = path.begin(); - for (; candidateComponent != candidate.end(); ++candidateComponent, ++pathComponent) { - if (pathComponent == path.end() || !pathComponentEquals(*candidateComponent, *pathComponent)) { - return false; - } - } - 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) -{ - requireOutputTarget(ctfDirectory, "CTF output directory"); - requireOutputTarget(traceCompassXml, "Trace Compass XML output"); - const auto normalizedCtf = normalizedAbsolutePath(ctfDirectory); - const auto normalizedXml = normalizedAbsolutePath(traceCompassXml); - if (isAncestorPath(normalizedCtf, normalizedXml) || isAncestorPath(normalizedXml, normalizedCtf)) { - throw std::invalid_argument("CTF output directory and Trace Compass XML output must be separate, non-nested paths"); + const auto status = std::filesystem::symlink_status(directory, error); + if (error && error != std::errc::no_such_file_or_directory) { + throw std::runtime_error("Failed to inspect existing CTF output " + directory.string() + ": " + error.message()); } -} - -/** @brief Removes an existing CTF directory without following unsafe targets. */ -static void removeOutputDirectory(const std::filesystem::path& path) -{ - std::error_code error; - std::filesystem::remove_all(path, error); - if (error) { - throw std::runtime_error("Failed to remove existing CTF output " + path.string() + ": " + error.message()); - } -} - -/** @brief Verifies that existing output targets have replaceable filesystem types. */ -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) { - throw std::runtime_error("Failed to inspect existing CTF output " + ctfDirectory.string() + ": " + - ctfError.message()); - } - if (!ctfError && std::filesystem::exists(ctfStatus) && !std::filesystem::is_directory(ctfStatus) && - !std::filesystem::is_symlink(ctfStatus)) { + if (!error && std::filesystem::exists(status) && !std::filesystem::is_directory(status) && + !std::filesystem::is_symlink(status)) { throw std::runtime_error("Refusing to replace CTF output because the target is not a directory: " + - ctfDirectory.string()); - } - - std::error_code xmlError; - const auto xmlStatus = std::filesystem::symlink_status(traceCompassXml, xmlError); - if (xmlError && xmlError != std::errc::no_such_file_or_directory) { - throw std::runtime_error("Failed to inspect existing Trace Compass XML " + traceCompassXml.string() + ": " + - xmlError.message()); - } - if (!xmlError && std::filesystem::is_directory(xmlStatus)) { - throw std::runtime_error("Refusing to replace Trace Compass XML because the target is a directory: " + - traceCompassXml.string()); - } -} - -/** @brief Removes an existing regular output file. */ -static void removeOutputFile(const std::filesystem::path& path) -{ - std::error_code error; - std::filesystem::remove(path, error); - if (error) { - throw std::runtime_error("Failed to remove existing Trace Compass XML " + path.string() + ": " + error.message()); + directory.string()); } } -/** @brief Creates a prepared CTF output directory. */ -static void createOutputDirectory(const std::filesystem::path& path) +/** @brief Removes only the selected CTF directory, without following a target symlink. */ +static void removeOutputDirectory(const std::filesystem::path& directory) { std::error_code error; - std::filesystem::create_directories(path, error); + std::filesystem::remove_all(directory, error); if (error) { - throw std::runtime_error("Failed to create CTF output directory " + path.string() + ": " + error.message()); + throw std::runtime_error("Failed to remove CTF directory " + directory.string() + ": " + error.message()); } } -/** @brief Removes all partial artifacts after an aborted bundle. */ -static void removeIncompleteOutputs(const std::filesystem::path& ctfDirectory, - const std::filesystem::path& traceCompassXml) -{ - std::ostringstream errors; - std::error_code ctfError; - std::filesystem::remove_all(ctfDirectory, ctfError); - if (ctfError) { - errors << "CTF directory " << ctfDirectory.string() << ": " << ctfError.message(); - } - - std::error_code xmlError; - std::filesystem::remove(traceCompassXml, xmlError); - if (xmlError) { - if (errors.tellp() > 0) { - errors << "; "; - } - errors << "Trace Compass XML " << traceCompassXml.string() << ": " << xmlError.message(); - } - if (errors.tellp() > 0) { - throw std::runtime_error("Failed to remove incomplete CTF output: " + errors.str()); - } -} - -/** @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{ std::move(config.metadata), std::move(config.selection), diagnostics, std::move(config.routes), - }), - m_diagnostics(diagnostics) + }) { - validateOutputTargets(m_ctfOutputDirectory, m_traceCompassXmlPath); + OutputPath::requireSpecificTarget(m_ctfOutputDirectory, "CTF output directory"); } CtfBundleOutput::~CtfBundleOutput() @@ -254,80 +69,37 @@ std::string CtfBundleOutput::targetPath() const return m_ctfOutputDirectory.string(); } +const CtfMetadataModel* CtfBundleOutput::completedMetadata() const noexcept +{ + return m_encoder.completedMetadata(); +} + void CtfBundleOutput::prepareOutput() { - validateExistingOutputTypes(m_ctfOutputDirectory, m_traceCompassXmlPath); + validateExistingOutput(m_ctfOutputDirectory); removeOutputDirectory(m_ctfOutputDirectory); - removeOutputFile(m_traceCompassXmlPath); - createOutputDirectory(m_ctfOutputDirectory); + std::error_code error; + std::filesystem::create_directories(m_ctfOutputDirectory, error); + if (error) { + throw std::runtime_error("Failed to create CTF output directory " + m_ctfOutputDirectory.string() + ": " + + error.message()); + } } void CtfBundleOutput::startOutput() { - const auto traceUuid = CtfUuid::randomV4(); - m_encoder.start(m_ctfOutputDirectory, traceUuid); + m_encoder.start(m_ctfOutputDirectory, CtfUuid::randomV4()); } 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) -{ - const auto& streams = metadata.topology().streams; - if (streams.empty()) { - removeOutputFile(m_traceCompassXmlPath); - return; - } - - const auto clockDomainCount = traceCompassClockDomainCount(streams); - if (clockDomainCount != 1U) { - omitTraceCompassXml(clockDomainCount); - return; - } - - const auto viewRoutes = traceCompassViewRoutes(metadata); - const auto hasViews = std::any_of(viewRoutes.begin(), viewRoutes.end(), [](const auto& route) { - return route.views != 0U; - }); - if (!hasViews) { - removeOutputFile(m_traceCompassXmlPath); - return; - } - - if (metadata.isLegacySingleStreamLayout()) { - TraceCompassXmlWriter::writeLegacyFile(m_traceCompassXmlPath, viewRoutes.front().views); - return; - } - TraceCompassXmlWriter::writeRoutedFile(m_traceCompassXmlPath, viewRoutes); -} - -void CtfBundleOutput::omitTraceCompassXml(std::size_t clockDomainCount) -{ - 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); + removeOutputDirectory(m_ctfOutputDirectory); } void CtfBundleOutput::writeOutput(const TraceEvent& event) diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.h b/tools/ctrace/src/output/ctf/CtfBundleOutput.h index 4b734082d..75a1873ad 100644 --- a/tools/ctrace/src/output/ctf/CtfBundleOutput.h +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.h @@ -13,12 +13,11 @@ #include "TraceOutput.h" #include "TraceOutputConfig.h" -#include #include class DiagnosticSink; -/** @brief Owns a CTF directory and its optional companion Trace Compass XML file. */ +/** @brief Owns the lifecycle of one independently completed CTF directory. */ class CtfBundleOutput final : public TraceOutput { public: /** @@ -34,15 +33,17 @@ class CtfBundleOutput final : public TraceOutput { std::string_view backendName() const noexcept override; /** @brief Returns the CTF output directory path. */ std::string targetPath() const override; + /** @brief Returns emitted metadata after a successful stop, or nullptr otherwise. */ + const CtfMetadataModel* completedMetadata() const noexcept; protected: - /** @brief Prepares an empty CTF target and removes stale companion XML. */ + /** @brief Prepares an empty CTF target. */ void prepareOutput() override; /** @brief Starts the CTF encoder for the prepared target. */ void startOutput() override; - /** @brief Completes the bundle and writes XML only for observed views sharing one clock. */ + /** @brief Completes the CTF bundle and publishes its emitted metadata. */ void stopOutput() override; - /** @brief Aborts the encoder and removes incomplete CTF and XML targets. */ + /** @brief Aborts the encoder and removes the incomplete CTF target. */ void abortOutput() override; /** * @brief Encodes one selected semantic event. @@ -51,15 +52,8 @@ class CtfBundleOutput final : public TraceOutput { 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; - DiagnosticSink* m_diagnostics = nullptr; }; #endif // CTRACE_SRC_OUTPUT_CTF_CTFBUNDLEOUTPUT_H diff --git a/tools/ctrace/src/output/ctf/CtfMetadataModel.cpp b/tools/ctrace/src/output/ctf/CtfMetadataModel.cpp index 5781a5208..a297e1e3f 100644 --- a/tools/ctrace/src/output/ctf/CtfMetadataModel.cpp +++ b/tools/ctrace/src/output/ctf/CtfMetadataModel.cpp @@ -173,7 +173,7 @@ bool CtfMetadataModel::isLegacySingleStreamLayout() const noexcept } 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 && + return clock.id == CtfClockDomainId{0U} && clock.name == "swo_clock" && !clock.absolute && stream.streamClassId == CtfStreamClassId{0U} && !stream.route.traceBusId.has_value() && stream.clockDomainId == clock.id; } diff --git a/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp b/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp index 2d9f04fbc..c87e58091 100644 --- a/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp +++ b/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp @@ -229,7 +229,7 @@ static MetadataSymbols collectMetadataSymbols(const std::vectortoString() << "\";\n"; + } + out << R"( precision = 0; offset_s = 0; offset = 0; absolute = false; freq = )" - << coreClockHz << R"(; + << clock.frequencyHz << R"(; }; )"; } @@ -790,7 +794,7 @@ void CtfMetadataWriter::write(const std::filesystem::path& outputDir, const CtfM const auto& topology = model.topology(); if (model.isLegacySingleStreamLayout()) { const auto symbols = collectMetadataSymbols(topology.sources); - writeTraceEnvironment(out, model.traceUuid().toString(), topology.clockDomains.front().frequencyHz, symbols); + writeTraceEnvironment(out, model.traceUuid().toString(), topology.clockDomains.front(), symbols); writeTypeDefinitions(out, symbols, model.observedExceptions(topology.streams.front().streamClassId)); writeStreamDefinition(out); writeItmEvent(out); diff --git a/tools/ctrace/src/output/ctf/TraceCompassXmlOutput.cpp b/tools/ctrace/src/output/ctf/TraceCompassXmlOutput.cpp new file mode 100644 index 000000000..67b075cbe --- /dev/null +++ b/tools/ctrace/src/output/ctf/TraceCompassXmlOutput.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TraceCompassXmlOutput.h" + +#include "CtfMetadataModel.h" +#include "DiagnosticSink.h" +#include "OutputPath.h" + +#include +#include +#include +#include +#include +#include +#include + +/** @brief Validates an XML target without following or modifying an existing symlink. */ +static void validateXmlTarget(const std::filesystem::path& path) +{ + OutputPath::validateParent(path, "Trace Compass XML output"); + std::error_code error; + const auto status = std::filesystem::symlink_status(path, error); + if (error && error != std::errc::no_such_file_or_directory) { + throw std::runtime_error("Failed to inspect existing Trace Compass XML " + path.string() + ": " + error.message()); + } + if (!error && std::filesystem::is_directory(status)) { + throw std::runtime_error("Refusing to replace Trace Compass XML because the target is a directory: " + + path.string()); + } +} + +/** @brief Removes an old XML file or symlink without modifying its destination. */ +static void removeXmlFile(const std::filesystem::path& path) +{ + std::error_code error; + std::filesystem::remove(path, error); + if (error) { + throw std::runtime_error("Failed to remove existing Trace Compass XML " + path.string() + ": " + error.message()); + } +} + +/** @brief Selects only graphical topics backed by emitted records. */ +static TraceCompassXmlWriter::ViewMask observedViews(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 Builds visible, clock-qualified routes without changing the collected output. */ +static std::vector viewRoutes(std::string_view channel, + const CtfMetadataModel& metadata, + const CtfClockDomainDescriptor& clock) +{ + std::vector routes; + for (const auto& stream : metadata.topology().streams) { + const auto views = observedViews(metadata, stream.streamClassId); + if (views == 0U) { + continue; + } + if (!clock.uuid.has_value()) { + throw std::invalid_argument("Trace Compass XML requires an explicit clock UUID for input " + std::string(channel)); + } + auto label = std::string(channel); + if (stream.processorName.has_value() && !stream.processorName->empty()) { + label += " - " + *stream.processorName; + } + routes.push_back({stream.route.traceBusId.value_or(0U), std::move(label), views, clock.uuid->toString()}); + } + return routes; +} + +TraceCompassXmlOutput::TraceCompassXmlOutput(std::filesystem::path path, DiagnosticSink& diagnostics) + : m_path(std::move(path)), m_diagnostics(diagnostics) +{ + OutputPath::requireSpecificTarget(m_path, "Trace Compass XML output"); +} + +void TraceCompassXmlOutput::prepare() +{ + validateXmlTarget(m_path); + removeXmlFile(m_path); + m_routes.clear(); + m_clockUuids.clear(); +} + +void TraceCompassXmlOutput::add(std::string_view channel, const CtfMetadataModel& metadata) +{ + std::set clocks; + for (const auto& stream : metadata.topology().streams) { + clocks.insert(stream.clockDomainId); + } + if (clocks.empty()) { + return; + } + if (clocks.size() != 1U) { + m_diagnostics.report({ + DiagnosticSink::Severity::Warning, + "Trace Compass XML views were not generated for this input because emitted CTF streams use multiple clock domains", + {{"backend", "ctf"}, {"path", m_path.string()}, {"inputChannel", std::string(channel)}, + {"clockDomains", std::to_string(clocks.size())}}, + }); + return; + } + // CtfMetadataModel validates that every stream references an existing clock. + auto routes = viewRoutes(channel, metadata, *metadata.clockDomain(*clocks.begin())); + if (routes.empty()) { + return; + } + if (!m_clockUuids.insert(routes.front().clockUuid).second) { + throw std::invalid_argument("Trace Compass XML requires independent clock UUIDs for separate inputs: " + + std::string(channel)); + } + m_routes.insert(m_routes.end(), std::make_move_iterator(routes.begin()), std::make_move_iterator(routes.end())); +} + +void TraceCompassXmlOutput::finish() +{ + if (m_routes.empty()) { + return; + } + validateXmlTarget(m_path); + removeXmlFile(m_path); + try { + TraceCompassXmlWriter::writeFile(m_path, m_routes); + } catch (...) { + std::error_code ignored; + std::filesystem::remove(m_path, ignored); + throw; + } +} diff --git a/tools/ctrace/src/output/ctf/TraceCompassXmlOutput.h b/tools/ctrace/src/output/ctf/TraceCompassXmlOutput.h new file mode 100644 index 000000000..c63de44cc --- /dev/null +++ b/tools/ctrace/src/output/ctf/TraceCompassXmlOutput.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_CTF_TRACECOMPASSXMLOUTPUT_H +#define CTRACE_SRC_OUTPUT_CTF_TRACECOMPASSXMLOUTPUT_H + +#include "TraceCompassXmlWriter.h" + +#include +#include +#include +#include +#include + +class CtfMetadataModel; +class DiagnosticSink; + +/** @brief Collects completed per-channel views into one solution-set XML file. */ +class TraceCompassXmlOutput final { +public: + /** @brief Selects the target XML and the sink for unsupported-clock warnings. */ + TraceCompassXmlOutput(std::filesystem::path path, DiagnosticSink& diagnostics); + /** @brief Validates the target, removes stale XML, and starts a fresh collection. */ + void prepare(); + /** @brief Collects observed views from one completed, single-clock CTF bundle. */ + void add(std::string_view channel, const CtfMetadataModel& metadata); + /** @brief Writes collected views, removing partial XML if writing fails. */ + void finish(); + +private: + std::filesystem::path m_path; + DiagnosticSink& m_diagnostics; + std::vector m_routes; + std::set m_clockUuids; +}; + +#endif // CTRACE_SRC_OUTPUT_CTF_TRACECOMPASSXMLOUTPUT_H diff --git a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp index a2b9c5b7e..8c0d519c0 100644 --- a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp +++ b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -53,18 +54,16 @@ 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) +/** @brief Separates every event by its capture clock and architectural route. */ +static std::string statePathPrefix() { - return routePrefixed - ? " \n" - " \n" - : ""; + return " \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, bool routePrefixed) + const char* valueAttribute) { std::ostringstream handlers; for (const auto& arm : CtfSchema::ValueVariants) { @@ -79,7 +78,7 @@ static std::string valueHandlers(CtfSchema::EventId eventId, const char* prefix, )"; - handlers << statePathPrefix(routePrefixed); + handlers << statePathPrefix(); handlers << R"( static std::string eventCounterHandlers(CtfSchema::EventId eventId, std::string_view field, - const std::array& counters, CounterName counterName, - bool routePrefixed) + const std::array& counters, CounterName counterName) { std::ostringstream handlers; for (const auto counter : counters) { @@ -116,7 +114,7 @@ static std::string eventCounterHandlers(CtfSchema::EventId eventId, std::string_ )" - << statePathPrefix(routePrefixed) << R"( @@ -134,7 +132,7 @@ static std::string eventCounterHandlers(CtfSchema::EventId eventId, std::string_ )" - << statePathPrefix(routePrefixed) << R"( @@ -151,12 +149,12 @@ static std::string eventCounterHandlers(CtfSchema::EventId eventId, std::string_ } /** @brief Generates one visible pulse for every comparator-only DWT match. */ -static std::string dwtMatchHandler(bool routePrefixed) +static std::string dwtMatchHandler() { std::ostringstream handler; handler << R"( )"; - handler << statePathPrefix(routePrefixed); + handler << statePathPrefix(); handler << R"( @@ -164,7 +162,7 @@ static std::string dwtMatchHandler(bool routePrefixed) )"; - handler << statePathPrefix(routePrefixed); + handler << statePathPrefix(); handler << R"( @@ -179,7 +177,7 @@ static std::string dwtMatchHandler(bool routePrefixed) } /** @brief Generates one DWT address handler for each encoded data-address width. */ -static std::string dwtAddressHandlers(bool routePrefixed) +static std::string dwtAddressHandlers() { std::ostringstream handlers; static_assert(CtfSchema::DwtAddressVariants.front().tag == CtfSchema::DwtAddressTag::None); @@ -195,7 +193,7 @@ static std::string dwtAddressHandlers(bool routePrefixed) )"; - handlers << statePathPrefix(routePrefixed); + handlers << statePathPrefix(); handlers << R"( @@ -226,7 +224,7 @@ static void writeStateHandler(std::ostream& xml, CtfSchema::EventId eventId, con } /** @brief Writes exception-entry state changes. */ -static void writeExceptionEnteredStateChanges(std::ostream& xml, bool routePrefixed) +static void writeExceptionEnteredStateChanges(std::ostream& xml) { xml << R"( @@ -237,7 +235,7 @@ static void writeExceptionEnteredStateChanges(std::ostream& xml, bool routePrefi )"; - xml << statePathPrefix(routePrefixed); + xml << statePathPrefix(); xml << R"( @@ -254,7 +252,7 @@ static void writeExceptionEnteredStateChanges(std::ostream& xml, bool routePrefi )" - << statePathPrefix(routePrefixed) << R"( + << statePathPrefix() << R"( @@ -264,7 +262,7 @@ static void writeExceptionEnteredStateChanges(std::ostream& xml, bool routePrefi } /** @brief Writes exception-exit state changes. */ -static void writeExceptionExitedStateChanges(std::ostream& xml, bool routePrefixed) +static void writeExceptionExitedStateChanges(std::ostream& xml) { xml << R"( @@ -275,7 +273,7 @@ static void writeExceptionExitedStateChanges(std::ostream& xml, bool routePrefix )"; - xml << statePathPrefix(routePrefixed); + xml << statePathPrefix(); xml << R"( @@ -298,7 +296,7 @@ static void writeExceptionExitedStateChanges(std::ostream& xml, bool routePrefix )" - << statePathPrefix(routePrefixed) << R"( + << statePathPrefix() << R"( @@ -308,7 +306,7 @@ static void writeExceptionExitedStateChanges(std::ostream& xml, bool routePrefix } /** @brief Writes exception-return state changes. */ -static void writeExceptionReturnedStateChanges(std::ostream& xml, bool routePrefixed) +static void writeExceptionReturnedStateChanges(std::ostream& xml) { xml << R"( @@ -319,7 +317,7 @@ static void writeExceptionReturnedStateChanges(std::ostream& xml, bool routePref )"; - xml << statePathPrefix(routePrefixed); + xml << statePathPrefix(); xml << R"( @@ -336,7 +334,7 @@ static void writeExceptionReturnedStateChanges(std::ostream& xml, bool routePref )" - << statePathPrefix(routePrefixed) << R"( + << statePathPrefix() << R"( @@ -346,17 +344,17 @@ static void writeExceptionReturnedStateChanges(std::ostream& xml, bool routePref } /** @brief Writes state changes that track exception nesting and returns. */ -static void writeExceptionStateHandler(std::ostream& xml, bool routePrefixed) +static void writeExceptionStateHandler(std::ostream& xml) { std::ostringstream stateChanges; - writeExceptionEnteredStateChanges(stateChanges, routePrefixed); - writeExceptionExitedStateChanges(stateChanges, routePrefixed); - writeExceptionReturnedStateChanges(stateChanges, routePrefixed); + writeExceptionEnteredStateChanges(stateChanges); + writeExceptionExitedStateChanges(stateChanges); + writeExceptionReturnedStateChanges(stateChanges); writeStateHandler(xml, CtfSchema::EventId::Exception, stateChanges.str()); } /** @brief Writes state changes that expose processor sleep intervals. */ -static void writeProcessorStateHandler(std::ostream& xml, bool routePrefixed) +static void writeProcessorStateHandler(std::ostream& xml) { std::ostringstream stateChanges; stateChanges << R"( @@ -369,7 +367,7 @@ static void writeProcessorStateHandler(std::ostream& xml, bool routePrefixed) )"; - stateChanges << statePathPrefix(routePrefixed); + stateChanges << statePathPrefix(); stateChanges << R"( @@ -387,7 +385,7 @@ static void writeProcessorStateHandler(std::ostream& xml, bool routePrefixed) )" - << statePathPrefix(routePrefixed) << R"( @@ -399,7 +397,7 @@ static void writeProcessorStateHandler(std::ostream& xml, bool routePrefixed) std::ostringstream prohibitedStateChange; prohibitedStateChange << R"( )" - << statePathPrefix(routePrefixed) << R"( @@ -409,7 +407,7 @@ static void writeProcessorStateHandler(std::ostream& xml, bool routePrefixed) } /** @brief Writes the processor-state reset performed after discontinuities. */ -static void writeProcessorDiscontinuityStateChange(std::ostream& xml, bool routePrefixed) +static void writeProcessorDiscontinuityStateChange(std::ostream& xml) { xml << R"( @@ -426,7 +424,7 @@ static void writeProcessorDiscontinuityStateChange(std::ostream& xml, bool route )" - << statePathPrefix(routePrefixed) << R"( @@ -436,7 +434,7 @@ static void writeProcessorDiscontinuityStateChange(std::ostream& xml, bool route } /** @brief Writes the exception-return reset performed after discontinuities. */ -static void writeExceptionDiscontinuityStateChanges(std::ostream& xml, bool routePrefixed) +static void writeExceptionDiscontinuityStateChanges(std::ostream& xml) { xml << R"( @@ -447,7 +445,7 @@ static void writeExceptionDiscontinuityStateChanges(std::ostream& xml, bool rout )"; - xml << statePathPrefix(routePrefixed); + xml << statePathPrefix(); xml << R"( @@ -463,7 +461,7 @@ static void writeExceptionDiscontinuityStateChanges(std::ostream& xml, bool rout )" - << statePathPrefix(routePrefixed) << R"( + << statePathPrefix() << R"( @@ -473,7 +471,7 @@ static void writeExceptionDiscontinuityStateChanges(std::ostream& xml, bool rout } /** @brief Writes state resets for trace discontinuities. */ -static void writeDiscontinuityStateHandler(std::ostream& xml, bool routePrefixed, bool exceptionView, +static void writeDiscontinuityStateHandler(std::ostream& xml, bool exceptionView, bool processorStateView) { if (!exceptionView && !processorStateView) { @@ -482,53 +480,126 @@ static void writeDiscontinuityStateHandler(std::ostream& xml, bool routePrefixed std::ostringstream stateChanges; if (processorStateView) { - writeProcessorDiscontinuityStateChange(stateChanges, routePrefixed); + writeProcessorDiscontinuityStateChange(stateChanges); } if (exceptionView) { - writeExceptionDiscontinuityStateChanges(stateChanges, routePrefixed); + writeExceptionDiscontinuityStateChanges(stateChanges); } writeStateHandler(xml, CtfSchema::EventId::TraceStatus, stateChanges.str()); } +/** @brief Names one imported analysis and all of its graphical providers. */ +struct AnalysisIdentity { + std::string nameSpace; + std::string providerId; +}; + +/** @brief Accepts exactly the canonical UUID spelling emitted by CtfUuid::toString. */ +static bool isCanonicalClockUuid(std::string_view uuid) +{ + if (uuid.size() != 36U) { + return false; + } + for (std::size_t index = 0U; index < uuid.size(); ++index) { + const auto character = uuid[index]; + if (index == 8U || index == 13U || index == 18U || index == 23U) { + if (character != '-') { + return false; + } + } else if (!((character >= '0' && character <= '9') || (character >= 'a' && character <= 'f'))) { + return false; + } + } + return true; +} + +/** @brief Rejects route values that cannot safely identify one state-system path. */ +static void validateViewRoute(const TraceCompassXmlWriter::ViewRoute& route) +{ + if ((route.views & ~TraceCompassXmlWriter::AllViews) != 0U) { + throw std::invalid_argument("Trace Compass XML contains an unsupported route view selection"); + } + if (route.traceBusId != 0U && !CoreSight::isAtbTraceId(route.traceBusId)) { + throw std::invalid_argument("Trace Compass XML requires Trace Bus IDs between 0 and 111"); + } + if (!isCanonicalClockUuid(route.clockUuid)) { + throw std::invalid_argument("Trace Compass XML requires canonical lower-case clock UUIDs"); + } +} + +/** @brief Validates routes and derives an order-independent namespace from their identities. */ +static AnalysisIdentity analysisIdentity(const std::vector& routes) +{ + if (routes.empty()) { + throw std::invalid_argument("Trace Compass XML requires at least one view route"); + } + std::set> identities; + auto views = TraceCompassXmlWriter::ViewMask{0U}; + for (const auto& route : routes) { + validateViewRoute(route); + if (!identities.emplace(route.clockUuid, route.traceBusId).second) { + throw std::invalid_argument("Trace Compass XML requires unique clock UUID and Trace Bus ID pairs"); + } + views |= route.views; + } + if (views == 0U) { + throw std::invalid_argument("Trace Compass XML requires at least one graphical view"); + } + + constexpr std::uint64_t kFnvOffsetBasis = 14695981039346656037ULL; + constexpr std::uint64_t kFnvPrime = 1099511628211ULL; + auto hash = kFnvOffsetBasis; + for (const auto& [clockUuid, traceBusId] : identities) { + const auto key = clockUuid + '/' + std::to_string(traceBusId) + ';'; + for (const auto character : key) { + hash ^= static_cast(character); + hash *= kFnvPrime; + } + } + std::ostringstream nameSpace; + nameSpace << std::hex << hash; + const auto suffix = nameSpace.str(); + return {suffix, "arm.cmsis.ctrace.analysis." + suffix + ".v1"}; +} + /** @brief Generates the Trace Compass state-provider definition. */ -static std::string stateProviderXml(bool routePrefixed, TraceCompassXmlWriter::ViewMask views) +static std::string stateProviderXml(const AnalysisIdentity& identity, 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"( -