diff --git a/tools/ctrace/README.md b/tools/ctrace/README.md index f0faf09df..446e0fc06 100644 --- a/tools/ctrace/README.md +++ b/tools/ctrace/README.md @@ -43,7 +43,7 @@ For `ctrace .trace --target Board --all`, the supported input produces: Board.ctf/ metadata stream_0 - Board.SWO.traceanalysis.xml # when retained streams use one clock domain; views are data-driven + Board.SWO.traceanalysis.xml # only with graphical data and one retained clock domain ``` Without an explicit format declaration, ctrace preserves the legacy SWO-only diff --git a/tools/ctrace/docs/architecture.md b/tools/ctrace/docs/architecture.md index 308656615..6d911942e 100644 --- a/tools/ctrace/docs/architecture.md +++ b/tools/ctrace/docs/architecture.md @@ -212,7 +212,8 @@ lazily creates a stream writer for each formatted route that emits selected even 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. -This avoids empty views and invented durations for point events. Route identity stays separate from display labels, +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. diff --git a/tools/ctrace/docs/constraints.md b/tools/ctrace/docs/constraints.md index f22cfda99..254c20b9f 100644 --- a/tools/ctrace/docs/constraints.md +++ b/tools/ctrace/docs/constraints.md @@ -100,9 +100,11 @@ and producer integration remain tracked as unfinished work. - CSV remains one combined file in semantic callback order. The unformatted route has an empty `stream` field; formatted routes expose their architectural IDs. Type and stream filters affect output, not decoding or diagnostic - reporting. Ctrace deliberately names the seventh CSV column `address`; the currently published CMSIS-Toolbox trace - specification still says `offset`, and must be corrected to match this intended schema before the difference is - treated as standardized. + reporting. The seventh CSV column is `address`, as defined by the CMSIS-Toolbox trace specification. +- PC sampling distinguishes four-byte PC values from the one-byte `0x00` (`CPU Sleeping`) and Armv8-M `0xff` + (`Trace prohibited`) status markers. CSV leaves `pc` empty for both markers and writes their meaning in `note`; + all three remain selectable as `pcsample`. Markers preserve route, timestamp, and quality without creating an error + 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. @@ -117,8 +119,10 @@ and producer integration remain tracked as unfinished work. DWT/PMU overflow events, and processor sleep state as time graphs. Each block is emitted only if the completed stream contains matching trace data; synthetic exception bootstrap records alone do not enable an exception block, and `Processor State` specifically requires a sleep indication. ITM payloads, ordinary sampled - PCs, and trace-status records stay available through the CTF event table. Trace Compass XML has no data-driven - table-view type, so ctrace does not model these point records as artificial timelines. + 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; + 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 diff --git a/tools/ctrace/docs/ctf-format.md b/tools/ctrace/docs/ctf-format.md index 9f1f95f01..fe4e02e27 100644 --- a/tools/ctrace/docs/ctf-format.md +++ b/tools/ctrace/docs/ctf-format.md @@ -10,7 +10,8 @@ authoritative for trace input and `*.ctrace-run.yml` configuration. - DWT data trace: [`DWT_VALUE`](#dwt_value-event-id-1), [`DWT_ADDR`](#dwt_addr-event-id-2), and [`DWT_MATCH`](#dwt_match-event-id-9) - Trace integrity: [`TRACE_STATUS`](#trace_status-event-id-3) -- Execution state: [`EXCEPTION`](#exception-event-id-4) and [`PC_SAMPLE`](#pc_sample-event-id-6) +- Execution state: [`EXCEPTION`](#exception-event-id-4), [`PC_SAMPLE`](#pc_sample-event-id-6), and + [`PC_SAMPLE_PROHIBITED`](#pc_sample_prohibited-event-id-10) - Time correlation: [`GLOBAL_TIMESTAMP`](#global_timestamp-event-id-5) - Profiling: [`DWT_EVENT`](#dwt_event-event-id-7) and [`PMU_EVENT`](#pmu_event-event-id-8) @@ -71,9 +72,9 @@ The timestamp fields use the stream class's clock mapping. Trace loss is represe than the CTF `events_discarded` counter, which is currently zero. The optional `..traceanalysis.xml` companion is stored next to the bundle. It is generated -only when the completed metadata retains at least one stream and all retained streams reference one clock domain, -because the supported Trace Compass reader cannot safely combine independent clocks. This limitation affects only -the generated visualization; a metadata-only or multi-clock CTF bundle remains valid. +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. ## Event catalogue @@ -89,6 +90,7 @@ the generated visualization; a metadata-only or multi-clock CTF bundle remains v | Profiling | 7 | `DWT_EVENT` | Architectural DWT event-counter overflow | | Profiling | 8 | `PMU_EVENT` | Programmable PMU counter overflow | | DWT data trace | 9 | `DWT_MATCH` | Comparator match without additional data | +| Execution state | 10 | `PC_SAMPLE_PROHIBITED` | PC sampling reports that trace is prohibited | ## Common sample fields @@ -264,8 +266,27 @@ State `0` denotes processor sleep and leaves the PC array empty. State `1` denot PC samples are point observations and are available in the CTF event table. Trace Compass does not invent execution duration between sampled PCs. If a sleep indication was emitted, the generated `Processor State` view opens a -`Sleep` interval; the next PC sample, overflow, or data loss closes it. Ordinary PC samples alone do not create this -view. +`Sleep` interval; the next PC sample, trace-prohibited marker, overflow, or data loss closes it. Ordinary PC samples +alone do not create this view. + +### PC_SAMPLE_PROHIBITED (event ID 10) + +```text +uint8_t cmsis_sample_flags +uint32_t cmsis_overflow_count +``` + +The Armv8-M one-byte DWT PC-sampling payload `0xff` reports `Trace prohibited`. It is a valid point observation, +not a PC address, decoder error, or data-loss boundary. Its timestamp and sample quality are preserved; the marker +does not increment the overflow count or reset exception state. + +This additive event keeps the existing `PC_SAMPLE` binary layout and profile version unchanged. Adding another +`cmsis_pc_sample_state` value instead would change the PC array length and misalign consumers. Both event kinds remain +part of the `pcsample` output selection. + +The event is visible in the CTF event table. If a `Processor State` view exists, it closes any open `Sleep` interval +for that route, leaving the state unknown. It neither establishes running state nor creates a trace-prohibited +duration or a graphical view by itself. ## Time-correlation events @@ -325,11 +346,13 @@ events as generated XML tables. The companion XML contains only graphical views - XY views for DWT values and data-address fragments. - Time graphs for DWT matches, DWT and PMU counter pulses, decoded exception activity, and processor sleep. -ITM values, trace-status records, Global Timestamps, and ordinary PC samples stay in the standard event table because -they do not establish a duration. Each generalized route receives a separate graphical view. Its visible suffix is -the resolved processor name, if available; numeric Trace Bus IDs are omitted from visible labels but remain in the +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 public CTF event context and internally in provider IDs and state queries. Routes and topics without corresponding -emitted data do not add graphical views. +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. 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 diff --git a/tools/ctrace/src/decode/DwtPacketDecoder.cpp b/tools/ctrace/src/decode/DwtPacketDecoder.cpp index 508a7d472..0a933ce76 100644 --- a/tools/ctrace/src/decode/DwtPacketDecoder.cpp +++ b/tools/ctrace/src/decode/DwtPacketDecoder.cpp @@ -62,6 +62,23 @@ static bool isSupportedAddressFragmentSize(std::uint8_t size) return size == 1U || size == 2U || size == 4U; } +/** @brief Classifies PC-sample payloads without confusing a four-byte PC with a one-byte marker. */ +static std::optional pcSampleKind(const DwtPayloadPacket& payload) +{ + if (payload.size == 4U) { + return PcSampleKind::Pc; + } + if (payload.size == 1U) { + if (payload.value == 0U) { + return PcSampleKind::Sleep; + } + if (payload.value == 0xffU) { + return PcSampleKind::TraceProhibited; + } + } + return std::nullopt; +} + /** @brief Describes an invalid DWT event-counter payload. */ static std::string invalidEventCounterMessage(const DwtPayloadPacket& payload) { @@ -180,21 +197,21 @@ std::vector DwtPacketDecoder::decodeExceptionTrace(const DwtPayloadP std::vector DwtPacketDecoder::decodePeriodicPcSample(const DwtPayloadPacket& payload) { auto output = flush(payload.quality, payload.tcyc); - const auto isPc = payload.size == 4U; - const auto isSleeping = payload.size == 1U && payload.value == 0U; - if (!isPc && !isSleeping) { + const auto kind = pcSampleKind(payload); + if (!kind.has_value()) { output.push_back(makeDwtEvent(payload, TraceIssueEvent{ TraceIssueCode::UnsupportedDwtPcSamplePayload, TraceIssueSeverity::Error, "unsupported DWT PC-sample payload: size " + std::to_string(payload.size) + ", value " + std::to_string(payload.value) + - "; expected a 4-byte PC or a 1-byte zero sleep indication", + "; expected a 4-byte PC or a 1-byte marker (0x00: CPU Sleeping, 0xff: Trace prohibited)", std::nullopt, std::nullopt, })); return output; } - output.push_back(makeDwtEvent(payload, PcSampleTraceEvent{payload.value, isSleeping})); + const auto pc = kind.value() == PcSampleKind::Pc ? payload.value : 0U; + output.push_back(makeDwtEvent(payload, PcSampleTraceEvent{pc, kind.value()})); return output; } diff --git a/tools/ctrace/src/model/TraceEvent.h b/tools/ctrace/src/model/TraceEvent.h index 71bd93f41..8e6bbb2ac 100644 --- a/tools/ctrace/src/model/TraceEvent.h +++ b/tools/ctrace/src/model/TraceEvent.h @@ -217,10 +217,17 @@ struct PmuTraceEvent { std::uint8_t overflowMask = 0; }; -/** @brief Contains a periodic DWT PC sample or its processor-sleep indication. */ +/** @brief Distinguishes a sampled PC from the status markers carried by the same DWT packet. */ +enum class PcSampleKind { + Pc, + Sleep, + TraceProhibited, +}; + +/** @brief Contains a periodic DWT PC sample or status marker; pc is meaningful only for kind Pc. */ struct PcSampleTraceEvent { std::uint32_t pc = 0; - bool sleeping = false; + PcSampleKind kind = PcSampleKind::Pc; }; /** @brief Marks a decoded local timestamp packet. */ diff --git a/tools/ctrace/src/output/csv/CsvRowMapper.cpp b/tools/ctrace/src/output/csv/CsvRowMapper.cpp index 01e4b5df1..ecb54b803 100644 --- a/tools/ctrace/src/output/csv/CsvRowMapper.cpp +++ b/tools/ctrace/src/output/csv/CsvRowMapper.cpp @@ -187,8 +187,16 @@ static void writePayloadColumns(CsvRow& row, const PmuTraceEvent& event) /** @brief Writes one periodic PC sample to the CSV event columns. */ static void writePayloadColumns(CsvRow& row, const PcSampleTraceEvent& event) { - if (!event.sleeping) { + switch (event.kind) { + case PcSampleKind::Pc: row[column(CsvColumn::Pc)] = hexValue(event.pc, 4U); + break; + case PcSampleKind::Sleep: + row[column(CsvColumn::Note)] = "CPU Sleeping"; + break; + case PcSampleKind::TraceProhibited: + row[column(CsvColumn::Note)] = "Trace prohibited"; + break; } } diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp index 1929b34d9..6a5d017c6 100644 --- a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp @@ -291,12 +291,20 @@ void CtfBundleOutput::finalizeTraceCompassXml(const CtfMetadataModel& metadata) 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, - traceCompassViews(metadata, streams.front().streamClassId)); + TraceCompassXmlWriter::writeLegacyFile(m_traceCompassXmlPath, viewRoutes.front().views); return; } - TraceCompassXmlWriter::writeRoutedFile(m_traceCompassXmlPath, traceCompassViewRoutes(metadata)); + TraceCompassXmlWriter::writeRoutedFile(m_traceCompassXmlPath, viewRoutes); } void CtfBundleOutput::omitTraceCompassXml(std::size_t clockDomainCount) diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.h b/tools/ctrace/src/output/ctf/CtfBundleOutput.h index 08e368c4a..4b734082d 100644 --- a/tools/ctrace/src/output/ctf/CtfBundleOutput.h +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.h @@ -18,7 +18,7 @@ class DiagnosticSink; -/** @brief Owns a CTF directory and its companion Trace Compass XML file. */ +/** @brief Owns a CTF directory and its optional companion Trace Compass XML file. */ class CtfBundleOutput final : public TraceOutput { public: /** @@ -40,7 +40,7 @@ class CtfBundleOutput final : public TraceOutput { void prepareOutput() override; /** @brief Starts the CTF encoder for the prepared target. */ void startOutput() override; - /** @brief Completes metadata and streams, then writes XML when their clocks permit it. */ + /** @brief Completes the bundle and writes XML only for observed views sharing one clock. */ void stopOutput() override; /** @brief Aborts the encoder and removes incomplete CTF and XML targets. */ void abortOutput() override; diff --git a/tools/ctrace/src/output/ctf/CtfEncoder.cpp b/tools/ctrace/src/output/ctf/CtfEncoder.cpp index a01443bf8..54cb1bc19 100644 --- a/tools/ctrace/src/output/ctf/CtfEncoder.cpp +++ b/tools/ctrace/src/output/ctf/CtfEncoder.cpp @@ -353,25 +353,31 @@ const CtfMetadataModel* CtfEncoder::completedMetadata() const noexcept void CtfEncoder::writePcSampleEvent(const TraceEvent& event, const PcSampleTraceEvent& sample) { - const auto pcSize = sample.sleeping ? 0U : 4U; - const auto payloadSize = 1U + pcSize + 1U + 4U; + const auto isPc = sample.kind == PcSampleKind::Pc; + const auto isSleeping = sample.kind == PcSampleKind::Sleep; + const auto isProhibited = sample.kind == PcSampleKind::TraceProhibited; + // A separate event preserves PC_SAMPLE's zero-or-one PC sequence length. + const auto pcSize = isPc ? 4U : 0U; + const auto payloadSize = (isProhibited ? 0U : 1U) + pcSize + 1U + 4U; const auto eventTimestamp = allocateEventTimestamp(event.route); const auto traceBusId = legacyCtfTraceBusId(event.route); const auto quality = computeSampleQuality(event); - const auto state = CtfSchema::value(sample.sleeping ? CtfSchema::PcSampleState::Sleep - : CtfSchema::PcSampleState::Pc); + const auto eventId = isProhibited ? CtfSchema::EventId::PcSampleProhibited : CtfSchema::EventId::PcSample; streamWriter(event.route) - .writeRecord(CtfSchema::value(CtfSchema::EventId::PcSample), eventTimestamp, traceBusId, payloadSize, + .writeRecord(CtfSchema::value(eventId), eventTimestamp, traceBusId, payloadSize, [&](CtfStreamWriter::Record& record) { - record.writeU8(state); - if (!sample.sleeping) { + if (!isProhibited) { + record.writeU8(CtfSchema::value(isSleeping ? CtfSchema::PcSampleState::Sleep + : CtfSchema::PcSampleState::Pc)); + } + if (isPc) { record.writeU32(sample.pc); } record.writeU8(quality.first); record.writeU32(quality.second); }); const auto streamClassId = streamDescriptor(event.route).streamClassId; - if (sample.sleeping) { + if (isSleeping) { m_metadata->observeGraphicalTopic(streamClassId, CtfGraphicalTopic::ProcessorState); } } diff --git a/tools/ctrace/src/output/ctf/CtfEncoder.h b/tools/ctrace/src/output/ctf/CtfEncoder.h index 660312bcb..2b26304fd 100644 --- a/tools/ctrace/src/output/ctf/CtfEncoder.h +++ b/tools/ctrace/src/output/ctf/CtfEncoder.h @@ -102,7 +102,7 @@ class CtfEncoder final { void writeDwtEvent(const TraceEvent& event, const DwtEventTraceEvent& counters); /** @brief Expands one PMU trace-on-overflow mask into individual CTF records. */ void writePmuEvent(const TraceEvent& event, const PmuTraceEvent& counters); - /** @brief Encodes one periodic PC-sample or processor-sleep event. */ + /** @brief Encodes one periodic PC sample, sleep indication, or trace-prohibited marker. */ void writePcSampleEvent(const TraceEvent& event, const PcSampleTraceEvent& sample); /** @brief Encodes one reconstructed global timestamp event. */ void writeGlobalTimestampEvent(const TraceEvent& event, const GlobalTimestampTraceEvent& timestamp); diff --git a/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp b/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp index 7d5ded373..2d9f04fbc 100644 --- a/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp +++ b/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp @@ -563,6 +563,25 @@ event { )"; } +/** @brief Writes the trace-prohibited PC-sampling marker declaration. */ +static void writePcSampleProhibitedEvent(std::ostream& out, std::uint32_t streamClassId = CtfSchema::SwoStreamId) +{ + out << R"( +event { + id = )" + << CtfSchema::value(CtfSchema::EventId::PcSampleProhibited) << R"(; + name = ")" + << CtfSchema::eventName(CtfSchema::EventId::PcSampleProhibited) << R"("; + stream_id = )" + << streamClassId << R"(; + fields := struct { + uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; +)"; +} + /** @brief Writes status, exception, and global timestamp declarations. */ static void writeStatusEvents(std::ostream& out, std::uint32_t streamClassId = CtfSchema::SwoStreamId, std::string_view exceptionType = "cmsis_exception_number_t") @@ -756,6 +775,7 @@ static void writeGeneralStreamSchemas(std::ostream& out, const CtfMetadataModel& writePmuEvent(out, streamClassId); writeStatusEvents(out, streamClassId, prefix + "_exception_number_t"); writePcSampleEvent(out, streamClassId); + writePcSampleProhibitedEvent(out, streamClassId); } } @@ -781,6 +801,7 @@ void CtfMetadataWriter::write(const std::filesystem::path& outputDir, const CtfM writePmuEvent(out); writeStatusEvents(out); writePcSampleEvent(out); + writePcSampleProhibitedEvent(out); } else { writeGeneralTraceEnvironment(out, model); writeGeneralCommonTypes(out, model); diff --git a/tools/ctrace/src/output/ctf/CtfSchema.h b/tools/ctrace/src/output/ctf/CtfSchema.h index 2bd7f5cf9..4cf2ed93a 100644 --- a/tools/ctrace/src/output/ctf/CtfSchema.h +++ b/tools/ctrace/src/output/ctf/CtfSchema.h @@ -32,6 +32,7 @@ enum class EventId : std::uint32_t { DwtEvent = 7U, PmuEvent = 8U, DwtMatch = 9U, + PcSampleProhibited = 10U, }; /** @brief Classifies CTF trace-status records. */ @@ -62,7 +63,7 @@ enum class ExceptionOrigin : std::uint8_t { Synthetic = 1U, }; -/** @brief Identifies whether a periodic PC sample carries a PC or reports processor sleep. */ +/** @brief Encodes the zero-or-one PC sequence length in PC_SAMPLE records. */ enum class PcSampleState : std::uint8_t { Sleep = 0U, Pc = 1U, @@ -297,6 +298,8 @@ constexpr std::string_view eventName(EventId id) return "PMU_EVENT"; case EventId::DwtMatch: return "DWT_MATCH"; + case EventId::PcSampleProhibited: + return "PC_SAMPLE_PROHIBITED"; } return "UNKNOWN"; } diff --git a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp index 27d3a1012..a2b9c5b7e 100644 --- a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp +++ b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp @@ -395,6 +395,17 @@ static void writeProcessorStateHandler(std::ostream& xml, bool routePrefixed) )"; writeStateHandler(xml, CtfSchema::EventId::PcSample, stateChanges.str()); + + std::ostringstream prohibitedStateChange; + prohibitedStateChange << R"( +)" + << statePathPrefix(routePrefixed) << R"( + + + +)"; + writeStateHandler(xml, CtfSchema::EventId::PcSampleProhibited, prohibitedStateChange.str()); } /** @brief Writes the processor-state reset performed after discontinuities. */ diff --git a/tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.ctf/metadata b/tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.ctf/metadata index 65679754f..f0400a3cf 100644 --- a/tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.ctf/metadata +++ b/tools/ctrace/test/data/Blinky+Arm/expected/Blinky+Arm.ctf/metadata @@ -308,3 +308,13 @@ event { uint32_t cmsis_overflow_count; }; }; + +event { + id = 10; + name = "PC_SAMPLE_PROHIBITED"; + stream_id = 0; + fields := struct { + uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; diff --git a/tools/ctrace/test/data/README.md b/tools/ctrace/test/data/README.md index 7127f6477..2fb1eff60 100644 --- a/tools/ctrace/test/data/README.md +++ b/tools/ctrace/test/data/README.md @@ -13,12 +13,14 @@ comparison test. ## Fixture integrity The [fixture manifest](../integration/src/ValidateFixtureIntegrity.cmake) is -the canonical SHA-256 and size inventory for checked-in fixtures, including -fixture-local provenance documents. `CtraceFixtureIntegrity` checks that the -inventory is complete and the reconstructed TB capture contains 256 frames. -Update the manifest in the same review as a fixture change. Inputs generated -at test runtime are defined and checked by the integration tests, not listed -in this manifest. +the canonical SHA-256 and size inventory for checked-in test inputs, reference +outputs and fixture scripts. Markdown documentation is excluded. +`CtraceFixtureIntegrity` checks that the inventory is complete and the +reconstructed TB capture contains 256 frames. Update the manifest in the same +review as a fixture change. Tests work on copies in the build tree; the manifest +guards against unintended changes to the versioned fixtures, not test-time +mutation. Inputs generated at test runtime are defined and checked by the +integration tests, not listed in this manifest. ## Blinky reference outputs @@ -106,6 +108,11 @@ packet for each comparator 0 through 3 and local timestamps. The integration test verifies the generated CSV rows, CTF records, labels, and Trace Compass timeline configuration. +[trace-pc-sample](trace-pc-sample/README.md) is a synthetic PC/sleep/trace-prohibited/PC +sequence with local timestamps. Tests exercise unformatted SWO and two-route +formatted TB input, output modes, filtering and an independent Babeltrace +consumer. The marker is valid status information, not a decoder error. + ## Reader and entry-point inputs `trace-run` contains only the small current-schema inputs needed by executable diff --git a/tools/ctrace/test/data/trace-pc-sample/README.md b/tools/ctrace/test/data/trace-pc-sample/README.md new file mode 100644 index 000000000..146e6fc77 --- /dev/null +++ b/tools/ctrace/test/data/trace-pc-sample/README.md @@ -0,0 +1,28 @@ + + +# PC Sampling Marker Fixture + +This synthetic Armv8-M ITM/DWT byte stream is not a hardware capture. It tests +the [PC sampling marker contract][markers]: one-byte payload `0x00` means +`CPU Sleeping`; one-byte payload `0xFF` means `Trace prohibited`. Neither +marker carries a PC address or indicates a damaged packet. + +[markers]: https://open-cmsis-pack.github.io/cmsis-toolbox/Experimental-Features/#pc-sampling-markers + +The complete 24-byte `trace-pc-sample.raw` consists of: + +| Bytes | Meaning | Timestamp after local timestamp | +| --- | --- | --- | +| `00 00 00 00 00 80` | ITM hardware synchronization | 0 | +| `17 34 12 00 08 10` | PC `0x08001234`, local increment 1 | 1 | +| `15 00 20` | CPU sleeping, local increment 2 | 3 | +| `15 FF 30` | Trace prohibited, local increment 3 | 6 | +| `17 78 56 00 08 40` | PC `0x08005678`, local increment 4 | 10 | + +The YAML declares a 1 MHz clock and prescaler 1. See the +[test-data overview](../README.md#other-decoder-fixtures) for test coverage and +the [integration-test documentation](../../integration/README.md) for Babeltrace +and Trace Compass validation. diff --git a/tools/ctrace/test/data/trace-pc-sample/trace-pc-sample.ctrace-run.yml b/tools/ctrace/test/data/trace-pc-sample/trace-pc-sample.ctrace-run.yml new file mode 100644 index 000000000..1bd11841b --- /dev/null +++ b/tools/ctrace/test/data/trace-pc-sample/trace-pc-sample.ctrace-run.yml @@ -0,0 +1,7 @@ +ctrace-run: + generated-by: synthetic Armv8-M PC sampling marker fixture + ctrace-setup: + - timestamps: + clock: 1000000 + itm-prescaler: 1 + ctrace-refs: [] diff --git a/tools/ctrace/test/data/trace-pc-sample/trace-pc-sample.raw b/tools/ctrace/test/data/trace-pc-sample/trace-pc-sample.raw new file mode 100644 index 000000000..a18783088 Binary files /dev/null and b/tools/ctrace/test/data/trace-pc-sample/trace-pc-sample.raw differ diff --git a/tools/ctrace/test/integration/README.md b/tools/ctrace/test/integration/README.md index 722ae5e76..c057f6f18 100644 --- a/tools/ctrace/test/integration/README.md +++ b/tools/ctrace/test/integration/README.md @@ -30,6 +30,11 @@ scales a 25,729-tick stream-1 sample at 240 MHz to `0.000107204` seconds and a that Babeltrace's default whole-bundle mux rejects the two distinct clock UUIDs instead of inventing a global event order. +A second fixture exercises PC sampling at 1 MHz: PC, sleep marker, trace-prohibited +marker, then PC. Babeltrace must read exactly those four records at 1, 3, 6 and +10 microseconds. This independently checks the unchanged `PC_SAMPLE` layout and +the additive `PC_SAMPLE_PROHIBITED` event, including the record following it. + ## Trace Compass acceptance The data-driven XML output was accepted on 2026-09-11 against Trace Compass @@ -125,3 +130,24 @@ No companion `Blinky+Arm.TB.traceanalysis.xml` existed, the server's XML configuration list was empty, and its output list contained no `arm.cmsis.swo.*` provider. Trace Compass therefore imported all event-table data without constructing an invalid cross-clock graphical timeline. + +### PC-sampling markers (2026-09-21) + +The release build's [synthetic marker fixture](../data/trace-pc-sample/README.md) +was converted with `--all --type pcsample` and imported into an isolated +instance of the same server version, with its own configuration and workspace. +The event table exposed exactly four records: PC at 1,000 ns, sleep at 3,000 ns, +`PC_SAMPLE_PROHIBITED` at 6,000 ns, and the following PC at 10,000 ns. Both PC +addresses, the empty sleep PC array, sample flags `2`, and overflow count `0` +were preserved. + +The `arm.cmsis.swo.tg.processor_state.v1` provider exposed a `Sleep` interval +from 3,000 to 6,000 ns and gaps before and after it. The prohibited marker closed +sleep without creating a running or prohibited-duration state. The existing +legacy XML golden remains unchanged because its capture contains no sleep +indications and therefore emits no processor-state handler. + +A marker-only capture produced one `PC_SAMPLE_PROHIBITED` table record, no +companion XML, and no `Processor State` graph. An XML file left from an earlier +capture was removed. This also prevents empty analyses for other point-only +or fully filtered captures: Trace Compass rejects an empty `stateProvider`. diff --git a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp index 931287bef..e58ee1341 100644 --- a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp +++ b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp @@ -179,7 +179,7 @@ void expectSyntheticCsvRoute(std::string_view csv, std::uint8_t stream, std::uin prefix + "dwt,0,0x00,,,\n", prefix + "dwt,1,0x0000,,,\n", prefix + "dwt,2,0x00000000,,,\n", - prefix + "pcsample,,,,,\n", + prefix + "pcsample,,,,,CPU Sleeping\n", prefix + "event,0,0x21,,,\n", prefix + "pmu,3,0x81,,,\n", std::to_string(globalTimestamp) + "," + std::to_string(stream) + ",global_ts,,,,,\n", @@ -316,6 +316,90 @@ std::size_t countCsvStreamRows(std::string_view csv, std::string_view stream) return count; } +/** @brief Uses the same marker payload as raw SWO or on two formatted Trace Bus routes. */ +void writePcSamplingFixture(const std::filesystem::path& directory, const std::filesystem::path& fixtureDirectory, + bool formatted) +{ + std::error_code error; + std::filesystem::create_directories(directory, error); + ASSERT_FALSE(error) << directory << ": " << error.message(); + auto traceRun = readTestTextFile(fixtureDirectory / "trace-pc-sample.ctrace-run.yml"); + auto raw = readTestBinaryFile(fixtureDirectory / "trace-pc-sample.raw"); + ASSERT_EQ(raw.size(), 24U); + if (formatted) { + replaceFixtureText(traceRun, "ctrace-run:\n", "ctrace-run:\n trace-format: formatted\n"); + replaceFixtureText(traceRun, " - timestamps:\n", R"yml( - pname: first + timestamps: + clock: 1000000 + itm-prescaler: 1 + - pname: second + timestamps: +)yml"); + replaceFixtureText(traceRun, " ctrace-refs: []\n", R"yml( ctrace-refs: + - ctrace-ref: first/itm + type: itm + stream: 1 + - ctrace-ref: second/itm + type: itm + stream: 2 +)yml"); + raw = FormattedTraceTestSupport::memoryAlignedFrames({{1U, raw}, {2U, raw}}); + } + writeTestFile(directory / "trace-pc-sample.ctrace-run.yml", traceRun); + writeTestFile(directory / (formatted ? "trace-pc-sample.TB.raw" : "trace-pc-sample.SWO.raw"), + {reinterpret_cast(raw.data()), raw.size()}); +} + +/** @brief Verifies marker notes without mistaking their payloads for sampled PC addresses. */ +void expectPcSamplingCsv(std::string_view csv, std::string_view stream) +{ + const auto prefix = "," + std::string(stream) + ",pcsample,"; + expectContains(csv, "1" + prefix + ",,0x08001234,,\n"); + expectContains(csv, "3" + prefix + ",,,,CPU Sleeping\n"); + expectContains(csv, "6" + prefix + ",,,,Trace prohibited\n"); + expectContains(csv, "10" + prefix + ",,0x08005678,,\n"); + EXPECT_EQ(countCsvStreamRows(csv, stream), 4U); + expectNotContains(csv, ",error,"); + expectNotContains(csv, ",overflow,"); +} + +/** @brief Checks the additive marker record and the unchanged PC/sleep binary layouts. */ +void expectPcSamplingCtf(const std::filesystem::path& streamPath, std::uint8_t stream, bool samplesOnly = false) +{ + const auto layout = stream == 0U ? CtfStreamWriter::EventContextLayout::Legacy + : CtfStreamWriter::EventContextLayout::RouteLabeled; + const auto records = CtfTestSupport::readCtfRecords(streamPath, layout); + std::vector samples; + for (const auto& record : records) { + EXPECT_EQ(record.traceBusId, stream); + if (record.id == CtfSchema::value(CtfSchema::EventId::PcSample) || + record.id == CtfSchema::value(CtfSchema::EventId::PcSampleProhibited)) { + samples.push_back(record); + } + if (record.id == CtfSchema::value(CtfSchema::EventId::TraceStatus)) { + EXPECT_LT(record.payload.front(), CtfSchema::value(CtfSchema::TraceStatusReason::Overflow)); + } + } + if (samplesOnly) { + EXPECT_EQ(records.size(), samples.size()); + } + ASSERT_EQ(samples.size(), 4U); + constexpr std::array timestamps{{1U, 3U, 6U, 10U}}; + constexpr std::array payloadSizes{{10U, 6U, 5U, 10U}}; + for (std::size_t index = 0U; index < samples.size(); ++index) { + EXPECT_EQ(samples[index].timestamp, timestamps[index]); + ASSERT_EQ(samples[index].payload.size(), payloadSizes[index]); + EXPECT_EQ(samples[index].payload[payloadSizes[index] - 5U], CtfSchema::SampleFlagTimestampReliable); + EXPECT_EQ(CtfTestSupport::readLe32(samples[index].payload, payloadSizes[index] - 4U), 0U); + } + EXPECT_EQ(samples[0U].payload.front(), CtfSchema::value(CtfSchema::PcSampleState::Pc)); + EXPECT_EQ(CtfTestSupport::readLe32(samples[0U].payload, 1U), 0x08001234U); + EXPECT_EQ(samples[1U].payload.front(), CtfSchema::value(CtfSchema::PcSampleState::Sleep)); + EXPECT_EQ(samples[2U].id, CtfSchema::value(CtfSchema::EventId::PcSampleProhibited)); + EXPECT_EQ(samples[3U].payload.front(), CtfSchema::value(CtfSchema::PcSampleState::Pc)); + EXPECT_EQ(CtfTestSupport::readLe32(samples[3U].payload, 1U), 0x08005678U); +} + std::string normalizeGeneratedTextLineEndings(std::string text, std::string_view artifact) { std::string normalized; @@ -433,7 +517,7 @@ void expectMatchesGolden(const Container& expected, const Container& actual, std << expectedByte << ", actual 0x" << actualByte; } -TEST_F(CtraceIntegTests, GeneratesAllOutputs) +TEST_F(CtraceIntegTests, GeneratesCsvAndCtfWithoutEmptyGraphicalConfiguration) { writeTestFile(workDirectory() / "Minimal.ctrace-run.yml", R"yml(ctrace-run: ctrace-setup: @@ -453,7 +537,7 @@ TEST_F(CtraceIntegTests, GeneratesAllOutputs) readTestTextFile(workDirectory() / "Minimal.SWO.csv")); expectNonEmptyFile(workDirectory() / "Minimal.ctf" / "metadata"); expectNonEmptyFile(workDirectory() / "Minimal.ctf" / "stream_0"); - expectNonEmptyFile(workDirectory() / "Minimal.SWO.traceanalysis.xml"); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Minimal.SWO.traceanalysis.xml")); } TEST_F(CtraceIntegTests, DecodesExplicitUnformattedNamedTraceBuffer) @@ -477,7 +561,7 @@ TEST_F(CtraceIntegTests, DecodesExplicitUnformattedNamedTraceBuffer) readTestTextFile(workDirectory() / "Named.TB_MTB.csv")); expectNonEmptyFile(workDirectory() / "Named.ctf" / "metadata"); expectNonEmptyFile(workDirectory() / "Named.ctf" / "stream_0"); - expectNonEmptyFile(workDirectory() / "Named.TB_MTB.traceanalysis.xml"); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Named.TB_MTB.traceanalysis.xml")); } TEST_F(CtraceIntegTests, ExcludesUnformattedRawSwoWithoutRequiringClock) @@ -561,7 +645,7 @@ TEST_F(CtraceIntegTests, SkipsUnsupportedFormattedSourceOnceAndKeepsConfiguredRo readTestTextFile(workDirectory() / "Mixed.TB.csv")); expectNonEmptyFile(workDirectory() / "Mixed.ctf" / "stream_1"); EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Mixed.ctf" / "stream_42")); - expectNonEmptyFile(workDirectory() / "Mixed.TB.traceanalysis.xml"); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Mixed.TB.traceanalysis.xml")); } TEST_F(CtraceIntegTests, PublishesOutputsWithUnresolvedFormattedRouteRecovery) @@ -611,7 +695,7 @@ TEST_F(CtraceIntegTests, PublishesOutputsWithUnresolvedFormattedRouteRecovery) readTestTextFile(workDirectory() / "Invalid.TB.csv")); expectNonEmptyFile(workDirectory() / "Invalid.ctf" / "metadata"); expectNonEmptyFile(workDirectory() / "Invalid.ctf" / "stream_1"); - expectNonEmptyFile(workDirectory() / "Invalid.TB.traceanalysis.xml"); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Invalid.TB.traceanalysis.xml")); } TEST_F(CtraceIntegTests, RecoversOneFormattedRouteWithoutLosingInterleavedOutput) @@ -773,6 +857,128 @@ TEST_F(CtraceIntegTests, ConvertsDwtMatchAcrossCsvAndCtf) expectContains(xml, " modes{{ + {"check", {}, false, false}, + {"csv", "--csv", true, false}, + {"ctf", "--ctf", false, true}, + {"all", "--all", true, true}, + }}; + for (const bool formatted : {false, true}) { + const auto channel = formatted ? "TB" : "SWO"; + for (const auto& mode : modes) { + SCOPED_TRACE(std::string(channel) + "/" + std::string(mode.name)); + const auto directory = workDirectory() / channel / mode.name; + writePcSamplingFixture(directory, testDataDirectory() / "trace-pc-sample", formatted); + std::vector arguments{"ctrace", directory.string(), "--target", "trace-pc-sample"}; + if (!mode.option.empty()) { + arguments.emplace_back(mode.option); + } + const auto result = run(std::move(arguments)); + EXPECT_EQ(result.exitCode, 0) << result.stderrText; + expectNotContains(result.stderrText, "[error]"); + const auto csvPath = directory / (std::string("trace-pc-sample.") + channel + ".csv"); + const auto xmlPath = directory / (std::string("trace-pc-sample.") + channel + ".traceanalysis.xml"); + EXPECT_EQ(std::filesystem::exists(csvPath), mode.csv); + EXPECT_EQ(std::filesystem::exists(directory / "trace-pc-sample.ctf"), mode.ctf); + EXPECT_EQ(std::filesystem::exists(xmlPath), mode.ctf && !formatted); + for (const auto stream : formatted ? std::vector{1U, 2U} : std::vector{0U}) { + if (mode.csv) { + expectPcSamplingCsv(readTestTextFile(csvPath), formatted ? std::to_string(stream) : ""); + } + if (mode.ctf) { + expectPcSamplingCtf(directory / "trace-pc-sample.ctf" / ("stream_" + std::to_string(stream)), stream); + } + } + if (mode.ctf && !formatted) { + expectContains(readTestTextFile(xmlPath), "eventName=\"PC_SAMPLE_PROHIBITED\""); + } + } + } +} + +TEST_F(CtraceIntegTests, FiltersPcSamplingMarkersByTypeAndFormattedStream) +{ + writePcSamplingFixture(workDirectory(), testDataDirectory() / "trace-pc-sample", true); + const auto result = run({"ctrace", workDirectory().string(), "--target", "trace-pc-sample", "--all", + "--type", "pcsample", "--stream", "2"}); + EXPECT_EQ(result.exitCode, 0) << result.stderrText; + expectNotContains(result.stderrText, "[error]"); + const auto csv = readTestTextFile(workDirectory() / "trace-pc-sample.TB.csv"); + expectPcSamplingCsv(csv, "2"); + EXPECT_EQ(countCsvStreamRows(csv, "1"), 0U); + expectPcSamplingCtf(workDirectory() / "trace-pc-sample.ctf" / "stream_2", 2U, true); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "trace-pc-sample.ctf" / "stream_1")); + expectContains(readTestTextFile(workDirectory() / "trace-pc-sample.TB.traceanalysis.xml"), + "eventName=\"PC_SAMPLE_PROHIBITED\""); +} + +TEST_F(CtraceIntegTests, DoesNotSelectPcSamplingMarkersAsErrors) +{ + writePcSamplingFixture(workDirectory(), testDataDirectory() / "trace-pc-sample", false); + const auto result = run({"ctrace", workDirectory().string(), "--target", "trace-pc-sample", "--all", + "--type", "error"}); + EXPECT_EQ(result.exitCode, 0) << result.stderrText; + expectNotContains(result.stderrText, "[error]"); + EXPECT_EQ(readTestTextFile(workDirectory() / "trace-pc-sample.SWO.csv"), + "cycles,stream,type,source,value,pc,address,note\n"); + EXPECT_TRUE(CtfTestSupport::readCtfRecords(workDirectory() / "trace-pc-sample.ctf" / "stream_0").empty()); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "trace-pc-sample.SWO.traceanalysis.xml")); +} + +TEST_F(CtraceIntegTests, RetainsProhibitedOnlyTraceDataWithoutGeneratingEmptyXml) +{ + for (const bool formatted : {false, true}) { + const auto channel = formatted ? "TB" : "SWO"; + SCOPED_TRACE(channel); + const auto directory = workDirectory() / channel; + ASSERT_TRUE(std::filesystem::create_directory(directory)); + auto traceRun = readTestTextFile(testDataDirectory() / "trace-pc-sample" / "trace-pc-sample.ctrace-run.yml"); + std::vector raw{0U, 0U, 0U, 0U, 0U, 0x80U, 0x15U, 0xffU, 0x60U}; + if (formatted) { + replaceFixtureText(traceRun, "ctrace-run:\n", "ctrace-run:\n trace-format: formatted\n"); + replaceFixtureText(traceRun, " ctrace-refs: []\n", R"yml( ctrace-refs: + - ctrace-ref: itm + type: itm + stream: 1 +)yml"); + raw = FormattedTraceTestSupport::memoryAlignedFrames({{1U, raw}}); + } + const auto baseName = std::string("trace-pc-sample.") + channel; + writeTestFile(directory / "trace-pc-sample.ctrace-run.yml", traceRun); + writeTestFile(directory / (baseName + ".raw"), {reinterpret_cast(raw.data()), raw.size()}); + const auto xmlPath = directory / (baseName + ".traceanalysis.xml"); + writeTestFile(xmlPath, "stale XML from an earlier capture\n"); + + const auto result = run({"ctrace", directory.string(), "--target", "trace-pc-sample", "--all", + "--type", "pcsample"}); + EXPECT_EQ(result.exitCode, 0) << result.stderrText; + expectNotContains(result.stderrText, "[error]"); + EXPECT_EQ(readTestTextFile(directory / (baseName + ".csv")), + std::string("cycles,stream,type,source,value,pc,address,note\n6,") + + (formatted ? "1" : "") + ",pcsample,,,,,Trace prohibited\n"); + const auto layout = formatted ? CtfStreamWriter::EventContextLayout::RouteLabeled + : CtfStreamWriter::EventContextLayout::Legacy; + const auto records = CtfTestSupport::readCtfRecords( + directory / "trace-pc-sample.ctf" / (formatted ? "stream_1" : "stream_0"), layout); + ASSERT_EQ(records.size(), 1U); + EXPECT_EQ(records.front().id, CtfSchema::value(CtfSchema::EventId::PcSampleProhibited)); + EXPECT_EQ(records.front().timestamp, 6U); + EXPECT_EQ(records.front().traceBusId, formatted ? 1U : 0U); + EXPECT_EQ(records.front().payload, + (std::vector{CtfSchema::SampleFlagTimestampReliable, 0U, 0U, 0U, 0U})); + expectContains(readTestTextFile(directory / "trace-pc-sample.ctf" / "metadata"), "PC_SAMPLE_PROHIBITED"); + EXPECT_FALSE(std::filesystem::exists(xmlPath)); + } +} + TEST_F(CtraceIntegTests, ConvertsCapturedDwtEventCountersAcrossOverflow) { const auto fixtureDirectory = testDataDirectory() / "trace-event"; @@ -961,7 +1167,7 @@ TEST_F(CtraceIntegTests, ReportsDiagnosticsFromConsumedTraceRunReferences) expectNonEmptyFile(workDirectory() / "Diagnostics.SWO.csv"); expectNonEmptyFile(workDirectory() / "Diagnostics.ctf" / "metadata"); expectNonEmptyFile(workDirectory() / "Diagnostics.ctf" / "stream_0"); - expectNonEmptyFile(workDirectory() / "Diagnostics.SWO.traceanalysis.xml"); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Diagnostics.SWO.traceanalysis.xml")); } TEST_F(CtraceIntegTests, GeneratesRequestedOutputsAfterDecoderError) @@ -979,7 +1185,7 @@ TEST_F(CtraceIntegTests, GeneratesRequestedOutputsAfterDecoderError) expectContains(readTestTextFile(csvPath), ",error,"); expectNonEmptyFile(workDirectory() / "Minimal.ctf" / "metadata"); expectNonEmptyFile(workDirectory() / "Minimal.ctf" / "stream_0"); - expectNonEmptyFile(workDirectory() / "Minimal.SWO.traceanalysis.xml"); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Minimal.SWO.traceanalysis.xml")); } TEST_F(CtraceIntegTests, ConvertsBlinkyFixtureToGoldenOutputsAndSkipsLegacyExcludedTraceBusInput) diff --git a/tools/ctrace/test/integration/src/ValidateBabeltrace2Consumer.cmake b/tools/ctrace/test/integration/src/ValidateBabeltrace2Consumer.cmake index f8cf43351..b29d287f6 100644 --- a/tools/ctrace/test/integration/src/ValidateBabeltrace2Consumer.cmake +++ b/tools/ctrace/test/integration/src/ValidateBabeltrace2Consumer.cmake @@ -193,3 +193,56 @@ if(NOT bundle_output_lower MATCHES "different (uuid|identity)") "Babeltrace rejected the multi-clock bundle for an unexpected reason " "(${bundle_result}):\n${bundle_output}") endif() + +# The status marker uses an additive event, not a new PC_SAMPLE array-length +# value. A real consumer must decode the following PC record without drift. +get_filename_component(fixture_root "${FIXTURE_DIRECTORY}" DIRECTORY) +set(marker_fixture_directory "${fixture_root}/trace-pc-sample") +set(marker_work_directory "${test_work_directory}/pc-sampling-markers") +require_nonempty_file("${marker_fixture_directory}/trace-pc-sample.ctrace-run.yml" + "PC sampling marker configuration fixture") +require_nonempty_file("${marker_fixture_directory}/trace-pc-sample.raw" + "PC sampling marker raw fixture") +file(MAKE_DIRECTORY "${marker_work_directory}") +file(COPY + "${marker_fixture_directory}/trace-pc-sample.ctrace-run.yml" + "${marker_fixture_directory}/trace-pc-sample.raw" + DESTINATION "${marker_work_directory}") +file(RENAME + "${marker_work_directory}/trace-pc-sample.raw" + "${marker_work_directory}/trace-pc-sample.SWO.raw") +execute_process( + COMMAND "${CTRACE_EXECUTABLE}" "${marker_work_directory}" --target trace-pc-sample --ctf --type pcsample + WORKING_DIRECTORY "${marker_work_directory}" + RESULT_VARIABLE marker_ctrace_result + OUTPUT_VARIABLE marker_ctrace_stdout + ERROR_VARIABLE marker_ctrace_stderr +) +if(NOT "${marker_ctrace_result}" STREQUAL "0") + message(FATAL_ERROR + "ctrace failed to generate the PC sampling marker consumer fixture (${marker_ctrace_result}):\n" + "${marker_ctrace_stdout}${marker_ctrace_stderr}") +endif() + +run_babeltrace("${marker_work_directory}/trace-pc-sample.ctf" marker_output) +string(STRIP "${marker_output}" marker_output) +string(REPLACE "\n" ";" marker_records "${marker_output}") +list(LENGTH marker_records marker_record_count) +if(NOT marker_record_count EQUAL 4) + message(FATAL_ERROR "Expected exactly four PC sampling records, got ${marker_record_count}:\n${marker_output}") +endif() + +set(expected_marker_records + "[0.000001000] PC_SAMPLE: { cmsis_trace_bus_id = 0 }, { cmsis_pc_sample_state = 1, cmsis_pc = [ [0] = 134222388 ], cmsis_sample_flags = 2, cmsis_overflow_count = 0 }" + "[0.000003000] PC_SAMPLE: { cmsis_trace_bus_id = 0 }, { cmsis_pc_sample_state = 0, cmsis_pc = [ ], cmsis_sample_flags = 2, cmsis_overflow_count = 0 }" + "[0.000006000] PC_SAMPLE_PROHIBITED: { cmsis_trace_bus_id = 0 }, { cmsis_sample_flags = 2, cmsis_overflow_count = 0 }" + "[0.000010000] PC_SAMPLE: { cmsis_trace_bus_id = 0 }, { cmsis_pc_sample_state = 1, cmsis_pc = [ [0] = 134239864 ], cmsis_sample_flags = 2, cmsis_overflow_count = 0 }" +) +foreach(index RANGE 0 3) + list(GET marker_records ${index} actual_record) + list(GET expected_marker_records ${index} expected_record) + if(NOT actual_record STREQUAL expected_record) + message(FATAL_ERROR + "Babeltrace PC sampling record ${index} differs:\nexpected: ${expected_record}\nactual: ${actual_record}") + endif() +endforeach() diff --git a/tools/ctrace/test/integration/src/ValidateFixtureIntegrity.cmake b/tools/ctrace/test/integration/src/ValidateFixtureIntegrity.cmake index 5b765c930..f412cad70 100644 --- a/tools/ctrace/test/integration/src/ValidateFixtureIntegrity.cmake +++ b/tools/ctrace/test/integration/src/ValidateFixtureIntegrity.cmake @@ -27,6 +27,7 @@ endfunction() # Keep this manifest in source control with the reviewed fixture. It is deliberately # independent of host checksum utilities and verifies both identity and expected size. +# Markdown documentation does not affect fixture identity and is excluded. set(fixture_entries "Arm-reset/Arm.SWO.raw|8c7ba2b90e42188517c7b793e8b7dd4030fa5455b7a38a2de15d8ca2b47995c9|131071" "Arm-reset/Arm.ctrace-run.yml|372e3bf3986fd6860dee5046920cbe129db6fd298c3e22468b3e374c09b8cf52|730" @@ -35,19 +36,19 @@ set(fixture_entries "Blinky+Arm/Blinky+Arm.TB.raw|b0fccabe1a326ffe9fadf12d5c3a205d87628985e5e75a99da23c97d7f33d13b|4096" "Blinky+Arm/Blinky+Arm.ctrace-run.yml|c9816183dde98ded93e57afd44312fb3026e3efdd1681f745bc03f7426713563|1171" "Blinky+Arm/expected/Blinky+Arm.SWO.traceanalysis.xml|2ef7a28b11497494f8c8d8aa7b96252bd92ee17b144dc4f344b5780cf526da4f|5450" - "Blinky+Arm/expected/Blinky+Arm.ctf/metadata|5179a4768c9faa9c9ce8ffb0639f373748c1d961195fb46af65c9a67c21739c6|7863" + "Blinky+Arm/expected/Blinky+Arm.ctf/metadata|4fc390221c3d3df0dcfa15f4aa41168b1964e0828bab940fde7dec558ca01785|8047" "Blinky+Arm/expected/Blinky+Arm.ctf/stream_0|2054d43163cf1ff8e921be92b397469e2eb75fb55f4f81c08c20382f38918ef6|65536" "TB-Trace/Blinky+Arm.TB.raw|aab49e56a07783b984fa7c6faeea101a51141423e66ba043dbd8d30702012639|4096" "TB-Trace/Blinky+Arm.ctrace-run.yml|19efd6f35a647f1e5fb73f71ffafa867278d693a7114e3861a43309ebf8f8c4a|3080" - "TB-Trace/README.md|740249499f9cb5f9ef71a82357a6189b9274150775f16c0e9748f555abdab0d1|5739" "TB-Trace/regenerate_tb_trace.py|8ce6ca54cedc216c04a03587b8388003a8ab0563e6c79c39ebf436d9bfcd0050|10687" "TB-Trace/split_tb_trace.py|0ce65b99a2c51b2978cf0b790c653f5172715a1fa86521da8088fbd851ef9347|5361" - "formatted-synthetic/README.md|c881be35e8159b96036ba63171b644b503d10765ccfe8b4ef7cee312dfe571fb|3055" "formatted-synthetic/Synthetic.ctrace-run.yml|a8370d26cd2f75264fc4f48fcdbf5fa2c9e1404c80a61c898dd30de8a44b91c6|2109" "trace-event/trace-event.ctrace-run.yml|a7b924d89854ac85e2751d1297ec78783fa12cb3fa54f5638691dd48d546a34e|89" "trace-event/trace-event.raw|97807dad2f69b1274df8960d3459426d1da4a6892d05e7623f3e16f06c5d85c8|19999" "trace-match/trace-match.ctrace-run.yml|b40c10634b8ba335b14b75f0026758ad84dd68aaf68f0a1bbfd2a5745756c5e8|1132" "trace-match/trace-match.raw|5cffb5803675dc02ecd5ed4939a42c660ad7cabd3542b8ca1506230e20d14a50|18" + "trace-pc-sample/trace-pc-sample.ctrace-run.yml|e8875a9c5ec034ad4fae9ac823a3d442bbb4246af1ddd8d62093496bb8c52e1b|173" + "trace-pc-sample/trace-pc-sample.raw|4e75b75e27be0dab3bdfdecb6331d7bfc1d51337d2fcad8ea2adfe5026992ad4|24" "trace-run/Board.ctrace-run.yml|a6ef0f40757b94840cfbb93d35490e9b3cb0d68471e66a5ea09cc1d55c84f2f7|447" "trace-run/Minimal.ctrace-run.yml|12af0fdf3d19abfda197253fc60897f212e13f8c649d1d93ca9798019333b696|89" ) @@ -65,7 +66,7 @@ endforeach() file(GLOB_RECURSE actual_fixture_paths RELATIVE "${FIXTURE_ROOT}" LIST_DIRECTORIES false "${FIXTURE_ROOT}/*") list(FILTER actual_fixture_paths EXCLUDE REGEX "(^|/)(\\.DS_Store|\\.gitattributes)$") -list(FILTER actual_fixture_paths EXCLUDE REGEX "^README\\.md$") +list(FILTER actual_fixture_paths EXCLUDE REGEX "\\.md$") list(SORT actual_fixture_paths) list(SORT expected_fixture_paths) if(NOT actual_fixture_paths STREQUAL expected_fixture_paths) @@ -73,11 +74,6 @@ if(NOT actual_fixture_paths STREQUAL expected_fixture_paths) "fixture manifest does not match the checked-in fixture set. Expected: ${expected_fixture_paths}; actual: ${actual_fixture_paths}") endif() -list(LENGTH expected_fixture_paths expected_fixture_count) -if(NOT expected_fixture_count EQUAL 22) - message(FATAL_ERROR "fixture manifest must contain exactly 22 checked-in fixture files") -endif() - # The reconstructed Trace Bus input is exactly 256 memory-aligned frames. The # source-ID/payload counters are additionally enforced by its regeneration tool. file(SIZE "${FIXTURE_ROOT}/TB-Trace/Blinky+Arm.TB.raw" formatted_size) diff --git a/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp b/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp index 35491bd62..1cdf4e818 100644 --- a/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp +++ b/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp @@ -130,6 +130,7 @@ TEST(CtraceUnitTests, testTraceDirectoryTargetAndOutputNames) const auto& root = temporaryPath.path(); const auto traceDir = root / ".trace"; writeTraceInputs(traceDir, {"Alpha", "Beta"}); + writeTestFile(traceDir / "Alpha.SWO.raw", std::string{"\0\0\0\0\0\x80\x15\0", 8U}); writeTestFile(traceDir / "Alpha.TB_MTB.raw", "unsupported"); writeTestFile(traceDir / "Alpha.ER.raw", "unsupported"); @@ -231,7 +232,7 @@ TEST(CtraceUnitTests, testTraceDirectoryDecodesFormattedInputThroughRawFrontend) EXPECT_NE(readTestTextFile(traceDir / "Formatted.TB.csv").find(",1,itm,1,0x41,,,"), std::string::npos); EXPECT_TRUE(std::filesystem::is_regular_file(traceDir / "Formatted.ctf" / "stream_1")); EXPECT_FALSE(std::filesystem::exists(traceDir / "Formatted.ctf" / "stream_0")); - EXPECT_TRUE(std::filesystem::is_regular_file(traceDir / "Formatted.TB.traceanalysis.xml")); + EXPECT_FALSE(std::filesystem::exists(traceDir / "Formatted.TB.traceanalysis.xml")); } TEST(CtraceUnitTests, testTraceDirectoryPreflightsFormattedAlignmentBeforeDecoderAndArtifacts) diff --git a/tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp b/tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp index 6278b8e99..f46f7aa1a 100644 --- a/tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp +++ b/tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp @@ -771,21 +771,41 @@ TEST(CtraceUnitTests, testDecodePipelinePreservesDwtEventAndPmuPackets) TEST(CtraceUnitTests, testDecodePipelinePreservesPeriodicPcSamples) { const std::uint8_t trace[] = { - 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x17U, 0x34U, 0x12U, 0x00U, 0x08U, 0x15U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, + 0x17U, 0x34U, 0x12U, 0x00U, 0x08U, 0x10U, + 0x15U, 0x00U, 0x20U, + 0x15U, 0xffU, 0x30U, + 0x17U, 0xffU, 0x00U, 0x00U, 0x00U, 0x40U, + 0x17U, 0x00U, 0x00U, 0x00U, 0x00U, 0x50U, }; - const auto decoded = decodeTrace({rawBytes(trace)}); + const auto decoded = decodeTrace({rawBytes(trace)}, 1U); std::vector samples; + std::vector cycles; for (const auto& event : decoded.events) { + EXPECT_FALSE(isTraceEvent(event)) << "valid PC-sample markers must not be errors"; if (const auto* sample = traceEventPayload(event)) { samples.push_back(*sample); + ASSERT_TRUE(event.tcyc.has_value()); + cycles.push_back(event.tcyc.value()); + ASSERT_TRUE(event.quality.has_value()); + EXPECT_TRUE(event.quality->timestampReliable); + EXPECT_FALSE(event.quality->overflow); + EXPECT_EQ(event.quality->overflowCount, 0U); } } - ASSERT_EQ(samples.size(), 2U) << "OpenCSD periodic PC-sample packet count mismatch"; + ASSERT_EQ(samples.size(), 5U) << "OpenCSD periodic PC-sample packet count mismatch"; EXPECT_EQ(samples[0].pc, 0x08001234U) << "OpenCSD periodic PC sample payload mismatch"; - EXPECT_FALSE(samples[0].sleeping) << "OpenCSD periodic PC sample payload mismatch"; + EXPECT_EQ(samples[0].kind, PcSampleKind::Pc); EXPECT_EQ(samples[1].pc, 0U) << "OpenCSD periodic PC sleep indication mismatch"; - EXPECT_TRUE(samples[1].sleeping) << "OpenCSD periodic PC sleep indication mismatch"; + EXPECT_EQ(samples[1].kind, PcSampleKind::Sleep); + EXPECT_EQ(samples[2].kind, PcSampleKind::TraceProhibited); + EXPECT_EQ(samples[2].pc, 0U); + EXPECT_EQ(samples[3].kind, PcSampleKind::Pc); + EXPECT_EQ(samples[3].pc, 0xffU); + EXPECT_EQ(samples[4].kind, PcSampleKind::Pc); + EXPECT_EQ(samples[4].pc, 0U); + EXPECT_EQ(cycles, (std::vector{1U, 3U, 6U, 10U, 15U})); } TEST(CtraceUnitTests, testDecodePipelinePreservesCompressedDataTracePcValues) diff --git a/tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp b/tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp index dc7445ed2..b310a3313 100644 --- a/tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp +++ b/tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp @@ -41,7 +41,7 @@ TEST(CtraceUnitTests, testDwtPcSampleProducesDedicatedEvent) const auto* sample = traceEventPayload(packets.front()); ASSERT_NE(sample, nullptr); EXPECT_EQ(sample->pc, 0x08001234U) << "DWT PC sample payload mismatch"; - EXPECT_FALSE(sample->sleeping) << "DWT PC sample payload mismatch"; + EXPECT_EQ(sample->kind, PcSampleKind::Pc) << "DWT PC sample payload mismatch"; EXPECT_EQ(packets.front().index, 19U) << "DWT PC sample identity mismatch"; EXPECT_EQ(packets.front().route.traceBusId, 3U) << "DWT PC sample identity mismatch"; EXPECT_EQ(packets.front().tcyc, std::optional(949339000U)) @@ -52,7 +52,7 @@ TEST(CtraceUnitTests, testDwtPcSampleProducesDedicatedEvent) << "DWT PC sample selector mapping mismatch"; } -TEST(CtraceUnitTests, testDwtPcSamplePreservesProcessorSleep) +TEST(CtraceUnitTests, testDwtPcSampleDistinguishesZeroPcFromProcessorSleep) { DwtPacketDecoder decoder; const auto packets = decoder.decode(dwtPayload(2U, 1U, 0U, 20U, 4U, 949339100U)); @@ -60,7 +60,47 @@ TEST(CtraceUnitTests, testDwtPcSamplePreservesProcessorSleep) const auto* sample = traceEventPayload(packets.front()); ASSERT_NE(sample, nullptr); EXPECT_EQ(sample->pc, 0U) << "DWT PC sleep indication mismatch"; - EXPECT_TRUE(sample->sleeping) << "DWT PC sleep indication mismatch"; + EXPECT_EQ(sample->kind, PcSampleKind::Sleep) << "DWT PC sleep indication mismatch"; + EXPECT_EQ(CsvRowMapper::row(packets.front()), "949339100,4,pcsample,,,,,CPU Sleeping"); + + const auto following = decoder.decode(dwtPayload(2U, 4U, 0U, 21U, 4U, 949339200U)); + ASSERT_EQ(following.size(), 1U); + const auto* pc = traceEventPayload(following.front()); + ASSERT_NE(pc, nullptr); + EXPECT_EQ(pc->kind, PcSampleKind::Pc) << "a four-byte zero is a PC, not a sleep marker"; + EXPECT_EQ(pc->pc, 0U); + EXPECT_EQ(CsvRowMapper::row(following.front()), "949339200,4,pcsample,,,0x00000000,,"); +} + +TEST(CtraceUnitTests, testDwtPcSamplePreservesTraceProhibitedWithoutDataLoss) +{ + DwtPacketDecoder decoder; + auto payload = dwtPayload(2U, 1U, 0xffU, 21U, 4U, 949339200U); + payload.quality.timestampReliable = true; + payload.quality.overflowCount = 3U; + const auto packets = decoder.decode(payload); + ASSERT_EQ(packets.size(), 1U); + const auto& event = packets.front(); + const auto* sample = traceEventPayload(event); + ASSERT_NE(sample, nullptr); + EXPECT_EQ(sample->kind, PcSampleKind::TraceProhibited); + EXPECT_EQ(sample->pc, 0U) << "a status marker must not fabricate a PC address"; + EXPECT_EQ(event.index, payload.index); + EXPECT_EQ(event.route.id, payload.route.id); + EXPECT_EQ(event.route.traceBusId, payload.route.traceBusId); + EXPECT_EQ(event.tcyc, payload.tcyc); + ASSERT_TRUE(event.quality.has_value()); + EXPECT_TRUE(event.quality->timestampReliable); + EXPECT_FALSE(event.quality->overflow); + EXPECT_EQ(event.quality->overflowCount, 3U); + EXPECT_EQ(traceEventType(event), TraceEventType::PcSample); + + const auto following = decoder.decode(dwtPayload(2U, 4U, 0xffU)); + ASSERT_EQ(following.size(), 1U); + const auto* pc = traceEventPayload(following.front()); + ASSERT_NE(pc, nullptr); + EXPECT_EQ(pc->kind, PcSampleKind::Pc); + EXPECT_EQ(pc->pc, 0xffU) << "a four-byte 0xff is a PC, not a trace-prohibited marker"; } TEST(CtraceUnitTests, testDwtPcSampleRejectsUnsupportedPayloads) @@ -77,10 +117,11 @@ TEST(CtraceUnitTests, testDwtPcSampleRejectsUnsupportedPayloads) }; verify(dwtPayload(2U, 1U, 1U), - "unsupported DWT PC-sample payload: size 1, value 1; expected a 4-byte PC or a 1-byte zero sleep indication"); + "unsupported DWT PC-sample payload: size 1, value 1; expected a 4-byte PC or a 1-byte marker " + "(0x00: CPU Sleeping, 0xff: Trace prohibited)"); verify(dwtPayload(2U, 2U, 0x1234U), - "unsupported DWT PC-sample payload: size 2, value 4660; expected a 4-byte PC or a 1-byte zero sleep " - "indication"); + "unsupported DWT PC-sample payload: size 2, value 4660; expected a 4-byte PC or a 1-byte marker " + "(0x00: CPU Sleeping, 0xff: Trace prohibited)"); } TEST(CtraceUnitTests, testDwtEventCounterPacketIsValidatedAndExposed) diff --git a/tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp b/tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp index 8d5c24529..ee7b706ad 100644 --- a/tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp +++ b/tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp @@ -94,9 +94,21 @@ TEST(CtraceUnitTests, testTraceSelection) ASSERT_TRUE(traceEventSelectedForOutput(exception, TraceSelection{{"exception"}, {}})) << "TraceSelection exception type mismatch"; - TraceEvent pcSample{PcSampleTraceEvent{0x08001234U, false}}; + TraceEvent pcSample{PcSampleTraceEvent{0x08001234U}}; ASSERT_TRUE(traceEventSelectedForOutput(pcSample, TraceSelection{{"pcsample"}, {}})) << "TraceSelection PC-sample type mismatch"; ASSERT_FALSE(traceEventSelectedForOutput(pcSample, TraceSelection{{"dwt"}, {}})) << "TraceSelection must keep PC samples separate from DWT data trace"; } + +TEST(CtraceUnitTests, testTraceSelectionTreatsPcSampleMarkersAsSamplesNotErrors) +{ + for (const auto kind : {PcSampleKind::Sleep, PcSampleKind::TraceProhibited}) { + TraceEvent marker{PcSampleTraceEvent{0U, kind}}; + marker.route = {TraceRouteId{5U}, 7U}; + EXPECT_TRUE(traceEventSelectedForOutput(marker, TraceSelection{})); + EXPECT_TRUE(traceEventSelectedForOutput(marker, TraceSelection{{"pcsample"}, {7U}})); + EXPECT_FALSE(traceEventSelectedForOutput(marker, TraceSelection{{"pcsample"}, {5U}})); + EXPECT_FALSE(traceEventSelectedForOutput(marker, TraceSelection{{"error"}, {7U}})); + } +} diff --git a/tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp b/tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp index 21508f8ab..ed2bb1b43 100644 --- a/tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp +++ b/tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp @@ -183,6 +183,7 @@ TEST(CtraceUnitTests, testTraceOutputLifecycleCompletesCtfAfterRealCsvStartFailu outputs.push_back(std::make_unique(csvPath)); TraceOutputLifecycle lifecycle(std::move(outputs), diagnostics); lifecycle.append(softwarePacket(1U, 1U, 'A')); + lifecycle.append(TraceEvent{PcSampleTraceEvent{0U, PcSampleKind::Sleep}}); lifecycle.finish(); EXPECT_TRUE(std::filesystem::is_regular_file(ctfDirectory / "metadata")); diff --git a/tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp b/tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp index d36ee78ae..27bf2a5ef 100644 --- a/tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp +++ b/tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp @@ -134,17 +134,21 @@ TEST(CtraceUnitTests, testCsvFileOutputMatchesSpecification) }}, 949338400U)); output.writeEvent(atCycle(TraceEvent{ExceptionTraceEvent{11U, ExceptionAction::Entered}}, 950364820U)); - output.writeEvent(atCycle(TraceEvent{PcSampleTraceEvent{0x08000100U, false}}, 950364900U)); + output.writeEvent(atCycle(TraceEvent{PcSampleTraceEvent{0x08000100U}}, 950364900U)); + output.writeEvent(atCycle(TraceEvent{PcSampleTraceEvent{0U, PcSampleKind::Sleep}}, 950365000U)); + output.writeEvent(atCycle(TraceEvent{PcSampleTraceEvent{0U, PcSampleKind::TraceProhibited}}, 950365100U)); output.stop(); const auto lines = readTestLines(csvPath); - ASSERT_TRUE(lines.size() == 4U) << "CSV specification row count mismatch"; + ASSERT_TRUE(lines.size() == 6U) << "CSV specification row count mismatch"; ASSERT_TRUE(lines[0] == "cycles,stream,type,source,value,pc,address,note") << "CSV specification header mismatch"; ASSERT_TRUE(lines[1] == "949338400,,dwt,2,0xfffffdf9,0x08001234,0xfdf9,") << "CSV DWT row schema mismatch"; ASSERT_TRUE(lines[2] == "950364820,,exception,11,0x1,,,") << "CSV exception state schema mismatch"; ASSERT_TRUE(lines[3] == "950364900,,pcsample,,,0x08000100,,") << "CSV PC-sample row schema mismatch"; + EXPECT_EQ(lines[4], "950365000,,pcsample,,,,,CPU Sleeping"); + EXPECT_EQ(lines[5], "950365100,,pcsample,,,,,Trace prohibited"); } TEST(CtraceUnitTests, testCsvFileOutputPreservesInterleavedRouteOrderAndOnlyWritesArchitecturalIds) diff --git a/tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp b/tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp index ae528844e..775b8909c 100644 --- a/tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp +++ b/tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp @@ -79,12 +79,17 @@ TEST(CtraceUnitTests, testCsvRowMapperAndTraceEventSchema) EXPECT_EQ(CsvRowMapper::row(TraceEvent{DwtDataTraceEvent{0U, 4U, 0U, AccessType::Write}}), ",,dwt,0,0x00000000,,,"); ASSERT_TRUE(CsvRowMapper::row(TraceEvent{DwtMatchTraceEvent{2U}}) == ",,dwt,2,,,,") << "CSV must expose a match only through its DWT comparator source"; - ASSERT_TRUE(CsvRowMapper::row(atCycle(TraceEvent{PcSampleTraceEvent{0x08001234U, false}}, 949339000U)) == + ASSERT_TRUE(CsvRowMapper::row(atCycle(TraceEvent{PcSampleTraceEvent{0x08001234U}}, 949339000U)) == "949339000,,pcsample,,,0x08001234,,") << "CSV PC-sample row mismatch"; - ASSERT_TRUE(CsvRowMapper::row(atCycle(TraceEvent{PcSampleTraceEvent{0U, true}}, 949339100U)) == - "949339100,,pcsample,,,,,") + ASSERT_TRUE(CsvRowMapper::row(atCycle(TraceEvent{PcSampleTraceEvent{0U, PcSampleKind::Sleep}}, 949339100U)) == + "949339100,,pcsample,,,,,CPU Sleeping") << "CSV PC-sample sleep row mismatch"; + EXPECT_EQ(CsvRowMapper::row(onStream( + atCycle(TraceEvent{PcSampleTraceEvent{0U, PcSampleKind::TraceProhibited}}, 949339200U), 4U)), + "949339200,4,pcsample,,,,,Trace prohibited"); + EXPECT_EQ(CsvRowMapper::row(TraceEvent{PcSampleTraceEvent{0xffU}}), ",,pcsample,,,0x000000ff,,") + << "a four-byte PC value must not be rendered as a status marker"; } TEST(CtraceUnitTests, testCsvRowMapperCoversAddressAndExceptionVariants) diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp index efee799eb..4653ba58a 100644 --- a/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp +++ b/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp @@ -633,7 +633,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputAbortRemovesPartialBundle) << "CTF abort must remove the incomplete direct output"; } -TEST(CtraceUnitTests, testCtfBundleOutputGeneratesDataDrivenLegacyTraceCompassViews) +TEST(CtraceUnitTests, testCtfBundleOutputOmitsXmlWithoutGraphicalEvents) { const TemporaryTestPath root("ctrace-ctf-legacy-xml-completion-test"); const auto outputDirectory = root.path() / "output.ctf"; @@ -645,16 +645,12 @@ TEST(CtraceUnitTests, testCtfBundleOutputGeneratesDataDrivenLegacyTraceCompassVi output.writeEvent(atCycle(softwarePacket(1U, 1U, 'A'), 10U)); output.stop(); - ASSERT_TRUE(std::filesystem::is_regular_file(xmlPath)); + EXPECT_FALSE(std::filesystem::exists(xmlPath)) + << "software events and synthetic exception bootstrap records must not create empty XML"; const auto records = readCtfRecords(outputDirectory / "stream_0"); ASSERT_FALSE(records.empty()); EXPECT_FALSE(records.front().routeLabelId.has_value()); EXPECT_EQ(readTestTextFile(outputDirectory / "metadata").find("ctrace_route"), std::string::npos); - const auto xml = readTestTextFile(xmlPath); - EXPECT_EQ(readTestTextFile(xmlPath).find("{"stream_1", "stream_111"} + : std::vector{"stream_0"}; + for (const auto& name : streamNames) { + const auto records = readCtfRecords(outputDirectory / name, contextLayout); + EXPECT_EQ(std::count_if(records.begin(), records.end(), [](const auto& record) { + return record.id == CtfSchema::value(CtfSchema::EventId::PcSampleProhibited); + }), + 1); + } + } +} + TEST(CtraceUnitTests, testCtfBundleOutputGeneratesRoutePrefixedXmlForSharedClockStreams) { const TemporaryCtfOutput temporaryOutput("ctrace-ctf-shared-clock-xml-test"); @@ -976,7 +1030,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputReplacesExistingBundleAtStart) !std::filesystem::exists(outputDir / "metadata") && !std::filesystem::exists(xmlPath)) << "CTF start must replace existing output before decoding begins"; - output.writeEvent(atCycle(softwarePacket(1U, 1U, 'A'), 10U)); + output.writeEvent(atCycle(TraceEvent{PcSampleTraceEvent{0U, PcSampleKind::Sleep}}, 10U)); output.stop(); ASSERT_TRUE(std::filesystem::is_regular_file(outputDir / "metadata") && std::filesystem::file_size(outputDir / "stream_0") > 0U && std::filesystem::is_regular_file(xmlPath)) @@ -1028,8 +1082,8 @@ TEST(CtraceUnitTests, testCtfBundleOutputOwnsDirectLifecycle) auto config = makeCtfBundleConfig(ctfDirectory, 1000000U); CtfBundleOutput output(std::move(config)); output.start(); - const auto software = atCycle(softwarePacket(1U, 1U, 'A'), 10U); - output.writeEvent(software); + const auto sleep = atCycle(TraceEvent{PcSampleTraceEvent{0U, PcSampleKind::Sleep}}, 10U); + output.writeEvent(sleep); output.stop(); requireCompleteCtfBundle(ctfDirectory, xmlPath, "CTF bundle must support Unicode output paths"); @@ -1041,7 +1095,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputOwnsDirectLifecycle) ASSERT_TRUE(!std::filesystem::exists(ctfDirectory / "stale-marker") && !std::filesystem::exists(ctfDirectory / "metadata") && !std::filesystem::exists(xmlPath)) << "restarting CTF output must delete the previous bundle before writing"; - output.writeEvent(software); + output.writeEvent(sleep); output.stop(); requireCompleteCtfBundle(ctfDirectory, xmlPath, "restarted CTF bundle must complete normally"); @@ -1051,7 +1105,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputOwnsDirectLifecycle) const auto relativeCtf = relativeRoot / "Relative.ctf"; CtfBundleOutput relativeOutput(makeCtfBundleConfig(relativeCtf, 1000000U)); relativeOutput.start(); - relativeOutput.writeEvent(software); + relativeOutput.writeEvent(sleep); relativeOutput.stop(); ASSERT_TRUE(std::filesystem::is_regular_file(testRoot / "captures" / "Relative.ctf" / "metadata") && std::filesystem::is_regular_file(testRoot / "captures" / "Relative.SWO.traceanalysis.xml")) @@ -1103,6 +1157,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputRejectsInvalidExistingXmlAndLongPaths) failedAtStart = true; } if (!failedAtStart) { + longXml.writeEvent(TraceEvent{PcSampleTraceEvent{0U, PcSampleKind::Sleep}}); EXPECT_THROW(longXml.stop(), std::runtime_error); } EXPECT_FALSE(std::filesystem::exists(temporaryPath.path() / "long-xml.ctf")); @@ -1145,6 +1200,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputReportsPseudoFilesystemFinishFailure) CtfBundleOutput output( makeCtfBundleConfig(outputDirectory, TestPlatform::creationFailurePath("ctrace-coverage-output.xml"), 1000000U)); output.start(); + output.writeEvent(TraceEvent{PcSampleTraceEvent{0U, PcSampleKind::Sleep}}); EXPECT_THROW(output.stop(), std::runtime_error); EXPECT_FALSE(std::filesystem::exists(outputDirectory)); } @@ -1194,6 +1250,7 @@ TEST(CtraceUnitTests, testCtfBundleOutputReportsPermissionFailures) std::filesystem::permissions(blockedParent, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec); CtfBundleOutput finishCleanupFailure(makeCtfBundleConfig(cleanupCtf, blockedParent / "new.xml", 1000000U)); finishCleanupFailure.start(); + finishCleanupFailure.writeEvent(TraceEvent{PcSampleTraceEvent{0U, PcSampleKind::Sleep}}); EXPECT_THROW(finishCleanupFailure.stop(), std::runtime_error); EXPECT_FALSE(std::filesystem::exists(cleanupCtf)); std::filesystem::permissions(blockedParent, std::filesystem::perms::owner_all); diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp index be0dcdcad..dff050d7d 100644 --- a/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp +++ b/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -166,10 +167,10 @@ TEST(CtraceUnitTests, testCtfEncoderPcSampleEncoding) CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{"pcsample"}, {}})); startEncoder(encoder, outputDirectory); - auto pc = atCycle(TraceEvent{PcSampleTraceEvent{0x08001234U, false}}, 10U); + auto pc = atCycle(TraceEvent{PcSampleTraceEvent{0x08001234U, PcSampleKind::Pc}}, 10U); pc.quality = TraceQuality{false, true, 0U}; encoder.writeEvent(pc); - auto sleep = atCycle(TraceEvent{PcSampleTraceEvent{0x12345678U, true}}, 11U); + auto sleep = atCycle(TraceEvent{PcSampleTraceEvent{0x12345678U, PcSampleKind::Sleep}}, 11U); sleep.quality = TraceQuality{true, false, 7U}; encoder.writeEvent(sleep); encoder.stop(); @@ -200,6 +201,76 @@ TEST(CtraceUnitTests, testCtfEncoderPcSampleEncoding) EXPECT_NE(metadata.find("uint32_t cmsis_pc[cmsis_pc_sample_state]"), std::string::npos); } +TEST(CtraceUnitTests, testCtfEncoderPcSampleProhibitedPreservesQualityAndFollowingPc) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-pc-sample-prohibited-test"); + const auto& outputDirectory = temporaryPath.createDirectory(); + CtfEncoder encoder(legacyEncoderConfig(1000000U, TraceSelection{{"pcsample"}, {}})); + startEncoder(encoder, outputDirectory); + + auto prohibited = atCycle(TraceEvent{PcSampleTraceEvent{0xffffffffU, PcSampleKind::TraceProhibited}}, 12U); + prohibited.quality = TraceQuality{true, true, 7U}; + encoder.writeEvent(prohibited); + auto pc = atCycle(TraceEvent{PcSampleTraceEvent{0x08005678U, PcSampleKind::Pc}}, 13U); + pc.quality = TraceQuality{false, true, 7U}; + encoder.writeEvent(pc); + encoder.stop(); + + const auto records = readCtfRecords(outputDirectory / "stream_0"); + ASSERT_EQ(records.size(), 2U); + EXPECT_EQ(records[0].id, CtfSchema::value(CtfSchema::EventId::PcSampleProhibited)); + EXPECT_EQ(records[0].timestamp, 12U); + EXPECT_EQ(records[0].traceBusId, 0U); + EXPECT_FALSE(records[0].routeLabelId.has_value()); + ASSERT_EQ(records[0].payload.size(), 5U); + EXPECT_EQ(records[0].payload[0U], + CtfSchema::SampleFlagOverflow | CtfSchema::SampleFlagTimestampReliable); + EXPECT_EQ(readLe32(records[0].payload, 1U), 7U); + + EXPECT_EQ(records[1].id, CtfSchema::value(CtfSchema::EventId::PcSample)); + EXPECT_EQ(records[1].timestamp, 13U); + ASSERT_EQ(records[1].payload.size(), 10U); + EXPECT_EQ(records[1].payload[0U], CtfSchema::value(CtfSchema::PcSampleState::Pc)); + EXPECT_EQ(readLe32(records[1].payload, 1U), 0x08005678U); + EXPECT_EQ(records[1].payload[5U], CtfSchema::SampleFlagTimestampReliable); + EXPECT_EQ(readLe32(records[1].payload, 6U), 7U); +} + +TEST(CtraceUnitTests, testCtfEncoderPcSampleProhibitedRespectsTypeAndRouteFilters) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-pc-sample-prohibited-filter-test"); + const auto& outputDirectory = temporaryPath.createDirectory(); + CtfEncoder encoder(formattedEncoderConfig(TraceSelection{{"pcsample"}, {111U}})); + startEncoder(encoder, outputDirectory); + const TraceRouteIdentity firstRoute{TraceRouteId{4U}, 1U}; + const TraceRouteIdentity secondRoute{TraceRouteId{90U}, 111U}; + auto prohibited = atCycle(TraceEvent{PcSampleTraceEvent{0U, PcSampleKind::TraceProhibited}}, 15U); + prohibited.quality = TraceQuality{false, false, 3U}; + encoder.writeEvent(onRoute(prohibited, firstRoute)); + encoder.writeEvent(onRoute(prohibited, secondRoute)); + encoder.writeEvent(onRoute(softwarePacket(1U, 1U, 'A'), secondRoute)); + encoder.stop(); + + EXPECT_FALSE(std::filesystem::exists(outputDirectory / "stream_1")); + const auto records = + readCtfRecords(outputDirectory / "stream_111", CtfStreamWriter::EventContextLayout::RouteLabeled); + ASSERT_EQ(records.size(), 1U); + EXPECT_EQ(records[0].id, CtfSchema::value(CtfSchema::EventId::PcSampleProhibited)); + EXPECT_EQ(records[0].timestamp, 15U); + EXPECT_EQ(records[0].traceBusId, 111U); + EXPECT_EQ(records[0].routeLabelId, std::optional{111U}); + ASSERT_EQ(records[0].payload.size(), 5U); + EXPECT_EQ(records[0].payload[0U], CtfSchema::SampleFlagBeforeFirstTimestamp); + EXPECT_EQ(readLe32(records[0].payload, 1U), 3U); + + const TemporaryTestPath filteredPath("ctrace-ctf-pc-sample-prohibited-unselected-test"); + CtfEncoder filteredEncoder(legacyEncoderConfig(1000000U, TraceSelection{{"itm"}, {}})); + startEncoder(filteredEncoder, filteredPath.createDirectory()); + filteredEncoder.writeEvent(prohibited); + filteredEncoder.stop(); + EXPECT_TRUE(readCtfRecords(filteredPath.path() / "stream_0").empty()); +} + TEST(CtraceUnitTests, testCtfEncoderExpandsDwtEventCounterMask) { const TemporaryTestPath temporaryPath("ctrace-ctf-dwt-event-test"); diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp index e83d7c505..2474c93bb 100644 --- a/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp +++ b/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp @@ -64,6 +64,11 @@ TEST(CtraceUnitTests, testCtfMetadataWriterEscapesAndDeduplicatesSourceLabels) EXPECT_NE(metadata.find("variant "), std::string::npos); EXPECT_NE(metadata.find("uint32_t u32;"), std::string::npos); EXPECT_NE(metadata.find("cmsis_dwt0_address_end = \"0xFFFFFFFFFFFFFFFF\""), std::string::npos); + EXPECT_NE(metadata.find("cmsis_ctf_profile_version = 1;"), std::string::npos); + EXPECT_NE(metadata.find("id = 10;\n name = \"PC_SAMPLE_PROHIBITED\";\n stream_id = 0;\n" + " fields := struct {\n uint8_t cmsis_sample_flags;\n" + " uint32_t cmsis_overflow_count;\n };"), + std::string::npos); } TEST(CtraceUnitTests, testCtfMetadataWriterRejectsMissingOutputDirectory) @@ -134,6 +139,12 @@ TEST(CtraceUnitTests, testCtfMetadataWriterSerializesRouteScopedMultiStreamTopol EXPECT_EQ(metadata.find("name = swo_clock;"), std::string::npos); EXPECT_EQ(metadata.find("stream_id = 0;"), std::string::npos); EXPECT_FALSE(std::filesystem::exists(path.path() / "stream_0")); + for (const auto streamId : {1U, 111U}) { + EXPECT_NE(metadata.find("id = 10;\n name = \"PC_SAMPLE_PROHIBITED\";\n stream_id = " + + std::to_string(streamId) + ";\n fields := struct {\n" + " uint8_t cmsis_sample_flags;\n uint32_t cmsis_overflow_count;\n };"), + std::string::npos); + } } TEST(CtraceUnitTests, testCtfMetadataWriterUsesStreamClassIdLabelForUnboundRoute) diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfSchemaTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfSchemaTests.cpp index c1ffcecc7..b1d518fb1 100644 --- a/tools/ctrace/test/unit/src/output/ctf/CtfSchemaTests.cpp +++ b/tools/ctrace/test/unit/src/output/ctf/CtfSchemaTests.cpp @@ -21,13 +21,14 @@ TEST(CtraceUnitTests, testCtfSchemaUsesDenseIdentifiers) { - constexpr std::array eventIds{ + constexpr std::array eventIds{ CtfSchema::value(CtfSchema::EventId::Itm), CtfSchema::value(CtfSchema::EventId::DwtValue), CtfSchema::value(CtfSchema::EventId::DwtAddress), CtfSchema::value(CtfSchema::EventId::TraceStatus), CtfSchema::value(CtfSchema::EventId::Exception), CtfSchema::value(CtfSchema::EventId::GlobalTimestamp), CtfSchema::value(CtfSchema::EventId::PcSample), CtfSchema::value(CtfSchema::EventId::DwtEvent), CtfSchema::value(CtfSchema::EventId::PmuEvent), CtfSchema::value(CtfSchema::EventId::DwtMatch), + CtfSchema::value(CtfSchema::EventId::PcSampleProhibited), }; constexpr std::array statusReasons{ CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart), @@ -101,6 +102,7 @@ TEST(CtraceUnitTests, testCtfSchemaUsesDenseIdentifiers) TEST(CtraceUnitTests, testCtfValueTypes) { + EXPECT_EQ(CtfSchema::eventName(CtfSchema::EventId::PcSampleProhibited), "PC_SAMPLE_PROHIBITED"); EXPECT_EQ(CtfSchema::eventName(static_cast(255U)), "UNKNOWN"); /** @brief Describes one supported CTF value representation. */ struct SupportedType { diff --git a/tools/ctrace/test/unit/src/output/ctf/TraceCompassXmlWriterTests.cpp b/tools/ctrace/test/unit/src/output/ctf/TraceCompassXmlWriterTests.cpp index 9a1a478b4..e33dd5e48 100644 --- a/tools/ctrace/test/unit/src/output/ctf/TraceCompassXmlWriterTests.cpp +++ b/tools/ctrace/test/unit/src/output/ctf/TraceCompassXmlWriterTests.cpp @@ -42,10 +42,11 @@ TEST(CtraceUnitTests, testTraceCompassXmlUsesCurrentCtfEvents) const TemporaryTestPath path("ctrace-trace-compass-schema.xml"); TraceCompassXmlWriter::writeLegacyFile(path.path()); const auto xml = readTestTextFile(path.path()); - constexpr std::array stateDrivenEvents{ + constexpr std::array stateDrivenEvents{ CtfSchema::EventId::DwtValue, CtfSchema::EventId::DwtAddress, CtfSchema::EventId::Exception, CtfSchema::EventId::TraceStatus, CtfSchema::EventId::PcSample, CtfSchema::EventId::DwtEvent, CtfSchema::EventId::PmuEvent, CtfSchema::EventId::DwtMatch, + CtfSchema::EventId::PcSampleProhibited, }; for (const auto eventId : stateDrivenEvents) { @@ -145,6 +146,37 @@ TEST(CtraceUnitTests, testTraceCompassXmlScopesGraphicalViewsPerRoute) EXPECT_EQ(xml.find(""); + ASSERT_NE(handlerStart, std::string::npos); + const auto handlerEnd = xml.find("", handlerStart); + ASSERT_NE(handlerEnd, std::string::npos); + const auto handler = xml.substr(handlerStart, handlerEnd - handlerStart); + EXPECT_NE(handler.find(""), std::string::npos); + EXPECT_NE(handler.find(""), std::string::npos); + EXPECT_NE(handler.find(""), std::string::npos); + EXPECT_EQ(handler.find("value=\"context.ctrace_route\"") != std::string::npos, routePrefixed); + EXPECT_EQ(handler.find("value=\"context.cmsis_trace_bus_id\"") != std::string::npos, routePrefixed); + EXPECT_EQ(handler.find("cmsis_pc_sample_state"), std::string::npos); + EXPECT_EQ(handler.find("& bytes, std:: "CTF test parser encountered an invalid PC-sample state"); return 1U + (state == CtfSchema::value(CtfSchema::PcSampleState::Pc) ? 4U : 0U) + 5U; } + if (eventId == CtfSchema::value(CtfSchema::EventId::PcSampleProhibited)) { + return 5U; + } if (eventId == CtfSchema::value(CtfSchema::EventId::DwtEvent)) { return 6U; }