From 772c7911e61b8066f09c5cfbfe03fe4fc0ee021e Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Thu, 30 Jul 2026 13:13:23 +0200 Subject: [PATCH 01/35] [ctrace] Add trace decoder implementation and tests --- .gitattributes | 1 + .github/matrix_includes_ctrace.json | 2 +- .github/workflows/codeql-analysis.yml | 11 + .github/workflows/ctrace.yml | 99 ++- .github/workflows/shared_matrix_prep.yml | 4 +- .github/workflows/shared_setup_env.yml | 1 - .gitmodules | 3 + CMakeLists.txt | 5 + LICENSE.md | 11 +- external/CONTENT.md | 8 + external/OpenCSD | 1 + tools/ctrace/.clang-format | 17 + tools/ctrace/.clangd | 5 + tools/ctrace/CMakeLists.txt | 39 +- tools/ctrace/README.md | 117 +++ .../cmake/dependencies/ConfigureOpenCSD.cmake | 116 +++ tools/ctrace/docs/THIRD_PARTY_NOTICES.md | 14 + tools/ctrace/docs/architecture.md | 177 +++++ tools/ctrace/docs/architecture.svg | 152 ++++ tools/ctrace/docs/constraints.md | 101 +++ tools/ctrace/docs/todo.md | 23 + tools/ctrace/src/CMakeLists.txt | 127 ++++ tools/ctrace/src/CtraceMain.cpp | 59 +- .../CtraceMain.h => src/CtraceMain.hpp} | 7 +- tools/ctrace/src/cli/CliOptions.hpp | 28 + tools/ctrace/src/cli/CliParser.cpp | 219 ++++++ tools/ctrace/src/cli/CliParser.hpp | 21 + tools/ctrace/src/control/DecodeConsumers.cpp | 102 +++ tools/ctrace/src/control/DecodeConsumers.hpp | 46 ++ tools/ctrace/src/control/FileDecodeJob.cpp | 227 ++++++ tools/ctrace/src/control/FileDecodeJob.hpp | 28 + .../ctrace/src/control/TraceDirectoryJob.cpp | 163 +++++ .../ctrace/src/control/TraceDirectoryJob.hpp | 24 + .../ctrace/src/decode/CortexMPostDecoder.cpp | 342 +++++++++ .../ctrace/src/decode/CortexMPostDecoder.hpp | 67 ++ .../src/decode/CortexMStreamDecoder.cpp | 91 +++ .../src/decode/CortexMStreamDecoder.hpp | 44 ++ tools/ctrace/src/decode/DecodePipeline.cpp | 47 ++ tools/ctrace/src/decode/DecodePipeline.hpp | 43 ++ tools/ctrace/src/decode/DwtPacketDecoder.cpp | 310 ++++++++ tools/ctrace/src/decode/DwtPacketDecoder.hpp | 64 ++ .../src/decode/OpenCsdErrorController.cpp | 403 ++++++++++ .../src/decode/OpenCsdErrorController.hpp | 82 +++ tools/ctrace/src/decode/OpenCsdItmDecoder.cpp | 321 ++++++++ tools/ctrace/src/decode/OpenCsdItmDecoder.hpp | 52 ++ tools/ctrace/src/decode/OpenCsdItmSession.cpp | 113 +++ tools/ctrace/src/decode/OpenCsdItmSession.hpp | 53 ++ .../src/decode/OpenCsdPacketCollector.cpp | 329 +++++++++ .../src/decode/OpenCsdPacketCollector.hpp | 63 ++ .../ctrace/src/decode/OpenCsdTraceElement.hpp | 64 ++ .../ctrace/src/diagnostics/DiagnosticSink.cpp | 93 +++ .../ctrace/src/diagnostics/DiagnosticSink.hpp | 79 ++ .../src/diagnostics/TraceIssueReporter.cpp | 131 ++++ .../src/diagnostics/TraceIssueReporter.hpp | 34 + tools/ctrace/src/main.cpp | 7 +- tools/ctrace/src/model/TraceEvent.hpp | 172 +++++ tools/ctrace/src/model/TraceSelection.cpp | 136 ++++ tools/ctrace/src/model/TraceSelection.hpp | 53 ++ tools/ctrace/src/model/TraceStreamId.hpp | 28 + .../ctrace/src/output/OutputRequirements.cpp | 385 ++++++++++ .../ctrace/src/output/OutputRequirements.hpp | 29 + tools/ctrace/src/output/TraceOutput.hpp | 35 + tools/ctrace/src/output/TraceOutputConfig.hpp | 53 ++ .../src/output/TraceOutputLifecycle.cpp | 148 ++++ .../src/output/TraceOutputLifecycle.hpp | 47 ++ tools/ctrace/src/output/csv/CsvFileOutput.cpp | 141 ++++ tools/ctrace/src/output/csv/CsvFileOutput.hpp | 35 + tools/ctrace/src/output/csv/CsvRowMapper.cpp | 180 +++++ tools/ctrace/src/output/csv/CsvRowMapper.hpp | 19 + .../ctrace/src/output/ctf/CtfBundleOutput.cpp | 237 ++++++ .../ctrace/src/output/ctf/CtfBundleOutput.hpp | 36 + tools/ctrace/src/output/ctf/CtfEncoder.cpp | 449 ++++++++++++ tools/ctrace/src/output/ctf/CtfEncoder.hpp | 73 ++ .../output/ctf/CtfExceptionLaneTracker.cpp | 120 +++ .../output/ctf/CtfExceptionLaneTracker.hpp | 56 ++ .../src/output/ctf/CtfMetadataWriter.cpp | 423 +++++++++++ .../src/output/ctf/CtfMetadataWriter.hpp | 22 + tools/ctrace/src/output/ctf/CtfSchema.hpp | 157 ++++ .../ctrace/src/output/ctf/CtfStreamWriter.cpp | 228 ++++++ .../ctrace/src/output/ctf/CtfStreamWriter.hpp | 76 ++ .../src/output/ctf/TraceCompassXmlWriter.cpp | 211 ++++++ .../src/output/ctf/TraceCompassXmlWriter.hpp | 16 + tools/ctrace/src/tracerun/CtraceRunMeta.cpp | 559 ++++++++++++++ tools/ctrace/src/tracerun/CtraceRunMeta.hpp | 70 ++ tools/ctrace/src/tracerun/TraceRunConfig.hpp | 217 ++++++ .../src/tracerun/TraceRunConfigReader.hpp | 19 + .../ctrace/src/tracerun/TraceRunDiscovery.cpp | 148 ++++ .../ctrace/src/tracerun/TraceRunDiscovery.hpp | 27 + .../src/tracerun/YmlTraceRunConfigReader.cpp | 633 ++++++++++++++++ .../src/tracerun/YmlTraceRunConfigReader.hpp | 18 + tools/ctrace/test/CMakeLists.txt | 1 + .../Blinky+STM32H747I-EVAL.SWO.csv | 532 ++++++++++++++ .../Blinky+STM32H747I-EVAL.SWO.raw | Bin 0 -> 12288 bytes .../Blinky+STM32H747I-EVAL.TB.raw | Bin 0 -> 4096 bytes .../Blinky+STM32H747I-EVAL.ctrace-run.yml | 59 ++ tools/ctrace/test/data/README.md | 20 + .../test/data/trace-run/Board.ctrace-run.yml | 36 + .../data/trace-run/Minimal.ctrace-run.yml | 5 + tools/ctrace/test/integration/CMakeLists.txt | 121 +++ tools/ctrace/test/integration/README.md | 10 + .../integration/src/CtraceTestCommand.cmake | 10 + .../integration/src/RunExpectedFailure.cmake | 29 + .../integration/src/RunTraceFixture.cmake | 70 ++ .../integration/src/RunTraceRunYaml.cmake | 101 +++ .../src/RunValidTraceFixture.cmake | 81 ++ tools/ctrace/test/unit/CMakeLists.txt | 53 +- tools/ctrace/test/unit/src/CtraceMainTest.cpp | 17 - .../test/unit/src/cli/CliParserTests.cpp | 147 ++++ .../unit/src/control/DecodeConsumersTests.cpp | 126 ++++ .../src/control/TraceDirectoryJobTests.cpp | 106 +++ .../src/decode/CortexMPostDecoderTests.cpp | 110 +++ .../unit/src/decode/DecodePipelineTests.cpp | 688 +++++++++++++++++ .../unit/src/decode/DwtPacketDecoderTests.cpp | 214 ++++++ .../decode/OpenCsdErrorControllerTests.cpp | 91 +++ .../decode/OpenCsdPacketCollectorTests.cpp | 112 +++ .../unit/src/diagnostics/DiagnosticsTests.cpp | 104 +++ .../unit/src/model/TraceSelectionTests.cpp | 51 ++ .../src/output/OutputRequirementsTests.cpp | 370 ++++++++++ .../src/output/TraceOutputLifecycleTests.cpp | 113 +++ .../output/csv/CsvFileOutputCriteriaTests.cpp | 56 ++ .../src/output/csv/CsvFileOutputTests.cpp | 119 +++ .../unit/src/output/csv/CsvRowMapperTests.cpp | 64 ++ .../src/output/ctf/CtfBundleOutputTests.cpp | 691 ++++++++++++++++++ .../unit/src/output/ctf/CtfEncoderTests.cpp | 183 +++++ .../unit/src/output/ctf/CtfSchemaTests.cpp | 111 +++ .../unit/src/tracerun/CtraceRunMetaTests.cpp | 36 + .../tracerun/TraceRunConfigReaderTests.cpp | 144 ++++ .../src/tracerun/TraceRunDiscoveryTests.cpp | 59 ++ .../test/unit/support/CtfTestSupport.hpp | 44 ++ tools/ctrace/test/unit/support/TestPath.hpp | 58 ++ .../ctrace/test/unit/support/TestSupport.hpp | 145 ++++ 131 files changed, 15062 insertions(+), 71 deletions(-) create mode 160000 external/OpenCSD create mode 100644 tools/ctrace/.clang-format create mode 100644 tools/ctrace/.clangd create mode 100644 tools/ctrace/README.md create mode 100644 tools/ctrace/cmake/dependencies/ConfigureOpenCSD.cmake create mode 100644 tools/ctrace/docs/THIRD_PARTY_NOTICES.md create mode 100644 tools/ctrace/docs/architecture.md create mode 100644 tools/ctrace/docs/architecture.svg create mode 100644 tools/ctrace/docs/constraints.md create mode 100644 tools/ctrace/docs/todo.md create mode 100644 tools/ctrace/src/CMakeLists.txt rename tools/ctrace/{include/CtraceMain.h => src/CtraceMain.hpp} (55%) create mode 100644 tools/ctrace/src/cli/CliOptions.hpp create mode 100644 tools/ctrace/src/cli/CliParser.cpp create mode 100644 tools/ctrace/src/cli/CliParser.hpp create mode 100644 tools/ctrace/src/control/DecodeConsumers.cpp create mode 100644 tools/ctrace/src/control/DecodeConsumers.hpp create mode 100644 tools/ctrace/src/control/FileDecodeJob.cpp create mode 100644 tools/ctrace/src/control/FileDecodeJob.hpp create mode 100644 tools/ctrace/src/control/TraceDirectoryJob.cpp create mode 100644 tools/ctrace/src/control/TraceDirectoryJob.hpp create mode 100644 tools/ctrace/src/decode/CortexMPostDecoder.cpp create mode 100644 tools/ctrace/src/decode/CortexMPostDecoder.hpp create mode 100644 tools/ctrace/src/decode/CortexMStreamDecoder.cpp create mode 100644 tools/ctrace/src/decode/CortexMStreamDecoder.hpp create mode 100644 tools/ctrace/src/decode/DecodePipeline.cpp create mode 100644 tools/ctrace/src/decode/DecodePipeline.hpp create mode 100644 tools/ctrace/src/decode/DwtPacketDecoder.cpp create mode 100644 tools/ctrace/src/decode/DwtPacketDecoder.hpp create mode 100644 tools/ctrace/src/decode/OpenCsdErrorController.cpp create mode 100644 tools/ctrace/src/decode/OpenCsdErrorController.hpp create mode 100644 tools/ctrace/src/decode/OpenCsdItmDecoder.cpp create mode 100644 tools/ctrace/src/decode/OpenCsdItmDecoder.hpp create mode 100644 tools/ctrace/src/decode/OpenCsdItmSession.cpp create mode 100644 tools/ctrace/src/decode/OpenCsdItmSession.hpp create mode 100644 tools/ctrace/src/decode/OpenCsdPacketCollector.cpp create mode 100644 tools/ctrace/src/decode/OpenCsdPacketCollector.hpp create mode 100644 tools/ctrace/src/decode/OpenCsdTraceElement.hpp create mode 100644 tools/ctrace/src/diagnostics/DiagnosticSink.cpp create mode 100644 tools/ctrace/src/diagnostics/DiagnosticSink.hpp create mode 100644 tools/ctrace/src/diagnostics/TraceIssueReporter.cpp create mode 100644 tools/ctrace/src/diagnostics/TraceIssueReporter.hpp create mode 100644 tools/ctrace/src/model/TraceEvent.hpp create mode 100644 tools/ctrace/src/model/TraceSelection.cpp create mode 100644 tools/ctrace/src/model/TraceSelection.hpp create mode 100644 tools/ctrace/src/model/TraceStreamId.hpp create mode 100644 tools/ctrace/src/output/OutputRequirements.cpp create mode 100644 tools/ctrace/src/output/OutputRequirements.hpp create mode 100644 tools/ctrace/src/output/TraceOutput.hpp create mode 100644 tools/ctrace/src/output/TraceOutputConfig.hpp create mode 100644 tools/ctrace/src/output/TraceOutputLifecycle.cpp create mode 100644 tools/ctrace/src/output/TraceOutputLifecycle.hpp create mode 100644 tools/ctrace/src/output/csv/CsvFileOutput.cpp create mode 100644 tools/ctrace/src/output/csv/CsvFileOutput.hpp create mode 100644 tools/ctrace/src/output/csv/CsvRowMapper.cpp create mode 100644 tools/ctrace/src/output/csv/CsvRowMapper.hpp create mode 100644 tools/ctrace/src/output/ctf/CtfBundleOutput.cpp create mode 100644 tools/ctrace/src/output/ctf/CtfBundleOutput.hpp create mode 100644 tools/ctrace/src/output/ctf/CtfEncoder.cpp create mode 100644 tools/ctrace/src/output/ctf/CtfEncoder.hpp create mode 100644 tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.cpp create mode 100644 tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.hpp create mode 100644 tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp create mode 100644 tools/ctrace/src/output/ctf/CtfMetadataWriter.hpp create mode 100644 tools/ctrace/src/output/ctf/CtfSchema.hpp create mode 100644 tools/ctrace/src/output/ctf/CtfStreamWriter.cpp create mode 100644 tools/ctrace/src/output/ctf/CtfStreamWriter.hpp create mode 100644 tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp create mode 100644 tools/ctrace/src/output/ctf/TraceCompassXmlWriter.hpp create mode 100644 tools/ctrace/src/tracerun/CtraceRunMeta.cpp create mode 100644 tools/ctrace/src/tracerun/CtraceRunMeta.hpp create mode 100644 tools/ctrace/src/tracerun/TraceRunConfig.hpp create mode 100644 tools/ctrace/src/tracerun/TraceRunConfigReader.hpp create mode 100644 tools/ctrace/src/tracerun/TraceRunDiscovery.cpp create mode 100644 tools/ctrace/src/tracerun/TraceRunDiscovery.hpp create mode 100644 tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp create mode 100644 tools/ctrace/src/tracerun/YmlTraceRunConfigReader.hpp create mode 100644 tools/ctrace/test/data/Blinky+STM32H747I-EVAL/Blinky+STM32H747I-EVAL.SWO.csv create mode 100644 tools/ctrace/test/data/Blinky+STM32H747I-EVAL/Blinky+STM32H747I-EVAL.SWO.raw create mode 100644 tools/ctrace/test/data/Blinky+STM32H747I-EVAL/Blinky+STM32H747I-EVAL.TB.raw create mode 100644 tools/ctrace/test/data/Blinky+STM32H747I-EVAL/Blinky+STM32H747I-EVAL.ctrace-run.yml create mode 100644 tools/ctrace/test/data/README.md create mode 100644 tools/ctrace/test/data/trace-run/Board.ctrace-run.yml create mode 100644 tools/ctrace/test/data/trace-run/Minimal.ctrace-run.yml create mode 100644 tools/ctrace/test/integration/CMakeLists.txt create mode 100644 tools/ctrace/test/integration/README.md create mode 100644 tools/ctrace/test/integration/src/CtraceTestCommand.cmake create mode 100644 tools/ctrace/test/integration/src/RunExpectedFailure.cmake create mode 100644 tools/ctrace/test/integration/src/RunTraceFixture.cmake create mode 100644 tools/ctrace/test/integration/src/RunTraceRunYaml.cmake create mode 100644 tools/ctrace/test/integration/src/RunValidTraceFixture.cmake delete mode 100644 tools/ctrace/test/unit/src/CtraceMainTest.cpp create mode 100644 tools/ctrace/test/unit/src/cli/CliParserTests.cpp create mode 100644 tools/ctrace/test/unit/src/control/DecodeConsumersTests.cpp create mode 100644 tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp create mode 100644 tools/ctrace/test/unit/src/decode/CortexMPostDecoderTests.cpp create mode 100644 tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp create mode 100644 tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp create mode 100644 tools/ctrace/test/unit/src/decode/OpenCsdErrorControllerTests.cpp create mode 100644 tools/ctrace/test/unit/src/decode/OpenCsdPacketCollectorTests.cpp create mode 100644 tools/ctrace/test/unit/src/diagnostics/DiagnosticsTests.cpp create mode 100644 tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp create mode 100644 tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp create mode 100644 tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp create mode 100644 tools/ctrace/test/unit/src/output/csv/CsvFileOutputCriteriaTests.cpp create mode 100644 tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp create mode 100644 tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp create mode 100644 tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp create mode 100644 tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp create mode 100644 tools/ctrace/test/unit/src/output/ctf/CtfSchemaTests.cpp create mode 100644 tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp create mode 100644 tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp create mode 100644 tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp create mode 100644 tools/ctrace/test/unit/support/CtfTestSupport.hpp create mode 100644 tools/ctrace/test/unit/support/TestPath.hpp create mode 100644 tools/ctrace/test/unit/support/TestSupport.hpp diff --git a/.gitattributes b/.gitattributes index cf2aa917c..2e99adeb0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -46,3 +46,4 @@ # Denote all files that are truly binary and should not be modified. *.png binary *.jpg binary +*.raw binary diff --git a/.github/matrix_includes_ctrace.json b/.github/matrix_includes_ctrace.json index 21e94f811..68a8ca298 100644 --- a/.github/matrix_includes_ctrace.json +++ b/.github/matrix_includes_ctrace.json @@ -1,6 +1,6 @@ [ { - "runs_on":"macos-14", + "runs_on":"macos-15", "target":"darwin", "arch": "arm64", "binary": "ctrace", diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 977acad9d..bd31e7460 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -6,11 +6,22 @@ on: branches: [ main ] paths: - '.github/workflows/codeql-analysis.yml' + - '.github/codeql-config.yml' + - '.gitmodules' - '**/*.c' + - '**/*.cc' - '**/*.cpp' - '**/*.h' + - '**/*.hpp' - '**/*.in' - '**/CMakeLists.txt' + - '**/*.cmake' + - 'cmake/**' + - 'external/OpenCSD' + - 'external/cxxopts' + - 'external/cxxopts.patch' + - 'external/yaml-cpp' + - 'external/yaml-cpp.patch' - '!**/*.md' push: branches: [ main ] diff --git a/.github/workflows/ctrace.yml b/.github/workflows/ctrace.yml index fa1be0599..90f30fc3c 100644 --- a/.github/workflows/ctrace.yml +++ b/.github/workflows/ctrace.yml @@ -1,23 +1,40 @@ name: ctrace on: workflow_call: + workflow_dispatch: push: branches: - main paths: - '.github/workflows/ctrace.yml' + - '.github/workflows/shared_matrix_prep.yml' + - '.github/workflows/shared_setup_env.yml' - '.github/matrix_includes_ctrace.json' + - '.gitmodules' - 'cmake/**' - 'CMakeLists.txt' + - 'external/OpenCSD' + - 'external/cxxopts' + - 'external/cxxopts.patch' + - 'external/yaml-cpp' + - 'external/yaml-cpp.patch' - 'tools/ctrace/**' - '!**/docs/**/*' - '!**/*.md' pull_request: paths: - '.github/workflows/ctrace.yml' + - '.github/workflows/shared_matrix_prep.yml' + - '.github/workflows/shared_setup_env.yml' - '.github/matrix_includes_ctrace.json' + - '.gitmodules' - 'cmake/**' - 'CMakeLists.txt' + - 'external/OpenCSD' + - 'external/cxxopts' + - 'external/cxxopts.patch' + - 'external/yaml-cpp' + - 'external/yaml-cpp.patch' - 'tools/ctrace/**' - '!**/docs/**/*' - '!**/*.md' @@ -74,6 +91,30 @@ jobs: target: ctrace arch: ${{ matrix.arch }} + - name: Run ctrace integration tests + if: (matrix.target != 'windows' || matrix.arch != 'arm64') + run: ctest -V -C Release -R '^ctrace-' + working-directory: ./build + + - name: Verify Windows Arm64 binary architecture + if: (matrix.target == 'windows' && matrix.arch == 'arm64') + shell: pwsh + run: | + $binaryPath = "./build/tools/ctrace/${{ matrix.target }}-${{ matrix.arch }}/Release/${{ matrix.binary }}" + $bytes = [System.IO.File]::ReadAllBytes($binaryPath) + if ($bytes.Length -lt 64) { + throw "ctrace.exe is too short to contain a valid PE header" + } + $peOffset = [System.BitConverter]::ToUInt32($bytes, 0x3c) + if ($peOffset + 6 -gt $bytes.Length) { + throw "ctrace.exe contains an invalid PE header offset" + } + $signature = [System.Text.Encoding]::ASCII.GetString($bytes, $peOffset, 4) + $machine = [System.BitConverter]::ToUInt16($bytes, $peOffset + 4) + if ($signature -ne "PE`0`0" -or $machine -ne 0xAA64) { + throw "ctrace.exe is not an Arm64 PE binary (machine 0x$($machine.ToString('X4')))" + } + - name: Install strip tools if: | github.event_name == 'release' && @@ -96,6 +137,19 @@ jobs: strip "$binary_path" fi + - name: Smoke-test release binary + if: | + github.event_name == 'release' && + (matrix.target != 'windows' || matrix.arch != 'arm64') + shell: bash + run: | + binary_path="./build/tools/ctrace/${{ matrix.target }}-${{ matrix.arch }}/Release/${{ matrix.binary }}" + if [[ "${{ matrix.target }}" == "linux" && "${{ matrix.arch }}" == "arm64" ]]; then + qemu-aarch64 -L /usr/aarch64-linux-gnu "$binary_path" --version + else + "$binary_path" --version + fi + - name: Archive ctrace uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -116,11 +170,18 @@ jobs: steps: - name: Checkout devtools uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: true - name: Create distribution folders run: | mkdir -p tools/ctrace/distribution/bin - cp LICENSE tools/ctrace/distribution/ + mkdir -p tools/ctrace/distribution/THIRD_PARTY_LICENSES + cp LICENSE tools/ctrace/distribution/LICENSE.txt + cp tools/ctrace/docs/THIRD_PARTY_NOTICES.md tools/ctrace/distribution/ + cp external/cxxopts/LICENSE tools/ctrace/distribution/THIRD_PARTY_LICENSES/cxxopts.txt + cp external/yaml-cpp/LICENSE tools/ctrace/distribution/THIRD_PARTY_LICENSES/yaml-cpp.txt + cp external/OpenCSD/LICENSE tools/ctrace/distribution/THIRD_PARTY_LICENSES/OpenCSD.txt - name: Download ctrace linux-amd64 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -159,8 +220,13 @@ jobs: chmod +x bin/linux-arm64/ctrace working-directory: tools/ctrace/distribution - - name: Zip distribution folder - run: zip -r ctrace.zip * + - name: Verify and zip distribution + run: | + find bin THIRD_PARTY_LICENSES -type f -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS + sha256sum LICENSE.txt THIRD_PARTY_NOTICES.md >> SHA256SUMS + sha256sum --check SHA256SUMS + zip -r ctrace.zip bin LICENSE.txt THIRD_PARTY_LICENSES THIRD_PARTY_NOTICES.md SHA256SUMS + sha256sum ctrace.zip > ctrace.zip.sha256 working-directory: tools/ctrace/distribution - name: Attach zip archive to release assets @@ -172,6 +238,15 @@ jobs: overwrite: true asset_name: ctrace.zip + - name: Attach archive checksum to release assets + uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: tools/ctrace/distribution/ctrace.zip.sha256 + tag: ${{ github.ref }} + overwrite: true + asset_name: ctrace.zip.sha256 + unittest: needs: [ setup, matrix_prep ] name: unittest (${{ matrix.runs_on }}, ${{ matrix.arch }}) @@ -201,16 +276,16 @@ jobs: arch: ${{ matrix.arch }} - name: Run ctrace unit tests - if: (matrix.arch != 'arm64') + if: (matrix.target != 'windows' || matrix.arch != 'arm64') run: ctest -C Debug -R CtraceUnitTests working-directory: ./build - name: Archive unit tests results - if: always() && (matrix.arch != 'arm64') + if: always() && (matrix.target != 'windows' || matrix.arch != 'arm64') uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: unit_test_result_ctrace-${{ matrix.target }}-${{ matrix.arch }} - path: ./build/test_reports/ctraceunittests-*.xml + path: ./build/test_reports/CtraceUnitTests.*.xml retention-days: ${{ needs.setup.outputs.retention_days }} if-no-files-found: error @@ -252,8 +327,12 @@ jobs: build_type: Debug target: CtraceUnitTests - - name: Run CtraceUnitTests - run: ctest -V -C Debug -R CtraceUnitTests + - name: Build ctrace integration target + run: cmake --build . --target ctrace + working-directory: ./build + + - name: Run ctrace tests + run: ctest -V -C Debug -R '^(CtraceUnitTests|ctrace-)' working-directory: ./build - name: Get retention days @@ -266,7 +345,7 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: unit_test_result_ctrace-linux-amd64 - path: ./build/test_reports/ctraceunittests-*.xml + path: ./build/test_reports/CtraceUnitTests.*.xml retention-days: ${{ steps.var.outputs.retention_days }} if-no-files-found: error @@ -279,7 +358,7 @@ jobs: - name: Generate coverage report run: | lcov-1.15/bin/lcov --rc lcov_branch_coverage=1 --rc geninfo_no_exception_branch=1 -c --directory . --output-file full_coverage.info - lcov-1.15/bin/lcov --rc lcov_branch_coverage=1 --rc geninfo_no_exception_branch=1 -e full_coverage.info '*/tools/ctrace/include/*' '*/tools/ctrace/src/*' -o coverage_ctrace.info + lcov-1.15/bin/lcov --rc lcov_branch_coverage=1 --rc geninfo_no_exception_branch=1 -e full_coverage.info '*/tools/ctrace/src/*' -o coverage_ctrace.info genhtml coverage_ctrace.info --output-directory coverage_ctrace --branch-coverage working-directory: ./build/tools/ctrace diff --git a/.github/workflows/shared_matrix_prep.yml b/.github/workflows/shared_matrix_prep.yml index cb8e9efd0..70ca57a30 100644 --- a/.github/workflows/shared_matrix_prep.yml +++ b/.github/workflows/shared_matrix_prep.yml @@ -39,6 +39,6 @@ jobs: - id: set-matrix run: | publicRepo=$(echo '${{ github.event.repository.private && 'privateRepo' || 'publicRepo' }}') - matrix=$(jq --arg publicRepo "$publicRepo" 'map(. | select((.runOn==$publicRepo) or (.runOn=="always")) )' matrix_includes_${{ inputs.workflow_name }}.json) - echo "matrix={\"include\":$(echo $matrix)}\"" >> $GITHUB_OUTPUT + matrix=$(jq -c --arg publicRepo "$publicRepo" '{include: map(select(.runOn == $publicRepo or .runOn == "always"))}' matrix_includes_${{ inputs.workflow_name }}.json) + echo "matrix=${matrix}" >> "$GITHUB_OUTPUT" working-directory: .github/ diff --git a/.github/workflows/shared_setup_env.yml b/.github/workflows/shared_setup_env.yml index 857cd9d49..3f7391ae6 100644 --- a/.github/workflows/shared_setup_env.yml +++ b/.github/workflows/shared_setup_env.yml @@ -5,7 +5,6 @@ on: inputs: run_if: description: 'True if workflow needs to run' - default: false required: true type: boolean outputs: diff --git a/.gitmodules b/.gitmodules index c36eb1c30..c6be8207b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -32,3 +32,6 @@ [submodule "external/json-rpc-cxx"] path = external/json-rpc-cxx url = https://github.com/jsonrpcx/json-rpc-cxx.git +[submodule "external/OpenCSD"] + path = external/OpenCSD + url = https://github.com/Linaro/OpenCSD.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 085c07dd7..0fd913397 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,6 +158,11 @@ apply_patch(${CMAKE_CURRENT_SOURCE_DIR}/external/cxxopts) apply_patch(${CMAKE_CURRENT_SOURCE_DIR}/external/xerces-c) apply_patch(${CMAKE_CURRENT_SOURCE_DIR}/external/yaml-cpp) +# OpenCSD +if(NOT LIBS_ONLY) + include(tools/ctrace/cmake/dependencies/ConfigureOpenCSD.cmake) +endif() + # Google Test Framework set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) add_subdirectory(external/googletest) diff --git a/LICENSE.md b/LICENSE.md index 98ec77440..95a166e79 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -30,17 +30,18 @@ from https://github.com/ARM-software/CMSIS_5/tree/develop/Device/ARM. ## External Dependencies -The components listed below are not redistributed with the project but are used internally for building, development, -or testing purposes. +The components listed below are used for building, development, testing, or are incorporated into distributed tool +binaries as indicated by the Usage column. | Component | Version | License | Origin | Usage | | --------- | ------- | ------- | ------ | ----- | -|Google C++ Testing Framework|1.11.0|[BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause)|https://github.com/google/googletest.git| Testing | -|cxxopts|2.2.1|[MIT](https://opensource.org/licenses/MIT)|https://github.com/jarro2783/cxxopts.git| packgen | -|yaml-cpp|0.7.0|[MIT](https://opensource.org/licenses/MIT)|https://github.com/jbeder/yaml-cpp.git| packgen | +|Google C++ Testing Framework|1.16.0|[BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause)|https://github.com/google/googletest.git| Testing | +|cxxopts|3.0.0|[MIT](https://opensource.org/licenses/MIT)|https://github.com/jarro2783/cxxopts.git| packgen, ctrace (statically incorporated) | +|yaml-cpp|0.8.0|[MIT](https://opensource.org/licenses/MIT)|https://github.com/jbeder/yaml-cpp.git| packgen, ctrace (statically linked) | +|OpenCSD|1.8.3|[BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause)|https://github.com/Linaro/OpenCSD.git| ctrace (statically linked) | |[GetGitRevisionDescription.cmake](./cmake/GetGitRevisionDescription.cmake)||[Boost Software License, Version 1.0](http://www.boost.org/LICENSE_1_0.txt)||Continuous integration| |[GetGitRevisionDescription.cmake.in](./cmake/GetGitRevisionDescription.cmake.in)||[Boost Software License, Version 1.0](http://www.boost.org/LICENSE_1_0.txt)||Continuous integration| |json|3.10.5|[MIT](https://opensource.org/licenses/MIT)|https://github.com/nlohmann/json| yml-schema-checker | diff --git a/external/CONTENT.md b/external/CONTENT.md index a5dc3394d..013b5809d 100644 --- a/external/CONTENT.md +++ b/external/CONTENT.md @@ -6,6 +6,7 @@ should be committed into this folder. ```txt 📂external + ┣ 📂OpenCSD ┣ 📂cxxopts ┣ 📂fmt ┣ 📂googletest @@ -14,6 +15,13 @@ should be committed into this folder. ┗ 📂yaml-cpp ``` +## OpenCSD + + +The [OpenCSD](./OpenCSD) directory contains the CoreSight trace decode library +used by ctrace and is fetched from +[here](https://github.com/Linaro/OpenCSD). + ## cxxopts diff --git a/external/OpenCSD b/external/OpenCSD new file mode 160000 index 000000000..59f153206 --- /dev/null +++ b/external/OpenCSD @@ -0,0 +1 @@ +Subproject commit 59f15320637ce6e510604e7bed550d7ad109d15d diff --git a/tools/ctrace/.clang-format b/tools/ctrace/.clang-format new file mode 100644 index 000000000..f6feb3305 --- /dev/null +++ b/tools/ctrace/.clang-format @@ -0,0 +1,17 @@ +--- +BasedOnStyle: LLVM +IndentWidth: 2 +ContinuationIndentWidth: 4 +ConstructorInitializerIndentWidth: 2 +TabWidth: 2 +UseTab: Never +ColumnLimit: 120 +BreakBeforeBraces: Custom +BraceWrapping: + AfterFunction: true +PointerAlignment: Left +DerivePointerAlignment: false +SortIncludes: Never +SpaceBeforeParens: ControlStatements +AllowShortFunctionsOnASingleLine: Empty +... diff --git a/tools/ctrace/.clangd b/tools/ctrace/.clangd new file mode 100644 index 000000000..3ea8cca5e --- /dev/null +++ b/tools/ctrace/.clangd @@ -0,0 +1,5 @@ +CompileFlags: + CompilationDatabase: ../../build +Diagnostics: + UnusedIncludes: Strict + MissingIncludes: Strict diff --git a/tools/ctrace/CMakeLists.txt b/tools/ctrace/CMakeLists.txt index 6ddcbc770..fd2d81a52 100644 --- a/tools/ctrace/CMakeLists.txt +++ b/tools/ctrace/CMakeLists.txt @@ -9,21 +9,34 @@ get_version_from_git_tag("tools/ctrace/") project(ctrace VERSION "${PROJECT_VERSION}") dump_cmake_project_version() -set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT ctrace) - -# ctrace library -add_library(ctracelib OBJECT src/CtraceMain.cpp include/CtraceMain.h) -target_include_directories(ctracelib PUBLIC include) +set(CTRACE_VERSION_FULL "${PROJECT_VERSION_FULL}") +set(CTRACE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") -set_property(TARGET ctracelib PROPERTY - MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") - -# ctrace target -add_executable(ctrace src/main.cpp) -target_link_libraries(ctrace PRIVATE ctracelib) +set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT ctrace) -set_property(TARGET ctrace PROPERTY - MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +function(ctrace_configure_target target) + target_compile_definitions(${target} PRIVATE + $<$:NOMINMAX> + ) + target_compile_options(${target} PRIVATE + $<$:-Wall> + $<$:-Wextra> + $<$:-Wpedantic> + $<$:/W4> + ) +endfunction() + +add_subdirectory(src) + +add_executable(ctrace + src/CtraceMain.cpp + src/CtraceMain.hpp + src/main.cpp +) +target_link_libraries(ctrace PRIVATE + ctrace::control +) +ctrace_configure_target(ctrace) if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") diff --git a/tools/ctrace/README.md b/tools/ctrace/README.md new file mode 100644 index 000000000..9e3ea4d48 --- /dev/null +++ b/tools/ctrace/README.md @@ -0,0 +1,117 @@ +# ctrace + +`ctrace` converts CMSIS Cortex-M trace captures into outputs that can be inspected or processed by other tools. It +combines a `.ctrace-run.yml` description with matching raw trace files from one trace directory. + +The current implementation decodes SWO/ITM and DWT data. Trace Bus (`*.TB.raw`) files are discovered and reported, +but deliberately skipped until that decoder is implemented. + +![ctrace architecture](docs/architecture.svg) + +## Usage + +```text +ctrace [options] + + -t, --target Process one solution set; otherwise process all + --csv Generate CSV output + --ctf Generate CTF and Trace Compass XML output + -a, --all Generate all output formats + --type Select event types + --stream Select CoreSight Trace Bus IDs (0 to 111) + -V, --version Print the version +``` + +Values for `--type` and `--stream` are space-separated. With no output option, `ctrace` validates and decodes the +capture without writing output files. + +The first release exposes output selectors for `itm`, `dwt`, `exception`, `global_ts`, `overflow`, and `error`. +Decoded DWT event counters, PMU packets, and PC samples remain disabled until their output semantics are implemented. + +A trace directory uses solution-set-based file names: + +```text +.trace/ + Board.ctrace-run.yml + Board.SWO.raw + Board.TB.raw # discovered, currently not decoded +``` + +For `ctrace .trace --target Board --all`, the supported input produces: + +```text +.trace/ + Board.SWO.csv + Board.ctf/ + metadata + stream_0 + Board.SWO.traceanalysis.xml +``` + +The YAML reader validates fields consumed by `ctrace` and ignores unrelated or unknown fields. A malformed consumed +field is an error; an unneeded extension to the trace-run format does not break the tool. + +## Project structure + +| Path | Responsibility | +|---|---| +| `src/CtraceMain.hpp` | Platform-independent entry point used by the executable trampoline | +| `src/cli` | Command-line parsing and validation | +| `src/control` | Trace-directory orchestration, raw-file access, and per-file decode jobs | +| `src/tracerun` | Trace-run discovery, YAML parsing, and normalized metadata | +| `src/decode` | OpenCSD adapter, recovery, and Cortex-M semantic decoding | +| `src/model` | Backend-independent trace events and selections | +| `src/output/csv` | CSV schema and writer | +| `src/output/ctf` | CTF bundle and Trace Compass XML writers | +| `src/diagnostics` | Structured diagnostics and trace-issue reporting | +| `test/unit` | GoogleTest cases, arranged like the production modules | +| `test/integration` | File-oriented executable tests run through CTest | +| `test/data` | Stable fixtures and expected output | + +The module boundaries, dependency direction, runtime flow, and extension points are described in the +[architecture documentation](docs/architecture.md). See also the [verified constraints](docs/constraints.md) and the +compact [remaining TODO list](docs/todo.md). + +## Build and test + +Initialize all dependencies and configure the repository from its root: + +Python 3 is required when configuring the integration tests because one small, textual SWO byte sequence is +materialized below the build tree. + +```bash +git submodule update --init --recursive +cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug +cmake --build build --target ctrace CtraceUnitTests +``` + +Run the independently discoverable GoogleTest cases and the executable-level integration tests: + +```bash +ctest --test-dir build -C Debug -R '^(CtraceUnitTests|ctrace-)' +``` + +Editors using `clangd` should open the devtools repository root and configure into `build`. The tool-local +`.clangd` file points clangd at that compilation database. + +## Dependencies and releases + +`ctrace` uses the devtools copies of `cxxopts`, `yaml-cpp`, GoogleTest, and the `external/OpenCSD` submodule. It does +not carry private copies below `tools/ctrace`. + +Publishing a GitHub Release for a tag named `tools/ctrace/` runs the ctrace workflow. The workflow builds +Windows AMD64/Arm64, Linux AMD64/Arm64, and macOS Arm64 variants and attaches `ctrace.zip` to the release. Unit and +integration tests run on Windows AMD64 and both Linux architectures (Linux Arm64 through QEMU), as well as on the +native macOS Arm64 runner. Windows Arm64 is cross-built and its PE machine type is verified. Coverage runs on Linux +AMD64. The executable version is derived from the same tool-specific tag. + +The project is licensed under Apache-2.0. Product dependencies and their licenses are recorded in the repository's +top-level `LICENSE.md`. The release archive contains the project license, third-party notices, the exact license text +for every statically incorporated dependency, and checksums for all included files. The separately published +`ctrace.zip.sha256` verifies the archive itself. Release artifacts are currently unsigned; signing, macOS +notarization, and a formal SBOM remain explicit release-hardening decisions. + +The code, build, test, and packaging structure is ready for a first open-source release candidate. The Blinky SWO and +TB captures are approved redistributable test assets, as documented in the +[fixture README](test/data/README.md). Hosted platform and package verification remains tracked in the +[release TODO](docs/todo.md). diff --git a/tools/ctrace/cmake/dependencies/ConfigureOpenCSD.cmake b/tools/ctrace/cmake/dependencies/ConfigureOpenCSD.cmake new file mode 100644 index 000000000..dadd8dd2b --- /dev/null +++ b/tools/ctrace/cmake/dependencies/ConfigureOpenCSD.cmake @@ -0,0 +1,116 @@ +# Copyright (c) 2026 Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# Generated with AI + +set(CTRACE_OPENCSD_SOURCE_DIR "${CMAKE_SOURCE_DIR}/external/OpenCSD") +if(NOT EXISTS "${CTRACE_OPENCSD_SOURCE_DIR}/CMakeLists.txt") + message(FATAL_ERROR + "OpenCSD is unavailable. Initialize the external/OpenCSD submodule." + ) +endif() + +function(ctrace_apply_opencsd_itm_empty_packet_fix source_dir) + set(source_file "${source_dir}/decoder/source/itm/trc_pkt_proc_itm.cpp") + if(NOT EXISTS "${source_file}") + message(FATAL_ERROR "OpenCSD ITM packet processor not found: ${source_file}") + endif() + + file(READ "${source_file}" source_contents) + set(unsafe_expression "&m_packet_data[0]") + set(safe_expression "m_packet_data.data()") + string(FIND "${source_contents}" "${unsafe_expression}" unsafe_position) + if(unsafe_position EQUAL -1) + string(FIND "${source_contents}" "${safe_expression}" safe_position) + if(safe_position EQUAL -1) + message(FATAL_ERROR + "Unsupported OpenCSD ITM packet processor; expected empty-packet fix location was not found." + ) + endif() + return() + endif() + + string(REPLACE "${unsafe_expression}" "${safe_expression}" patched_contents "${source_contents}") + set(patched_dir "${CMAKE_BINARY_DIR}/patched-opencsd/decoder/source/itm") + set(patched_file "${patched_dir}/trc_pkt_proc_itm.cpp") + file(MAKE_DIRECTORY "${patched_dir}") + set(write_patched_file TRUE) + if(EXISTS "${patched_file}") + file(READ "${patched_file}" existing_patched_contents) + if(existing_patched_contents STREQUAL patched_contents) + set(write_patched_file FALSE) + endif() + endif() + if(write_patched_file) + file(WRITE "${patched_file}" "${patched_contents}") + endif() + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${source_file}") + + get_target_property(opencsd_sources opencsd SOURCES) + set(patched_sources) + set(replaced_source FALSE) + foreach(opencsd_source IN LISTS opencsd_sources) + if(opencsd_source MATCHES "(^|/)decoder/source/itm/trc_pkt_proc_itm\\.cpp$") + list(APPEND patched_sources "${patched_file}") + set(replaced_source TRUE) + else() + list(APPEND patched_sources "${opencsd_source}") + endif() + endforeach() + if(NOT replaced_source) + message(FATAL_ERROR "OpenCSD target does not contain the ITM packet processor source.") + endif() + set_property(TARGET opencsd PROPERTY SOURCES ${patched_sources}) +endfunction() + +function(ctrace_remove_opencsd_clang_cl_option_for_msvc) + if(NOT MSVC OR NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + return() + endif() + + set_property(TARGET opencsd_options PROPERTY INTERFACE_COMPILE_OPTIONS) + target_compile_options(opencsd PRIVATE /EHsc /W0) +endfunction() + +set(OPENCSD_BUILD_MIN_LIB_STATIC ON CACHE BOOL "" FORCE) +set(OPENCSD_FULL_PROJECT_LAYOUT OFF CACHE BOOL "" FORCE) + +# Do not let the embedded project register its own tests or replace the +# devtools install prefix. +get_property(ctrace_had_build_testing CACHE BUILD_TESTING PROPERTY TYPE SET) +if(ctrace_had_build_testing) + get_property(ctrace_saved_build_testing CACHE BUILD_TESTING PROPERTY VALUE) +endif() +set(BUILD_TESTING OFF CACHE BOOL "Build the testing tree." FORCE) + +set(ctrace_saved_install_prefix_default "${CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT}") +set(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT FALSE) + +add_subdirectory( + "${CTRACE_OPENCSD_SOURCE_DIR}" + "${CMAKE_BINARY_DIR}/external/OpenCSD" + EXCLUDE_FROM_ALL +) + +if(ctrace_had_build_testing) + set(BUILD_TESTING "${ctrace_saved_build_testing}" CACHE BOOL "Build the testing tree." FORCE) +else() + unset(BUILD_TESTING CACHE) +endif() +set(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT "${ctrace_saved_install_prefix_default}") + +ctrace_apply_opencsd_itm_empty_packet_fix("${CTRACE_OPENCSD_SOURCE_DIR}") +ctrace_remove_opencsd_clang_cl_option_for_msvc() +target_compile_definitions(opencsd PRIVATE ENABLE_LARGE_TRACE_SOURCES) + +add_library(ctrace-opencsd INTERFACE) +add_library(ctrace::opencsd ALIAS ctrace-opencsd) +target_compile_definitions(ctrace-opencsd INTERFACE + ENABLE_LARGE_TRACE_SOURCES +) +target_include_directories(ctrace-opencsd SYSTEM INTERFACE + "${CTRACE_OPENCSD_SOURCE_DIR}/decoder/include" +) +target_link_libraries(ctrace-opencsd INTERFACE + "$" +) diff --git a/tools/ctrace/docs/THIRD_PARTY_NOTICES.md b/tools/ctrace/docs/THIRD_PARTY_NOTICES.md new file mode 100644 index 000000000..f978a2e0f --- /dev/null +++ b/tools/ctrace/docs/THIRD_PARTY_NOTICES.md @@ -0,0 +1,14 @@ +# ctrace Third-Party Notices + +The ctrace executable incorporates the following open-source components. The +release archive contains the unmodified license text for each component under +`THIRD_PARTY_LICENSES`. + +| Component | Version | License | Source | +| --------- | ------: | ------------ | -------------------------------------- | +| cxxopts | 3.0.0 | MIT | | +| yaml-cpp | 0.8.0 | MIT | | +| OpenCSD | 1.8.3 | BSD-3-Clause | | + +GoogleTest is used only to build the test executable and is not incorporated +into the released ctrace executable. diff --git a/tools/ctrace/docs/architecture.md b/tools/ctrace/docs/architecture.md new file mode 100644 index 000000000..783818f70 --- /dev/null +++ b/tools/ctrace/docs/architecture.md @@ -0,0 +1,177 @@ +# ctrace Architecture + +This document describes the internal structure of `ctrace`, the runtime data flow, and the intended extension points. +For command-line usage, build instructions, and release information, see the [project README](../README.md). The +[verified constraints](constraints.md) record preserved contracts; the compact [TODO list](todo.md) tracks remaining +work. + +![ctrace architecture](architecture.svg) + +## Scope + +`ctrace` combines a trace-run configuration with raw CoreSight trace data and converts supported trace channels into +backend-independent semantic events. Output backends consume these events to create CSV or CTF artifacts. + +The first release profile supports SWO data containing ITM and DWT packets. Its stable output types are `itm`, `dwt`, +`exception`, `global_ts`, `overflow`, and `error`. DWT event-counter and PMU packets are retained internally but do +not have public output semantics yet; periodic PC samples remain disabled. Trace Bus input is discovered so that a +complete trace directory can be inspected, but `*.TB.raw` files are reported and skipped until a decoder is +implemented. + +The 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. + +## Runtime flow + +1. `CtraceMain` parses and validates the command line. +2. `TraceRunDiscovery` finds one or all `.ctrace-run.yml` files in the selected trace directory. +3. `YmlTraceRunConfigReader` reads fields used by `ctrace`; unrelated and unknown YAML fields are ignored. +4. `CtraceRunMeta` normalizes processor, timestamp, route, ITM, and DWT source metadata. +5. `TraceDirectoryJob` associates the configuration with matching raw trace channels. +6. `FileDecodeJob` creates the requested output plan and reads supported raw files in 64 KiB blocks. +7. `DecodePipeline` receives non-owning `RawByteView` values while preserving decoder state across every file-read + boundary. +8. The OpenCSD adapter decodes ITM protocol elements and preserves decoder warnings, errors, and recovery boundaries. +9. The Cortex-M post-decoder converts protocol elements into semantic `TraceEvent` values. +10. `DecodeConsumers` forwards every event to diagnostics and the selected output backends. +11. Output lifecycle handling completes valid artifacts or removes incomplete artifacts after a fatal failure. + +Without `--csv`, `--ctf`, or `--all`, the same pipeline runs in validation-only mode without creating output files. + +## Module boundaries + +### Entry point and orchestration + +| Module | Responsibility | +|---|---| +| `src/CtraceMain.hpp` | Platform-independent entry point used by the executable trampoline | +| `src/cli` | Command-line parsing, value normalization, and validation | +| `src/control` | Solution-set orchestration, raw-file access, output setup, and per-file decode jobs | +| `src/diagnostics` | Structured diagnostics, severity tracking, and decoder issue reporting | + +`control` is the composition layer. The raw-file reader is a private implementation detail of `FileDecodeJob`, not a +separate module or public abstraction. Control may depend on the other application modules, while lower-level modules +must not depend on control jobs or command-line details. + +### Trace-run metadata + +| Module | Responsibility | +|---|---| +| `src/tracerun` | File discovery, YAML parsing, schema subset validation, and normalized metadata | + +The YAML reader intentionally consumes only data required by `ctrace`. A malformed consumed field is an error, while +an unknown field is ignored. This permits compatible trace-run format extensions without weakening validation of the +data used for decoding or output generation. + +`CtraceRunMeta` is the boundary between the YAML representation and runtime processing. Decode and output modules use +normalized metadata instead of navigating YAML nodes. + +### Decode and event model + +| Module | Responsibility | +|---|---| +| `src/decode` | Raw-byte decoder interface, OpenCSD integration, packet recovery, timestamps, and Cortex-M semantics | +| `src/model` | Backend-independent event types, quality information, and event selection | + +OpenCSD is isolated behind adapter classes in `src/decode`. OpenCSD-specific elements do not escape into the output +modules. The post-decoder maps them to `TraceEvent` variants such as software trace, DWT data, exceptions, timestamps, +overflow, and trace issues. `TraceSelection` owns the stable public type names and maps semantic events onto that +release-facing set. + +Each event can retain its raw index, Trace Bus ID, timestamp, and quality state. This allows diagnostics and backends +to make independent decisions without reconstructing decoder state. + +### Output + +| Module | Responsibility | +|---|---| +| `src/output` | Backend requirements, output planning, and lifecycle management | +| `src/output/csv` | Stable CSV schema, row mapping, filtering, and file output | +| `src/output/ctf` | CTF metadata and stream encoding plus Trace Compass analysis XML | + +Output requirements are evaluated per backend. For example, missing CTF-specific metadata may disable CTF while an +independent CSV output remains valid. `--all` therefore does not make the backends share failure state unnecessarily. + +Outputs use an explicit `start`, `writeEvent`, `stop`, and `abort` lifecycle. A successful backend can finish even if +another backend fails. Fatal decode or finalization failures trigger cleanup of incomplete artifacts. + +## Diagnostics and failure semantics + +Diagnostics carry a severity, category, code, message, context, and impact. Severity describes the issue, while impact +determines whether the current job must fail. This distinction allows a trace-run generation error to remain visible +without necessarily preventing the decoding of otherwise valid trace input. + +Decoder issue packets remain part of the event stream. They can therefore be written to CSV or CTF and reported to +stderr independently of payload filters. Repeated issues are not silently collapsed. + +Processing continues with other solution sets where possible. The executable exits with a non-zero status if any +fatal diagnostic occurred. + +## External dependencies + +| Dependency | Use | +|---|---| +| `cxxopts` | Command-line parsing | +| `yaml-cpp` | Trace-run YAML parsing | +| `OpenCSD` 1.8.3 | ITM protocol decoding; pinned as a repository submodule | +| GoogleTest | Unit-test framework; not linked into the product executable | + +Dependencies are provided by the devtools repository. `tools/ctrace` does not maintain private library copies. + +## Extension points + +### Add a raw trace channel + +Implement a decoder that produces `TraceEvent` values, add it below `src/decode`, and select it in the control layer +for the corresponding channel. A Trace Bus implementation should replace the current warning and skip behavior while +reusing diagnostics, event selection, and output backends. + +### Add an event type + +Extend the model and its stable type names first. Then update the relevant decoder, selection behavior, and every +backend that can represent the event. Tests should cover semantic mapping separately from backend serialization. + +### Add an output backend + +Implement `TraceOutput`, define backend-specific preflight requirements, and add it to the output plan and lifecycle. +Do not introduce backend-specific state into the decode pipeline or event model. + +## Test architecture + +Unit tests under `test/unit/src` mirror the production modules. Shared file, event, and diagnostic helpers live under +`test/unit/support`. GoogleTest discovery registers every test case separately with CTest, so IDEs and CI can execute +individual cases. + +Integration tests under `test/integration` execute the real `ctrace` binary and verify command-line behavior, +diagnostics, output cleanup, and fixture conversion. Test data and expected artifacts live under `test/data`; generated +files are written only below the CMake build directory. + +The `CtraceUnitTests` target preserves one CI entry point while linking the ctrace modules explicitly. Integration +tests use the `ctrace-` CTest name prefix. The release workflow runs them on Windows AMD64, Linux AMD64, Linux Arm64 +through QEMU, and a native macOS Arm64 runner. Windows Arm64 is cross-built on an x64 runner and its PE machine type +is verified without executing the binary. + +## Build and release structure + +The source tree has seven static library targets: `model`, `cli`, `trace-run`, `diagnostics`, `decode`, `output`, and +`control`. The `ctrace` executable adds only the platform trampoline and `CtraceMain`. Dependencies form a directed, +cycle-free graph with `control` as the composition root. + +The tool-specific GitHub workflow is selected by a `tools/ctrace/` release tag. It builds Windows AMD64 and +Arm64, Linux AMD64 and Arm64, and macOS Arm64 binaries. The release archive contains the Apache-2.0 project license, +third-party notices, the exact cxxopts, yaml-cpp, and OpenCSD license texts, and per-file SHA-256 checksums. A separate +checksum verifies the archive. The version compiled into the executable is derived from the same tag. + +The checked-in SWO and TB captures are approved ctrace test assets and may be redistributed with devtools. Together +with the tool-specific build, test, packaging, versioning, and license integration, this forms the technical basis for +the first open-source release. + +## Architectural constraints + +- Runtime dependencies point from orchestration toward decode, model, and output modules. +- Output modules consume semantic events and never OpenCSD packet types. +- YAML nodes do not cross the `tracerun` boundary. +- Diagnostics are structured and are not inferred from formatted stderr text inside the application. +- Output backends own their artifacts and must support cleanup after partial failure. +- Published test fixtures must be approved for redistribution and must not contain private or unstable trace + payloads. diff --git a/tools/ctrace/docs/architecture.svg b/tools/ctrace/docs/architecture.svg new file mode 100644 index 000000000..704eb925f --- /dev/null +++ b/tools/ctrace/docs/architecture.svg @@ -0,0 +1,152 @@ + + + + ctrace software architecture + + Seven library modules and the first-release ITM and DWT data flow from command-line, trace-run YAML, and SWO raw + input through orchestration, decoding, the trace event model, diagnostics, and CSV and CTF output backends. + + + + + + + + + + + + + + + + + ctrace architecture + Seven library boundaries · initial ITM/DWT release profile + + INPUTS + + + Command line + trace directory, target, filters + + + *.ctrace-run.yml + setup, routes, source metadata + + + *.SWO.raw + ITM and DWT byte stream + + + *.TB.raw + discovered; not decoded yet + + ORCHESTRATION + + + + cli + options · cxxopts + + + tracerun + YAML · yaml-cpp + + + control + read raw files; compose decode jobs + + + diagnostics + structured issues and severity + + + + + + + + + + DECODE PIPELINE + + + + DecodePipeline + RawByteView · chunk continuity + + + OpenCSD adapter + packets, errors, recovery + + + Cortex-M decode + timestamps and DWT semantics + + + model + events, type mapping, selection + + + OpenCSD 1.8.3 + pinned external library + + + + + + + + OUTPUTS + + + + output orchestration + requirements, lifecycle, cleanup + + + CSV backend + *.SWO.csv + + + CTF backend + *.ctf + Trace Compass XML + + + + + + + runtime data flow + + service or library dependency + + unsupported input path + diff --git a/tools/ctrace/docs/constraints.md b/tools/ctrace/docs/constraints.md new file mode 100644 index 000000000..3619cf916 --- /dev/null +++ b/tools/ctrace/docs/constraints.md @@ -0,0 +1,101 @@ +# Verified ctrace Constraints + +These are the current behavioral and architectural contracts of ctrace. Each constraint is implemented in the current +code and covered by focused, individually discoverable CTest cases. + +## Architecture + +- **OpenCSD details remain inside the decode layer; outputs consume semantic `TraceEvent` values.** + OpenCSD includes occur only below [`src/decode`](../src/decode); output targets depend on `model`, not OpenCSD. + +- **YAML syntax and nodes do not escape the trace-run reader boundary.** + YAML types occur only in + [`YmlTraceRunConfigReader.cpp`](../src/tracerun/YmlTraceRunConfigReader.cpp); normalized configuration and metadata + cross the boundary. + +- **The YAML reader validates consumed fields and ignores unrelated extensions.** + `TraceRunReaderConsumesOnlyRelevantFields` and `TraceRunReaderValidatesConsumedFields` cover both sides. + +- **Production dependencies retain seven explicit, cycle-free module boundaries and point toward lower-level + modules.** [`src/CMakeLists.txt`](../src/CMakeLists.txt) defines model, CLI, trace-run, diagnostics, decode, output, + and control targets. The executable adds the platform-independent `CtraceMain` entry point directly; raw-file + reading remains private to control rather than creating another target. + +- **CSV and CTF requirements are evaluated independently after shared decode configuration.** + `testOutputRequirementsAreBackendSpecific` verifies that CTF-only failures do not disable valid CSV output. + +## Input and decoding + +- **Raw SWO input is authoritative; recovery never rewrites input or injects synthetic synchronization.** + `testDecodePipelineDoesNotInjectSync`, `testDecodePipelineRecoversAtRealSync`, and + `testDecodePipelineCountsBytesUntilRealSync` exercise the rule. + +- **File-read boundaries are not packet boundaries; partial OpenCSD state survives across chunks.** + `FileDecodeJob` reads 64 KiB blocks and passes non-owning `RawByteView` values to `DecodePipeline`. + `testDecodePipelineRecoversWhenErrorSpansChunks` and the persistent `OpenCsdItmSession` cover chunked input. + +- **Incomplete input and unrecoverable decoder responses remain visible errors, not successful packets.** + `testDecodePipelineReportsIncompletePacketAtEndOfInput` and `testOpenCsdErrorControllerClassifiesResponses` verify + classification. + +- **Overflow, reset, synchronization, and recovery boundaries cannot retain stale DWT state silently.** + `testCortexMPostDecoderOverflowFlushesDwtSegments` and post-decoder reset paths flush or clear pending state. + +- **Unformatted input uses Trace Bus ID `0`; formatted IDs are restricted to `1` through `111`.** + [`TraceStreamId.hpp`](../src/model/TraceStreamId.hpp), `testTraceSelection`, and `testCliParser` enforce the domains. + +- **ITM timestamp prescalers default to `1`, accept `1`, `4`, `16`, or `64`, and remain stream-specific.** + `testTimestampPrescalerMetadataDefaults`, `testCortexMStreamDecoderAppliesPerStreamPrescalers`, and metadata tests + cover the behavior. + +## Events, diagnostics, and output + +- **The first public output profile contains exactly `itm`, `dwt`, `exception`, `global_ts`, `overflow`, and + `error`.** `TraceSelection` owns this stable name set. Event-counter and PMU events have no public output type, and + periodic PC samples remain suppressed; `testTraceSelection`, `testCliParser`, `testCsvRowMapper`, and + `testDwtPcSampleIsSuppressedUntilDedicatedEventExists` enforce the boundary. + +- **Decoder warnings and errors remain observable independently of payload filtering; errors are fatal.** + `testTraceIssueReporterReportsEveryIssue` and + `testDecodeConsumersForwardsWarningsAndFailsOnErrorsWithOutputs` verify ordering and impact. + +- **ITM channel `0` is excluded from trace artifacts, but associated decoder errors remain visible.** + `testTraceSelection` and `testCtfBundleOutputExcludesSoftwareChannelZero` cover selection and CTF metadata. + +- **CTF timestamps never regress; global timestamps do not establish local timestamp quality.** + `testCtfHoldsRegressingEventTimestamps` and `testCtfGlobalTimestampDoesNotEstablishLocalTimeQuality` verify both. + +- **Each output owns a direct lifecycle and removes incomplete artifacts after failure.** + `testCsvFileOutputWritesDirectly`, `testCtfBundleOutputAbortRemovesPartialBundle`, and lifecycle tests cover cleanup + and independent completion. + +- **Existing directories or overlapping CTF/XML targets are rejected before destructive replacement.** + `testCtfBundleOutputRejectsOverlappingTargetsBeforeDeletion` and CSV path tests cover target safety. + +- **Validation-only mode creates no output; unsupported trace channels are diagnosed and skipped.** + `TraceDirectoryJob`, `ctrace-trace-run-yaml`, and the SWO/TB fixture integration test enforce the workflow. + +- **Process success comes from structured fatal diagnostics, not formatted stderr text.** + [`DiagnosticSink`](../src/diagnostics/DiagnosticSink.hpp) tracks fatal impact; `CtraceMain` returns failure when the + fatal count is non-zero. + +## Distribution + +- **The executable version and release workflow are tool-specific.** A release tag named `tools/ctrace/` + supplies the compiled version and selects the ctrace workflow. Its matrix builds Windows AMD64/Arm64, Linux + AMD64/Arm64, and macOS Arm64 artifacts. Tests execute on Windows AMD64, Linux AMD64, Linux Arm64 through QEMU, and + native macOS Arm64; Windows Arm64 receives a PE machine-type check. + +- **Product dependencies come from devtools and carry repository-level license records.** `cxxopts`, `yaml-cpp`, and + the pinned OpenCSD submodule are listed in the top-level `LICENSE.md`. The release archive includes the project + license, [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md), the unmodified license text for each incorporated + dependency, and verified per-file checksums. Its separately published checksum covers the complete ZIP archive. + +- **The Blinky SWO and TB captures are redistributable ctrace test assets.** Their purpose and derived golden CSV are + documented in the [fixture README](../test/data/README.md); generated test output remains confined to the build + tree. + +## Maintenance rule + +When changing one of these contracts, update its focused unit test, the relevant integration scenario, this document, +and the [architecture description](architecture.md) in the same change. diff --git a/tools/ctrace/docs/todo.md b/tools/ctrace/docs/todo.md new file mode 100644 index 000000000..8d08c132d --- /dev/null +++ b/tools/ctrace/docs/todo.md @@ -0,0 +1,23 @@ +# ctrace TODO + +This list contains only work that is currently open in ctrace. Completed work and duplicated implementation details +are intentionally omitted. + +- [ ] **Input schema:** select and document the supported CMSIS-Toolbox trace revision and define the architecture, + comparator-role, grouped-source, and symbol metadata required for complete decoding. +- [ ] **Metadata model:** preserve logical references, bindings, grouped resources, processor identity, and exact route + context instead of flattening DWT source arrays. +- [ ] **DWT architecture:** complete Armv7-M comparator semantics and add Armv8-M match, compressed address/PC, + linked-pair reconstruction, recovery boundaries, and architecture-specific diagnostics. +- [ ] **Formatted trace:** add the OpenCSD frame deformatter and per-Trace-Bus-ID decoders for `*.TB.raw` input. +- [ ] **Outputs:** add multiple CTF clock classes; define and enable `pcsample`, `event`, and `pmu`; extend CSV, CTF, + Trace Compass, multicore, and Armv8-M golden coverage. +- [ ] **OpenCSD boundary:** remove the remaining dependency on OpenCSD `common/` and `interfaces/` private headers. +- [ ] **Release verification:** obtain a green ctrace GitHub Actions run for the Windows, Linux, and macOS matrix and + inspect the generated multi-platform ZIP, checksums, and included license texts. Local Debug and Release tests are + green; sanitizer and hosted platform results remain to be recorded. +- [ ] **Release provenance:** decide whether production releases require artifact signing, macOS notarization, and a + machine-readable SBOM. Until then, release artifacts are explicitly unsigned and protected by published SHA-256 + checksums plus complete third-party notices and license texts. +- [ ] **Release coverage:** add an Armv8-M end-to-end fixture when the corresponding DWT semantics are implemented; + this extends the supported profile and does not block the initial ITM/DWT-focused release. diff --git a/tools/ctrace/src/CMakeLists.txt b/tools/ctrace/src/CMakeLists.txt new file mode 100644 index 000000000..4e0855547 --- /dev/null +++ b/tools/ctrace/src/CMakeLists.txt @@ -0,0 +1,127 @@ +# Copyright (c) 2026 Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +add_library(ctrace-model STATIC + model/TraceSelection.cpp +) +add_library(ctrace::model ALIAS ctrace-model) +target_include_directories(ctrace-model PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/model" +) +ctrace_configure_target(ctrace-model) + +add_library(ctrace-cli STATIC + cli/CliParser.cpp +) +add_library(ctrace::cli ALIAS ctrace-cli) +target_compile_definitions(ctrace-cli PRIVATE + CTRACE_VERSION="${CTRACE_VERSION_FULL}" +) +target_include_directories(ctrace-cli PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/cli" +) +target_link_libraries(ctrace-cli + PUBLIC ctrace::model + PRIVATE cxxopts +) +ctrace_configure_target(ctrace-cli) + +add_library(ctrace-trace-run STATIC + tracerun/TraceRunDiscovery.cpp + tracerun/CtraceRunMeta.cpp + tracerun/YmlTraceRunConfigReader.cpp +) +add_library(ctrace::trace-run ALIAS ctrace-trace-run) +target_include_directories(ctrace-trace-run PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/tracerun" +) +target_include_directories(ctrace-trace-run SYSTEM PRIVATE + "${CMAKE_SOURCE_DIR}/external/yaml-cpp/include" +) +target_link_libraries(ctrace-trace-run + PUBLIC ctrace::model + PRIVATE yaml-cpp +) +ctrace_configure_target(ctrace-trace-run) + +add_library(ctrace-diagnostics STATIC + diagnostics/DiagnosticSink.cpp + diagnostics/TraceIssueReporter.cpp +) +add_library(ctrace::diagnostics ALIAS ctrace-diagnostics) +target_include_directories(ctrace-diagnostics PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/diagnostics" +) +target_link_libraries(ctrace-diagnostics PUBLIC + ctrace::model +) +ctrace_configure_target(ctrace-diagnostics) + +add_library(ctrace-decode STATIC + decode/CortexMPostDecoder.cpp + decode/CortexMStreamDecoder.cpp + decode/DecodePipeline.cpp + decode/DwtPacketDecoder.cpp + decode/OpenCsdErrorController.cpp + decode/OpenCsdItmDecoder.cpp + decode/OpenCsdItmSession.cpp + decode/OpenCsdPacketCollector.cpp +) +add_library(ctrace::decode ALIAS ctrace-decode) +target_include_directories(ctrace-decode PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/decode" +) +target_link_libraries(ctrace-decode PUBLIC + ctrace::model +) +target_link_libraries(ctrace-decode PRIVATE + ctrace::opencsd +) +ctrace_configure_target(ctrace-decode) + +add_library(ctrace-output STATIC + output/csv/CsvFileOutput.cpp + output/csv/CsvRowMapper.cpp + output/ctf/CtfBundleOutput.cpp + output/ctf/CtfEncoder.cpp + output/ctf/CtfExceptionLaneTracker.cpp + output/ctf/CtfMetadataWriter.cpp + output/ctf/CtfStreamWriter.cpp + output/ctf/TraceCompassXmlWriter.cpp + output/OutputRequirements.cpp + output/TraceOutputLifecycle.cpp +) +add_library(ctrace::output ALIAS ctrace-output) +target_include_directories(ctrace-output PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/output" +) +target_link_libraries(ctrace-output + PUBLIC + ctrace::diagnostics + ctrace::model + PRIVATE + ctrace::trace-run +) +ctrace_configure_target(ctrace-output) + +add_library(ctrace-control STATIC + control/DecodeConsumers.cpp + control/FileDecodeJob.cpp + control/TraceDirectoryJob.cpp +) +add_library(ctrace::control ALIAS ctrace-control) +target_include_directories(ctrace-control PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/control" +) +target_link_libraries(ctrace-control PUBLIC + ctrace::cli + ctrace::diagnostics + ctrace::model + ctrace::output + ctrace::trace-run +) +target_link_libraries(ctrace-control PRIVATE + ctrace::decode +) +ctrace_configure_target(ctrace-control) diff --git a/tools/ctrace/src/CtraceMain.cpp b/tools/ctrace/src/CtraceMain.cpp index 0a8597e67..bb94667ad 100644 --- a/tools/ctrace/src/CtraceMain.cpp +++ b/tools/ctrace/src/CtraceMain.cpp @@ -2,13 +2,62 @@ * Copyright (c) 2026 Arm Limited. All rights reserved. * * SPDX-License-Identifier: Apache-2.0 + * Generated with AI */ -#include "CtraceMain.h" +#include "CtraceMain.hpp" -#include +#include "CliOptions.hpp" +#include "CliParser.hpp" +#include "DiagnosticSink.hpp" +#include "TraceDirectoryJob.hpp" +#include "YmlTraceRunConfigReader.hpp" -int CtraceMain() { - std::printf("ctrace Executable\n"); - return 0; +#include +#include + +namespace { + +void reportFailure(DiagnosticSink& diagnostics, DiagnosticSink::Category category, const char* code, + const std::exception& error) +{ + diagnostics.report({ + DiagnosticSink::Severity::Error, + category, + code, + error.what(), + }); +} + +} // namespace + +int CtraceMain(int argc, const char* const argv[]) +{ + StderrDiagnosticSink diagnostics; + CliOptions options; + try { + if (argc <= 1) { + std::cout << CliParser::helpString(); + return 0; + } + options = CliParser::parse(argc, argv); + CliParser::validate(options); + if (options.version) { + std::cout << CliParser::versionString() << "\n"; + return 0; + } + } catch (const std::exception& error) { + reportFailure(diagnostics, DiagnosticSink::Category::Cli, "invalid-arguments", error); + return 1; + } + + try { + YmlTraceRunConfigReader configReader; + TraceDirectoryJob job(options, diagnostics, configReader); + job.run(); + return diagnostics.fatalCount() == 0U ? 0 : 1; + } catch (const std::exception& error) { + reportFailure(diagnostics, DiagnosticSink::Category::Input, "trace-directory-failed", error); + return 1; + } } diff --git a/tools/ctrace/include/CtraceMain.h b/tools/ctrace/src/CtraceMain.hpp similarity index 55% rename from tools/ctrace/include/CtraceMain.h rename to tools/ctrace/src/CtraceMain.hpp index 0b7bc7955..b1786bfa5 100644 --- a/tools/ctrace/include/CtraceMain.h +++ b/tools/ctrace/src/CtraceMain.hpp @@ -4,9 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -#ifndef CTRACE_MAIN_H -#define CTRACE_MAIN_H +#pragma once -int CtraceMain(); - -#endif // CTRACE_MAIN_H +int CtraceMain(int argc, const char* const argv[]); diff --git a/tools/ctrace/src/cli/CliOptions.hpp b/tools/ctrace/src/cli/CliOptions.hpp new file mode 100644 index 000000000..032a294bf --- /dev/null +++ b/tools/ctrace/src/cli/CliOptions.hpp @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "TraceSelection.hpp" + +#include +#include + +enum class OutputFormat { + None, + Ctf, + Csv, + All, +}; + +struct CliOptions { + std::optional traceDir; + std::optional targetName; + OutputFormat outputFormat = OutputFormat::None; + TraceSelection selection; + bool version = false; +}; diff --git a/tools/ctrace/src/cli/CliParser.cpp b/tools/ctrace/src/cli/CliParser.cpp new file mode 100644 index 000000000..586263e2a --- /dev/null +++ b/tools/ctrace/src/cli/CliParser.cpp @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CliParser.hpp" + +#include "CliOptions.hpp" +#include "TraceSelection.hpp" +#include "TraceStreamId.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +bool startsWithDash(const std::string& value) +{ + return !value.empty() && value.front() == '-'; +} + +std::uint32_t parseUnsignedInteger(const std::string& value, const std::string& option) +{ + std::uint32_t parsed = 0; + const auto result = std::from_chars(value.data(), value.data() + value.size(), parsed); + if (value.empty() || result.ec != std::errc{} || result.ptr != value.data() + value.size()) { + throw std::runtime_error(option + " must be an unsigned integer, got " + value); + } + return parsed; +} + +std::optional> consumeMultiValueOption(const std::string& arg, const std::string& option, + int& index, int argc, const char* const argv[]) +{ + if (arg != option && arg.rfind(option + "=", 0) != 0) { + return std::nullopt; + } + + std::vector values; + const auto append = [&](const std::string& value) { + if (value.empty()) { + throw std::runtime_error("Missing value for " + option); + } + if (value.find(',') != std::string::npos) { + throw std::runtime_error(option + " values must be separated by spaces"); + } + values.push_back(value); + }; + if (arg == option) { + while (index + 1 < argc && !startsWithDash(argv[index + 1])) { + append(argv[++index]); + } + } else { + append(arg.substr(option.size() + 1U)); + } + if (values.empty()) { + throw std::runtime_error("Missing value for " + option); + } + return values; +} + +std::vector normalizeArgsForCxxopts(int argc, const char* const argv[]) +{ + std::vector args; + args.reserve(static_cast(argc)); + if (argc > 0) { + args.emplace_back(argv[0]); + } + + for (int index = 1; index < argc; ++index) { + const std::string arg = argv[index]; + if (const auto types = consumeMultiValueOption(arg, "--type", index, argc, argv)) { + for (const auto& type : *types) { + args.emplace_back("--type"); + args.push_back(type); + } + continue; + } + if (const auto streams = consumeMultiValueOption(arg, "--stream", index, argc, argv)) { + for (const auto& stream : *streams) { + args.emplace_back("--stream"); + args.push_back(stream); + } + continue; + } + args.push_back(arg); + } + return args; +} + +std::vector asArgv(const std::vector& args) +{ + std::vector argv; + argv.reserve(args.size()); + for (const auto& arg : args) { + argv.push_back(arg.c_str()); + } + return argv; +} + +OutputFormat selectedOutputFormat(bool csvRequested, bool ctfRequested, bool allRequested) +{ + if (allRequested || (csvRequested && ctfRequested)) { + return OutputFormat::All; + } + if (csvRequested) { + return OutputFormat::Csv; + } + if (ctfRequested) { + return OutputFormat::Ctf; + } + return OutputFormat::None; +} + +void configureCliParser(cxxopts::Options& parser) +{ + parser.custom_help(" [options]"); + parser.allow_unrecognised_options(); + parser.add_options()("V,version", "Print version")( + "t,target", "Specify solution-set (default: process all solution sets)", cxxopts::value())( + "csv", "Generate only CSV files")("ctf", "Generate only CTF files")("a,all", "Generate CSV and CTF files")( + "type", "Filter packet types (default: all packet types)", cxxopts::value>(), + "sel [...]")("stream", "Filter streams (default: all streams)", cxxopts::value>(), + "sel [...]"); +} + +CliOptions parseCliArgs(int argc, const char* const argv[]) +{ + CliOptions options; + auto normalizedArgs = normalizeArgsForCxxopts(argc, argv); + auto cargv = asArgv(normalizedArgs); + auto mutableArgc = static_cast(cargv.size()); + + cxxopts::Options parser("ctrace", "CMSIS trace utility"); + configureCliParser(parser); + + const auto parsed = parser.parse(mutableArgc, cargv.data()); + const auto& unknown = parsed.unmatched(); + for (const auto& argument : unknown) { + if (startsWithDash(argument)) { + throw std::runtime_error("Unknown argument: " + argument); + } + if (options.traceDir.has_value()) { + throw std::runtime_error("Unexpected positional argument: " + argument); + } + options.traceDir = argument; + } + + options.version = parsed.count("version") != 0U; + if (parsed.count("target") != 0U) { + options.targetName = parsed["target"].as(); + } + if (parsed.count("type") != 0U) { + for (const auto& type : parsed["type"].as>()) { + options.selection.types.push_back(type); + } + } + if (parsed.count("stream") != 0U) { + for (const auto& stream : parsed["stream"].as>()) { + const auto streamId = parseUnsignedInteger(stream, "--stream"); + if (!CoreSight::isTraceBusId(streamId)) { + throw std::runtime_error("--stream must be a CoreSight Trace Bus ID between 0 and 111, got " + stream); + } + options.selection.streams.push_back(static_cast(streamId)); + } + } + + options.outputFormat = + selectedOutputFormat(parsed.count("csv") != 0U, parsed.count("ctf") != 0U, parsed.count("all") != 0U); + return options; +} + +void validateCliOptions(const CliOptions& options) +{ + for (const auto& type : options.selection.types) { + if (!parseTraceEventType(type).has_value()) { + throw std::runtime_error("Invalid --type value: " + type + " (accepted: " + traceEventTypeList(", ") + ")"); + } + } + if (options.version) { + return; + } + if (!options.traceDir.has_value()) { + throw std::runtime_error("Specify "); + } +} + +} // namespace + +CliOptions CliParser::parse(int argc, const char* const argv[]) +{ + return parseCliArgs(argc, argv); +} + +void CliParser::validate(const CliOptions& options) +{ + validateCliOptions(options); +} + +std::string CliParser::helpString() +{ + cxxopts::Options parser("ctrace", "CMSIS trace utility"); + configureCliParser(parser); + return parser.help(); +} + +std::string CliParser::versionString() +{ + return std::string("ctrace ") + CTRACE_VERSION; +} diff --git a/tools/ctrace/src/cli/CliParser.hpp b/tools/ctrace/src/cli/CliParser.hpp new file mode 100644 index 000000000..79eb4d903 --- /dev/null +++ b/tools/ctrace/src/cli/CliParser.hpp @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "CliOptions.hpp" + +#include + +namespace CliParser { + +CliOptions parse(int argc, const char* const argv[]); +void validate(const CliOptions& options); +std::string helpString(); +std::string versionString(); + +} // namespace CliParser diff --git a/tools/ctrace/src/control/DecodeConsumers.cpp b/tools/ctrace/src/control/DecodeConsumers.cpp new file mode 100644 index 000000000..195bad258 --- /dev/null +++ b/tools/ctrace/src/control/DecodeConsumers.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "DecodeConsumers.hpp" + +#include "DiagnosticSink.hpp" +#include "TraceEvent.hpp" +#include "TraceOutput.hpp" +#include "TraceOutputLifecycle.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::string hexMask(std::uint32_t value) +{ + std::ostringstream out; + out << "0x" << std::hex << std::setw(8) << std::setfill('0') << value; + return out.str(); +} + +} // namespace + +DecodeConsumers::DecodeConsumers(std::vector> outputs, DiagnosticSink& diagnostics, + std::optional itmEnableMask, + std::map itmEnableMasksByTraceBusId) + : diagnostics_(diagnostics), itmEnableMask_(itmEnableMask), + itmEnableMasksByTraceBusId_(std::move(itmEnableMasksByTraceBusId)), issueReporter_(diagnostics), + outputLifecycle_(std::move(outputs), diagnostics) +{ +} + +void DecodeConsumers::append(const TraceEvent& event) +{ + ++eventCount_; + outputLifecycle_.append(event); + reportItmConfigurationMismatch(event); + issueReporter_.append(event); +} + +void DecodeConsumers::reportItmConfigurationMismatch(const TraceEvent& event) +{ + const auto* software = traceEventPayload(event); + if (software == nullptr || software->channel == 0U || software->channel >= 32U) { + return; + } + + auto enableMask = itmEnableMask_; + const auto streamMask = itmEnableMasksByTraceBusId_.find(event.traceBusId); + if (streamMask != itmEnableMasksByTraceBusId_.end()) { + enableMask = streamMask->second; + } + if (!enableMask.has_value() || ((*enableMask & (1U << software->channel)) != 0U) || + !reportedDisabledItmChannels_.emplace(event.traceBusId, software->channel).second) { + return; + } + + diagnostics_.report({ + DiagnosticSink::Severity::Warning, + DiagnosticSink::Category::Input, + "itm-channel-not-enabled", + "ITM data was received on a channel not enabled by ctrace-setup.itm.enable", + { + {"stream", std::to_string(event.traceBusId)}, + {"channel", std::to_string(software->channel)}, + {"enable", hexMask(*enableMask)}, + }, + }); +} + +std::uint64_t DecodeConsumers::eventCount() const +{ + return eventCount_; +} + +void DecodeConsumers::finishIssues() +{ + issueReporter_.finish(); +} + +void DecodeConsumers::finishOutputs() noexcept +{ + outputLifecycle_.finish(); +} + +void DecodeConsumers::abortOutputs() noexcept +{ + outputLifecycle_.abort(); +} diff --git a/tools/ctrace/src/control/DecodeConsumers.hpp b/tools/ctrace/src/control/DecodeConsumers.hpp new file mode 100644 index 000000000..016cc4a55 --- /dev/null +++ b/tools/ctrace/src/control/DecodeConsumers.hpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "DiagnosticSink.hpp" +#include "TraceEvent.hpp" +#include "TraceIssueReporter.hpp" +#include "TraceOutput.hpp" +#include "TraceOutputLifecycle.hpp" + +#include +#include +#include +#include +#include +#include +#include + +class DecodeConsumers final : public TraceEventSink { +public: + DecodeConsumers(std::vector> outputs, DiagnosticSink& diagnostics, + std::optional itmEnableMask = std::nullopt, + std::map itmEnableMasksByTraceBusId = {}); + + void append(const TraceEvent& event) override; + std::uint64_t eventCount() const; + void finishIssues(); + void finishOutputs() noexcept; + void abortOutputs() noexcept; + +private: + void reportItmConfigurationMismatch(const TraceEvent& event); + + DiagnosticSink& diagnostics_; + std::optional itmEnableMask_; + std::map itmEnableMasksByTraceBusId_; + std::set> reportedDisabledItmChannels_; + TraceIssueReporter issueReporter_; + TraceOutputLifecycle outputLifecycle_; + std::uint64_t eventCount_ = 0; +}; diff --git a/tools/ctrace/src/control/FileDecodeJob.cpp b/tools/ctrace/src/control/FileDecodeJob.cpp new file mode 100644 index 000000000..477a775a7 --- /dev/null +++ b/tools/ctrace/src/control/FileDecodeJob.cpp @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "FileDecodeJob.hpp" + +#include "CliOptions.hpp" +#include "csv/CsvFileOutput.hpp" +#include "ctf/CtfBundleOutput.hpp" +#include "CortexMStreamDecoder.hpp" +#include "DecodeConsumers.hpp" +#include "DecodePipeline.hpp" +#include "DiagnosticSink.hpp" +#include "OpenCsdItmDecoder.hpp" +#include "OutputRequirements.hpp" +#include "TraceOutput.hpp" +#include "TraceOutputConfig.hpp" +#include "TraceRunConfig.hpp" +#include "CtraceRunMeta.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +class RawFileReader final { +public: + struct ReadResult { + RawByteView bytes; + bool eof = false; + }; + + explicit RawFileReader(std::filesystem::path path) + : path_(std::move(path)), stream_(path_, std::ios::binary), buffer_(64U * 1024U) + { + if (!stream_) { + throw std::runtime_error("failed to open input file: " + path_.string()); + } + } + + ReadResult read() + { + if (eof_ || !stream_.is_open()) { + return {{}, true}; + } + + stream_.read(reinterpret_cast(buffer_.data()), static_cast(buffer_.size())); + const auto readBytes = stream_.gcount(); + if (readBytes > 0) { + if (stream_.eof()) { + eof_ = true; + stream_.close(); + } + return {{buffer_.data(), static_cast(readBytes)}, false}; + } + if (stream_.bad()) { + throw std::runtime_error("failed to read input file: " + path_.string()); + } + + eof_ = true; + stream_.close(); + return {{}, true}; + } + +private: + std::filesystem::path path_; + std::ifstream stream_; + std::vector buffer_; + bool eof_ = false; +}; + +std::string decodeSummary(const DecodeResult& decode, std::chrono::steady_clock::duration elapsed) +{ + const auto seconds = std::chrono::duration(elapsed).count(); + const auto mebibytes = static_cast(decode.bytesIn) / (1024.0 * 1024.0); + const auto mebibytesPerSecond = seconds > 0.0 ? mebibytes / seconds : 0.0; + + std::ostringstream out; + out << "decoded " << decode.packetsOut << " packets from " << decode.bytesIn << " bytes in " << std::fixed + << std::setprecision(3) << seconds << " s (" << std::setprecision(2) << mebibytesPerSecond << " MiB/s)"; + return out.str(); +} + +ItmTimestampPrescalers timestampPrescalers(const CtraceRunMeta& ctraceRunMeta) +{ + auto fallback = ctraceRunMeta.timestampPrescaler(); + if (!fallback.has_value() && !ctraceRunMeta.hasDistinctProcessorPrescalers()) { + fallback = TraceRunSchema::kDefaultTimestampPrescaler; + } + if (fallback.has_value() && !TraceRunSchema::isTimestampPrescaler(*fallback)) { + throw std::runtime_error("trace-run config contains invalid timestamps.itm-prescaler; " + "expected one of 1, 4, 16, or 64"); + } + for (const auto& [traceBusId, prescaler] : ctraceRunMeta.timestampPrescalersByTraceBusId()) { + (void)traceBusId; + if (!TraceRunSchema::isTimestampPrescaler(prescaler)) { + throw std::runtime_error("trace-run config contains invalid timestamps.itm-prescaler; " + "expected one of 1, 4, 16, or 64"); + } + } + return {fallback, ctraceRunMeta.timestampPrescalersByTraceBusId()}; +} + +TraceOutputRequest outputRequest(const CliOptions& options) +{ + return { + options.outputFormat == OutputFormat::Csv || options.outputFormat == OutputFormat::All, + options.outputFormat == OutputFormat::Ctf || options.outputFormat == OutputFormat::All, + options.selection, + }; +} + +std::vector> createConfiguredOutputs(const TraceOutputPlan& outputPlan, + DiagnosticSink& diagnostics) +{ + std::vector> outputs; + if (outputPlan.ctf.has_value()) { + outputs.push_back(std::make_unique(*outputPlan.ctf, &diagnostics)); + diagnostics.report({ + DiagnosticSink::Severity::Info, + DiagnosticSink::Category::Output, + "trace-compass-xml", + "configured Trace Compass XML", + {{"path", outputPlan.ctf->traceCompassXmlPath.string()}}, + }); + } + if (outputPlan.csv.has_value()) { + outputs.push_back(std::make_unique(outputPlan.csv->outputPath, outputPlan.csv->selection)); + } + return outputs; +} + +} // namespace + +FileDecodeJob::FileDecodeJob(CliOptions options, std::filesystem::path rawInputPath, DiagnosticSink& diagnostics, + CtraceRunMeta ctraceRunMeta) + : options_(std::move(options)), rawInputPath_(std::move(rawInputPath)), diagnostics_(diagnostics), + ctraceRunMeta_(std::move(ctraceRunMeta)) +{ +} + +void FileDecodeJob::run() +{ + const auto prescalers = timestampPrescalers(ctraceRunMeta_); + auto outputPlan = planTraceOutputs(outputRequest(options_), rawInputPath_, ctraceRunMeta_, diagnostics_); + if (outputPlan.hasRequestedOutputs() && !outputPlan.hasEnabledOutputs()) { + return; + } + diagnostics_.report({ + DiagnosticSink::Severity::Info, + DiagnosticSink::Category::Input, + "ctrace-run-meta", + "applied ctrace-run meta", + { + {"path", ctraceRunMeta_.configPath()}, + {"processors", std::to_string(ctraceRunMeta_.processorCount())}, + {"sources", std::to_string(ctraceRunMeta_.sources().size())}, + }, + }); + auto outputs = createConfiguredOutputs(outputPlan, diagnostics_); + DecodeConsumers consumers(std::move(outputs), diagnostics_, ctraceRunMeta_.itmEnableMask(), + ctraceRunMeta_.itmEnableMasksByTraceBusId()); + + if (prescalers.fallback.has_value()) { + diagnostics_.report({ + DiagnosticSink::Severity::Info, + DiagnosticSink::Category::Input, + "timestamp-prescaler", + "using timestamp prescaler", + {{"value", std::to_string(*prescalers.fallback)}}, + }); + } else { + diagnostics_.report({ + DiagnosticSink::Severity::Info, + DiagnosticSink::Category::Input, + "timestamp-prescaler", + "using Trace-Bus-ID-specific timestamp prescalers", + {{"traceBusIds", std::to_string(prescalers.byTraceBusId.size())}}, + }); + } + const auto decodeStart = std::chrono::steady_clock::now(); + DecodeResult decode; + bool decoderFatal = false; + try { + RawFileReader input(rawInputPath_); + DecodePipeline pipeline(prescalers, consumers); + while (true) { + const auto read = input.read(); + if (read.eof) { + break; + } + pipeline.push(read.bytes); + } + decode = pipeline.finish(); + } catch (const OpenCsdFatalError& error) { + decoderFatal = true; + decode.bytesIn = error.bytesProcessed(); + decode.packetsOut = consumers.eventCount(); + } + consumers.finishIssues(); + const auto decodeEnd = std::chrono::steady_clock::now(); + diagnostics_.report({ + DiagnosticSink::Severity::Info, + DiagnosticSink::Category::Decode, + "summary", + decodeSummary(decode, decodeEnd - decodeStart), + }); + if (decoderFatal) { + consumers.abortOutputs(); + } else { + consumers.finishOutputs(); + } +} diff --git a/tools/ctrace/src/control/FileDecodeJob.hpp b/tools/ctrace/src/control/FileDecodeJob.hpp new file mode 100644 index 000000000..93f6918f3 --- /dev/null +++ b/tools/ctrace/src/control/FileDecodeJob.hpp @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "CliOptions.hpp" +#include "DiagnosticSink.hpp" +#include "CtraceRunMeta.hpp" + +#include + +class FileDecodeJob { +public: + FileDecodeJob(CliOptions options, std::filesystem::path rawInputPath, DiagnosticSink& diagnostics, + CtraceRunMeta ctraceRunMeta); + + void run(); + +private: + CliOptions options_; + std::filesystem::path rawInputPath_; + DiagnosticSink& diagnostics_; + CtraceRunMeta ctraceRunMeta_; +}; diff --git a/tools/ctrace/src/control/TraceDirectoryJob.cpp b/tools/ctrace/src/control/TraceDirectoryJob.cpp new file mode 100644 index 000000000..71b7fe3c2 --- /dev/null +++ b/tools/ctrace/src/control/TraceDirectoryJob.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TraceDirectoryJob.hpp" + +#include "CliOptions.hpp" +#include "DiagnosticSink.hpp" +#include "FileDecodeJob.hpp" +#include "TraceRunConfig.hpp" +#include "TraceRunConfigReader.hpp" +#include "TraceRunDiscovery.hpp" +#include "CtraceRunMeta.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::vector> referenceContext(const TraceRunConfig& config, + const TraceRunReference& reference) +{ + std::vector> context{ + {"config", config.path}, + {"ctraceRef", reference.ctraceRef}, + {"type", reference.type}, + }; + if (reference.processorName.has_value()) { + context.emplace_back("pname", *reference.processorName); + } + if (reference.stream.has_value()) { + context.emplace_back("stream", std::to_string(*reference.stream)); + } + return context; +} + +void reportTraceRunDiagnostics(const TraceRunConfig& config, DiagnosticSink& diagnostics) +{ + for (const auto& reference : config.references) { + if (TraceRunSchema::isItmChannelZero(reference)) { + continue; + } + const auto report = [&](DiagnosticSink::Severity severity, const std::string& code, + const std::optional& message) { + if (!message.has_value() || message->empty()) { + return; + } + diagnostics.report({ + severity, + DiagnosticSink::Category::Input, + code, + *message, + referenceContext(config, reference), + }); + }; + report(DiagnosticSink::Severity::Info, "trace-run-generation-info", reference.info); + report(DiagnosticSink::Severity::Warning, "trace-run-generation-warning", reference.warning); + if (reference.error.has_value()) { + auto context = referenceContext(config, reference); + diagnostics.report({ + DiagnosticSink::Severity::Error, + DiagnosticSink::Category::Input, + "trace-run-generation-error", + reference.error->empty() ? "trace generation setup failed without a diagnostic message" : *reference.error, + std::move(context), + std::nullopt, + DiagnosticSink::Impact::NonFatal, + }); + } + } +} + +} // namespace + +TraceDirectoryJob::TraceDirectoryJob(CliOptions options, DiagnosticSink& diagnostics, + const TraceRunConfigReader& configReader) + : options_(std::move(options)), diagnostics_(diagnostics), configReader_(configReader) +{ +} + +void TraceDirectoryJob::run() +{ + std::vector configFiles; + if (options_.traceDir.has_value()) { + configFiles = TraceRunDiscovery::selectConfigFiles(*options_.traceDir, options_.targetName); + } else { + throw std::runtime_error("trace directory job requires "); + } + + for (const auto& configFile : configFiles) { + const auto solutionSet = TraceRunDiscovery::solutionSetName(configFile); + try { + const auto config = configReader_.read(configFile.string()); + diagnostics_.report({ + DiagnosticSink::Severity::Info, + DiagnosticSink::Category::Input, + "trace-run-config", + "selected trace-run configuration", + { + {"solutionSet", solutionSet}, + {"path", config.path}, + {"references", std::to_string(config.references.size())}, + {"setups", std::to_string(config.setups.size())}, + }, + }); + reportTraceRunDiagnostics(config, diagnostics_); + const auto ctraceRunMeta = CtraceRunMeta::fromConfig(config); + const auto rawInputs = TraceRunDiscovery::rawInputs(configFile); + bool processedSolutionSet = false; + for (const auto& rawInput : rawInputs) { + if (rawInput.channel != "SWO") { + diagnostics_.report({ + DiagnosticSink::Severity::Warning, + DiagnosticSink::Category::Input, + "unsupported-trace-channel", + "skipping raw trace channel that is not implemented yet", + { + {"solutionSet", solutionSet}, + {"channel", rawInput.channel}, + {"path", rawInput.path.string()}, + }, + }); + continue; + } + + FileDecodeJob fileJob(options_, rawInput.path, diagnostics_, ctraceRunMeta); + fileJob.run(); + processedSolutionSet = true; + } + if (!processedSolutionSet) { + diagnostics_.report({ + DiagnosticSink::Severity::Error, + DiagnosticSink::Category::Input, + "missing-swo-raw-input", + "no supported .SWO.raw input found", + { + {"solutionSet", solutionSet}, + {"traceDir", configFile.parent_path().string()}, + }, + }); + } + } catch (const std::exception& error) { + diagnostics_.report({ + DiagnosticSink::Severity::Error, + DiagnosticSink::Category::Input, + "solution-set-failed", + error.what(), + { + {"solutionSet", solutionSet}, + {"config", configFile.string()}, + }, + }); + } + } +} diff --git a/tools/ctrace/src/control/TraceDirectoryJob.hpp b/tools/ctrace/src/control/TraceDirectoryJob.hpp new file mode 100644 index 000000000..71f70975d --- /dev/null +++ b/tools/ctrace/src/control/TraceDirectoryJob.hpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "CliOptions.hpp" +#include "DiagnosticSink.hpp" +#include "TraceRunConfigReader.hpp" + +class TraceDirectoryJob { +public: + TraceDirectoryJob(CliOptions options, DiagnosticSink& diagnostics, const TraceRunConfigReader& configReader); + + void run(); + +private: + CliOptions options_; + DiagnosticSink& diagnostics_; + const TraceRunConfigReader& configReader_; +}; diff --git a/tools/ctrace/src/decode/CortexMPostDecoder.cpp b/tools/ctrace/src/decode/CortexMPostDecoder.cpp new file mode 100644 index 000000000..1a206f9ee --- /dev/null +++ b/tools/ctrace/src/decode/CortexMPostDecoder.cpp @@ -0,0 +1,342 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CortexMPostDecoder.hpp" + +#include "DwtPacketDecoder.hpp" +#include "TraceEvent.hpp" +#include "OpenCsdTraceElement.hpp" + +#include +#include +#include +#include +#include +#include +#include + +CortexMPostDecoder::CortexMPostDecoder(TraceEventSink& eventSink) : eventSink_(eventSink) {} + +void CortexMPostDecoder::append(OpenCsdTraceElement element) +{ + mapTimestampSegment(element); + switch (element.kind) { + case OpenCsdTraceElement::Kind::Software: + appendSoftware(element); + break; + case OpenCsdTraceElement::Kind::Hardware: + appendDwt(element); + break; + case OpenCsdTraceElement::Kind::LocalTimestamp: + appendTimestamp(element); + break; + case OpenCsdTraceElement::Kind::GlobalTimestamp: + appendGlobalTimestamp(element); + break; + case OpenCsdTraceElement::Kind::Sync: + appendSync(element); + break; + case OpenCsdTraceElement::Kind::Overflow: + appendOverflow(element); + break; + case OpenCsdTraceElement::Kind::Discontinuity: + appendDiscontinuity(element); + break; + case OpenCsdTraceElement::Kind::Error: + appendError(element); + break; + } +} + +void CortexMPostDecoder::mapTimestampSegment(OpenCsdTraceElement& element) +{ + if (element.overflow) { + startNewTimestampSegment(); + } + + switch (element.kind) { + case OpenCsdTraceElement::Kind::LocalTimestamp: + if (element.tcyc.has_value()) { + const auto mapped = saturatingAdd(timestampSegmentBase_, *element.tcyc); + element.tcyc = mapped; + mappedTimeline_ = mapped; + timelineKnown_ = true; + } + break; + case OpenCsdTraceElement::Kind::Overflow: + case OpenCsdTraceElement::Kind::Discontinuity: + startNewTimestampSegment(); + break; + case OpenCsdTraceElement::Kind::Error: + if (element.discontinuity) { + startNewTimestampSegment(); + } + break; + case OpenCsdTraceElement::Kind::Software: + case OpenCsdTraceElement::Kind::Hardware: + case OpenCsdTraceElement::Kind::GlobalTimestamp: + case OpenCsdTraceElement::Kind::Sync: + break; + } +} + +void CortexMPostDecoder::startNewTimestampSegment() +{ + timestampSegmentBase_ = timelineKnown_ ? mappedTimeline_ : 0U; +} + +std::uint64_t CortexMPostDecoder::saturatingAdd(std::uint64_t lhs, std::uint64_t rhs) +{ + const auto max = std::numeric_limits::max(); + return rhs > max - lhs ? max : lhs + rhs; +} + +void CortexMPostDecoder::finish() +{ + finalizePendingDiscontinuityIssues(std::nullopt); + flushPendingDataTrace(currentTraceStatus()); + flushPendingEvents(std::nullopt, currentTraceStatus()); +} + +std::uint64_t CortexMPostDecoder::eventCount() const +{ + return eventCount_; +} + +void CortexMPostDecoder::appendSync(const OpenCsdTraceElement& element) +{ + TraceEvent event{SyncTraceEvent{}}; + event.index = element.sourceIndex; + event.traceBusId = element.traceBusId; + queueOrEmitWhileAwaitingTimestamp(std::move(event)); +} + +void CortexMPostDecoder::appendOverflow(const OpenCsdTraceElement& element) +{ + noteOverflow(); + const auto status = currentTraceStatus(); + finalizePendingDiscontinuityIssues(std::nullopt); + flushPendingDataTrace(status); + flushPendingEvents(std::nullopt, status); + dwtDecoder_.reset(); + + TraceEvent event{OverflowTraceEvent{ + "overflow: new timestamp segment; time across boundary may be unreliable", + }}; + event.index = element.sourceIndex; + event.traceBusId = element.traceBusId; + event.tcyc = currentTcyc_; + event.quality = TraceQuality{true, false, overflowCount_}; + emitEvent(event); +} + +void CortexMPostDecoder::appendGlobalTimestamp(const OpenCsdTraceElement& element) +{ + flushPendingDataTrace(currentTraceStatus()); + TraceEvent event{GlobalTimestampTraceEvent{ + element.timestampValue, + element.clockChange, + }}; + event.index = element.sourceIndex; + event.traceBusId = element.traceBusId; + pendingEvents_.push_back(std::move(event)); +} + +void CortexMPostDecoder::appendDiscontinuity(const OpenCsdTraceElement& element) +{ + const auto status = markDiscontinuity(); + + queueDiscontinuityIssue( + element.sourceIndex, element.traceBusId, status, element.issueCode.empty() ? "data-loss" : element.issueCode, + element.errorMessage.empty() ? "data loss/resync boundary; timestamps across this point may not match" + : element.errorMessage, + element.rawBytesConsumed); +} + +void CortexMPostDecoder::appendError(const OpenCsdTraceElement& element) +{ + const auto status = element.discontinuity ? markDiscontinuity() : currentTraceStatus(); + + TraceEvent event{TraceIssueEvent{ + element.issueCode.empty() ? "opencsd-decode-error" : element.issueCode, + element.issueSeverity, + element.errorMessage, + element.rawBytesConsumed, + std::nullopt, + }}; + event.index = element.sourceIndex; + event.traceBusId = element.traceBusId; + event.tcyc = currentTcyc_; + applyDwtTraceStatus(event, status); + if (element.awaitingResumeTimestamp) { + std::get(event.payload).lastValidTcyc = currentTcyc_; + pendingEvents_.push_back(std::move(event)); + return; + } + queueOrEmitWhileAwaitingTimestamp(std::move(event)); +} + +void CortexMPostDecoder::appendSoftware(const OpenCsdTraceElement& element) +{ + flushPendingDataTrace(currentTraceStatus()); + TraceEvent event{SoftwareTraceEvent{ + element.channel, + element.size, + element.value, + }}; + event.index = element.sourceIndex; + event.traceBusId = element.traceBusId; + event.tcyc = currentTcyc_; + applyDwtTraceStatus(event, currentTraceStatus(element.overflow)); + pendingEvents_.push_back(std::move(event)); +} + +void CortexMPostDecoder::appendDwt(const OpenCsdTraceElement& element) +{ + auto events = dwtDecoder_.decode({ + element.sourceIndex, + element.traceBusId, + static_cast(element.discriminator), + element.size, + element.value, + currentTcyc_, + currentTraceStatus(element.overflow), + }); + appendPendingEvents(std::move(events)); +} + +void CortexMPostDecoder::appendTimestamp(const OpenCsdTraceElement& element) +{ + currentTcyc_ = element.tcyc.value_or(0); + timestampReliable_ = true; + + const auto status = statusResolvedByTimestamp(element.timestampRelation); + finalizePendingDiscontinuityIssues(currentTcyc_); + flushPendingDataTrace(status); + flushPendingEvents(currentTcyc_, status); + + TraceEvent event{LocalTimestampTraceEvent{}}; + event.index = element.sourceIndex; + event.traceBusId = element.traceBusId; + event.tcyc = currentTcyc_; + emitEvent(event); + + timestampReliable_ = true; + dataLossSinceLastTimestamp_ = false; +} + +void CortexMPostDecoder::flushPendingDataTrace(const DwtTraceStatus& status) +{ + appendPendingEvents(dwtDecoder_.flush(status, currentTcyc_)); +} + +void CortexMPostDecoder::flushPendingEvents(std::optional tcyc, const DwtTraceStatus& status) +{ + for (auto& event : pendingEvents_) { + const auto* issue = traceEventPayload(event); + const auto hasLastValidTcyc = issue != nullptr && issue->lastValidTcyc.has_value(); + const auto isControlEvent = isTraceEvent(event) || isTraceEvent(event); + if (tcyc.has_value() && !isControlEvent && !hasLastValidTcyc) { + event.tcyc = tcyc; + } + if (!isControlEvent && !hasLastValidTcyc) { + applyDwtTraceStatus(event, status); + } + emitEvent(event); + } + pendingEvents_.clear(); +} + +void CortexMPostDecoder::appendPendingEvents(std::vector events) +{ + pendingEvents_.insert(pendingEvents_.end(), std::make_move_iterator(events.begin()), + std::make_move_iterator(events.end())); +} + +void CortexMPostDecoder::queueDiscontinuityIssue(std::uint64_t sourceIndex, std::uint8_t traceBusId, + const DwtTraceStatus& status, const std::string& issueCode, + const std::string& message, + std::optional rawBytesConsumed) +{ + TraceEvent event{TraceIssueEvent{ + issueCode, + TraceIssueSeverity::Error, + message, + rawBytesConsumed, + currentTcyc_, + }}; + event.index = sourceIndex; + event.traceBusId = traceBusId; + event.tcyc = currentTcyc_; + applyDwtTraceStatus(event, status); + pendingEvents_.push_back(std::move(event)); +} + +void CortexMPostDecoder::finalizePendingDiscontinuityIssues(std::optional firstResumedTcyc) +{ + for (auto& event : pendingEvents_) { + auto* issue = traceEventPayload(event); + if (issue == nullptr || !issue->lastValidTcyc.has_value()) { + continue; + } + issue->message += "; timestamp " + std::to_string(*issue->lastValidTcyc) + " .. " + + (firstResumedTcyc.has_value() ? std::to_string(*firstResumedTcyc) : "unknown") + "."; + } +} + +void CortexMPostDecoder::queueOrEmitWhileAwaitingTimestamp(TraceEvent event) +{ + if (!pendingEvents_.empty()) { + pendingEvents_.push_back(std::move(event)); + return; + } + emitEvent(event); +} + +DwtTraceStatus CortexMPostDecoder::markDiscontinuity() +{ + noteOverflow(); + const auto status = currentTraceStatus(); + finalizePendingDiscontinuityIssues(std::nullopt); + flushPendingDataTrace(status); + flushPendingEvents(std::nullopt, status); + dwtDecoder_.reset(); + pendingEvents_.clear(); + timestampReliable_ = false; + dataLossSinceLastTimestamp_ = true; + return status; +} + +void CortexMPostDecoder::emitEvent(const TraceEvent& event) +{ + eventSink_.append(event); + ++eventCount_; +} + +DwtTraceStatus CortexMPostDecoder::currentTraceStatus(bool packetOverflow) const +{ + return { + dataLossSinceLastTimestamp_ || !timestampReliable_ || packetOverflow, + timestampReliable_ && !dataLossSinceLastTimestamp_ && !packetOverflow, + overflowCount_, + }; +} + +DwtTraceStatus CortexMPostDecoder::statusResolvedByTimestamp(LocalTimestampRelation relation) const +{ + const auto dataIntact = !dataLossSinceLastTimestamp_; + const auto payloadDelayed = relation == LocalTimestampRelation::PayloadDelayed || + relation == LocalTimestampRelation::TimestampAndPayloadDelayed; + return {!dataIntact, dataIntact && !payloadDelayed, overflowCount_}; +} + +void CortexMPostDecoder::noteOverflow() +{ + ++overflowCount_; + dataLossSinceLastTimestamp_ = true; + timestampReliable_ = false; +} diff --git a/tools/ctrace/src/decode/CortexMPostDecoder.hpp b/tools/ctrace/src/decode/CortexMPostDecoder.hpp new file mode 100644 index 000000000..2b6b9746e --- /dev/null +++ b/tools/ctrace/src/decode/CortexMPostDecoder.hpp @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "OpenCsdTraceElement.hpp" +#include "DwtPacketDecoder.hpp" +#include "TraceEvent.hpp" + +#include +#include +#include +#include + +class CortexMPostDecoder final : public OpenCsdTraceElementSink { +public: + explicit CortexMPostDecoder(TraceEventSink& eventSink); + + void append(OpenCsdTraceElement element) override; + void finish(); + + std::uint64_t eventCount() const; + +private: + void appendSync(const OpenCsdTraceElement& element); + void appendOverflow(const OpenCsdTraceElement& element); + void appendGlobalTimestamp(const OpenCsdTraceElement& element); + void appendDiscontinuity(const OpenCsdTraceElement& element); + void appendError(const OpenCsdTraceElement& element); + void appendSoftware(const OpenCsdTraceElement& element); + void appendDwt(const OpenCsdTraceElement& element); + void appendTimestamp(const OpenCsdTraceElement& element); + + void queueDiscontinuityIssue(std::uint64_t sourceIndex, std::uint8_t traceBusId, const DwtTraceStatus& status, + const std::string& issueCode, const std::string& message, + std::optional rawBytesConsumed = std::nullopt); + void finalizePendingDiscontinuityIssues(std::optional firstResumedTcyc); + void queueOrEmitWhileAwaitingTimestamp(TraceEvent event); + DwtTraceStatus markDiscontinuity(); + void flushPendingDataTrace(const DwtTraceStatus& status); + void flushPendingEvents(std::optional tcyc, const DwtTraceStatus& status); + void appendPendingEvents(std::vector events); + void emitEvent(const TraceEvent& event); + void mapTimestampSegment(OpenCsdTraceElement& element); + void startNewTimestampSegment(); + static std::uint64_t saturatingAdd(std::uint64_t lhs, std::uint64_t rhs); + + DwtTraceStatus currentTraceStatus(bool packetOverflow = false) const; + DwtTraceStatus statusResolvedByTimestamp(LocalTimestampRelation relation) const; + void noteOverflow(); + + TraceEventSink& eventSink_; + std::uint64_t eventCount_ = 0; + std::vector pendingEvents_; + DwtPacketDecoder dwtDecoder_; + bool timestampReliable_ = false; + bool dataLossSinceLastTimestamp_ = false; + std::uint64_t overflowCount_ = 0; + std::uint64_t currentTcyc_ = 0; + bool timelineKnown_ = false; + std::uint64_t mappedTimeline_ = 0; + std::uint64_t timestampSegmentBase_ = 0; +}; diff --git a/tools/ctrace/src/decode/CortexMStreamDecoder.cpp b/tools/ctrace/src/decode/CortexMStreamDecoder.cpp new file mode 100644 index 000000000..bcbae1d9a --- /dev/null +++ b/tools/ctrace/src/decode/CortexMStreamDecoder.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CortexMStreamDecoder.hpp" + +#include "CortexMPostDecoder.hpp" +#include "OpenCsdTraceElement.hpp" +#include "TraceEvent.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +std::uint64_t saturatingMultiply(std::uint64_t value, std::uint32_t factor) +{ + if (factor == 0U) { + throw std::invalid_argument("ITM timestamp prescaler must not be zero"); + } + const auto maximum = std::numeric_limits::max(); + return value > maximum / factor ? maximum : value * factor; +} + +} // namespace + +CortexMStreamDecoder::CortexMStreamDecoder(ItmTimestampPrescalers prescalers, TraceEventSink& eventSink) + : prescalers_(std::move(prescalers)), eventSink_(eventSink) +{ +} + +CortexMStreamDecoder::~CortexMStreamDecoder() = default; + +void CortexMStreamDecoder::append(OpenCsdTraceElement element) +{ + if (element.kind == OpenCsdTraceElement::Kind::LocalTimestamp && element.tcyc.has_value()) { + element.tcyc = saturatingMultiply(*element.tcyc, prescaler(element.traceBusId)); + } + decoder(element.traceBusId).append(std::move(element)); +} + +void CortexMStreamDecoder::finish() +{ + for (auto& [stream, decoder] : decoders_) { + (void)stream; + decoder->finish(); + } +} + +std::uint64_t CortexMStreamDecoder::eventCount() const +{ + std::uint64_t count = 0U; + for (const auto& [stream, decoder] : decoders_) { + (void)stream; + count += decoder->eventCount(); + } + return count; +} + +std::uint32_t CortexMStreamDecoder::prescaler(std::uint8_t traceBusId) const +{ + const auto found = prescalers_.byTraceBusId.find(traceBusId); + if (found != prescalers_.byTraceBusId.end()) { + return found->second; + } + if (prescalers_.fallback.has_value()) { + return *prescalers_.fallback; + } + if (traceBusId == 0U) { + throw std::runtime_error("ITM timestamps with Trace Bus ID 0 cannot be assigned to " + "different processor prescalers; a formatted source ID is required"); + } + throw std::runtime_error("CoreSight Trace Bus ID " + std::to_string(traceBusId) + + " has no unambiguous processor timestamp prescaler"); +} + +CortexMPostDecoder& CortexMStreamDecoder::decoder(std::uint8_t traceBusId) +{ + auto& result = decoders_[traceBusId]; + if (!result) { + result = std::make_unique(eventSink_); + } + return *result; +} diff --git a/tools/ctrace/src/decode/CortexMStreamDecoder.hpp b/tools/ctrace/src/decode/CortexMStreamDecoder.hpp new file mode 100644 index 000000000..b5fc8b899 --- /dev/null +++ b/tools/ctrace/src/decode/CortexMStreamDecoder.hpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "OpenCsdTraceElement.hpp" +#include "TraceEvent.hpp" + +#include +#include +#include +#include + +class CortexMPostDecoder; + +struct ItmTimestampPrescalers { + std::optional fallback; + std::map byTraceBusId; +}; + +class CortexMStreamDecoder final : public OpenCsdTraceElementSink { +public: + CortexMStreamDecoder(ItmTimestampPrescalers prescalers, TraceEventSink& eventSink); + ~CortexMStreamDecoder(); + + CortexMStreamDecoder(const CortexMStreamDecoder&) = delete; + CortexMStreamDecoder& operator=(const CortexMStreamDecoder&) = delete; + + void append(OpenCsdTraceElement element) override; + void finish(); + std::uint64_t eventCount() const; + +private: + std::uint32_t prescaler(std::uint8_t traceBusId) const; + CortexMPostDecoder& decoder(std::uint8_t traceBusId); + + ItmTimestampPrescalers prescalers_; + TraceEventSink& eventSink_; + std::map> decoders_; +}; diff --git a/tools/ctrace/src/decode/DecodePipeline.cpp b/tools/ctrace/src/decode/DecodePipeline.cpp new file mode 100644 index 000000000..53b2dacdb --- /dev/null +++ b/tools/ctrace/src/decode/DecodePipeline.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "DecodePipeline.hpp" + +#include "CortexMStreamDecoder.hpp" +#include "TraceEvent.hpp" + +#include +#include +#include +#include + +DecodePipeline::DecodePipeline(std::uint32_t timestampPrescaler, TraceEventSink& eventSink) + : DecodePipeline(ItmTimestampPrescalers{timestampPrescaler, {}}, eventSink) +{ +} + +DecodePipeline::DecodePipeline(ItmTimestampPrescalers timestampPrescalers, TraceEventSink& eventSink) + : streamDecoder_(std::move(timestampPrescalers), eventSink), decoder_(streamDecoder_) +{ +} + +void DecodePipeline::push(RawByteView bytes) +{ + if (bytes.empty()) { + return; + } + if (bytes.size > std::numeric_limits::max()) { + throw std::runtime_error("raw decode chunk is too large"); + } + decoder_.push(bytes.data, static_cast(bytes.size)); +} + +DecodeResult DecodePipeline::finish() +{ + const auto result = decoder_.finish(); + streamDecoder_.finish(); + return { + result.bytesIn, + streamDecoder_.eventCount(), + }; +} diff --git a/tools/ctrace/src/decode/DecodePipeline.hpp b/tools/ctrace/src/decode/DecodePipeline.hpp new file mode 100644 index 000000000..134f9e61b --- /dev/null +++ b/tools/ctrace/src/decode/DecodePipeline.hpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "CortexMStreamDecoder.hpp" +#include "TraceEvent.hpp" +#include "OpenCsdItmDecoder.hpp" + +#include +#include + +struct RawByteView { + const std::uint8_t* data = nullptr; + std::size_t size = 0; + + constexpr bool empty() const + { + return size == 0U; + } +}; + +struct DecodeResult { + std::uint64_t bytesIn = 0; + std::uint64_t packetsOut = 0; +}; + +class DecodePipeline final { +public: + DecodePipeline(std::uint32_t timestampPrescaler, TraceEventSink& eventSink); + DecodePipeline(ItmTimestampPrescalers timestampPrescalers, TraceEventSink& eventSink); + + void push(RawByteView bytes); + DecodeResult finish(); + +private: + CortexMStreamDecoder streamDecoder_; + OpenCsdItmDecoder decoder_; +}; diff --git a/tools/ctrace/src/decode/DwtPacketDecoder.cpp b/tools/ctrace/src/decode/DwtPacketDecoder.cpp new file mode 100644 index 000000000..ed2e79fa5 --- /dev/null +++ b/tools/ctrace/src/decode/DwtPacketDecoder.cpp @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "DwtPacketDecoder.hpp" + +#include "TraceEvent.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +enum class DwtPacketSource : std::uint8_t { + EventCounter = 0U, + ExceptionTrace = 1U, + PeriodicPcSample = 2U, + PmuOverflow = 3U, +}; + +enum class DwtDataPacketType : std::uint8_t { + Address = 1U, + Value = 2U, +}; + +enum class DwtExceptionActionCode : std::uint32_t { + Entered = 1U, + Exited = 2U, + Returned = 3U, +}; + +// DWT hardware source IDs 8..23 encode packet type, comparator, and subtype. +constexpr std::uint8_t kFirstDataTraceSource = 8U; +constexpr std::uint8_t kLastDataTraceSource = 23U; +constexpr std::uint8_t kDataTraceSubtypeMask = 0x1U; +constexpr std::uint8_t kDataTraceComparatorMask = 0x3U; +constexpr std::uint8_t kDataTraceComparatorShift = 1U; +constexpr std::uint8_t kDataTracePacketTypeMask = 0x3U; +constexpr std::uint8_t kDataTracePacketTypeShift = 3U; + +constexpr std::uint32_t kExceptionNumberMask = 0x1ffU; +constexpr std::uint32_t kExceptionActionMask = 0x3U; +constexpr std::uint32_t kExceptionActionShift = 12U; + +constexpr std::uint8_t kArmv7MFullPcBytes = 4U; +constexpr std::uint8_t kArmv7MAddressOffsetBytes = 2U; + +} // namespace + +void applyDwtTraceStatus(TraceEvent& event, const DwtTraceStatus& status) +{ + event.quality = status; +} + +std::vector DwtPacketDecoder::decode(const DwtPayloadPacket& payload) +{ + std::vector output; + const auto discriminator = payload.discriminator; + + const auto source = static_cast(discriminator); + if (source == DwtPacketSource::EventCounter || source == DwtPacketSource::PmuOverflow) { + output = flush(payload.status, payload.tcyc); + TraceEvent packet = source == DwtPacketSource::EventCounter + ? TraceEvent(DwtEventTraceEvent{discriminator, payload.size, payload.value}) + : TraceEvent(PmuTraceEvent{discriminator, payload.size, payload.value}); + packet.index = payload.index; + packet.traceBusId = payload.traceBusId; + packet.tcyc = payload.tcyc; + applyDwtTraceStatus(packet, payload.status); + output.push_back(std::move(packet)); + return output; + } + + if (source == DwtPacketSource::ExceptionTrace) { + output = flush(payload.status, payload.tcyc); + const auto exceptionNumber = payload.value & kExceptionNumberMask; + const auto action = exceptionAction((payload.value >> kExceptionActionShift) & kExceptionActionMask); + if (action == ExceptionAction::Unknown) { + TraceEvent error{TraceIssueEvent{ + "invalid-exception-action", + TraceIssueSeverity::Error, + "invalid exception action 0x0 for exception " + std::to_string(exceptionNumber), + std::nullopt, + std::nullopt, + }}; + error.index = payload.index; + error.traceBusId = payload.traceBusId; + error.tcyc = payload.tcyc; + applyDwtTraceStatus(error, payload.status); + output.push_back(std::move(error)); + return output; + } + TraceEvent packet{ExceptionTraceEvent{exceptionNumber, action}}; + packet.index = payload.index; + packet.traceBusId = payload.traceBusId; + packet.tcyc = payload.tcyc; + applyDwtTraceStatus(packet, payload.status); + output.push_back(std::move(packet)); + return output; + } + + if (source == DwtPacketSource::PeriodicPcSample) { + // PC samples need a dedicated output event. Until that event is + // defined, flush preceding data trace but do not expose the sample as + // an address event. + output = flush(payload.status, payload.tcyc); + return output; + } + + if (discriminator >= kFirstDataTraceSource && discriminator <= kLastDataTraceSource) { + decodeDataTrace(payload, output); + return output; + } + + output = flush(payload.status, payload.tcyc); + return output; +} + +std::vector DwtPacketDecoder::flush(const DwtTraceStatus& status, std::uint64_t tcyc) +{ + std::vector output; + std::vector comparators; + for (std::uint32_t comparator = 0; comparator < pendingDataTrace_.size(); ++comparator) { + if (pendingDataTrace_[comparator].has_value()) { + comparators.push_back(comparator); + } + } + std::sort(comparators.begin(), comparators.end(), [this](const auto left, const auto right) { + const auto leftIndex = pendingDataTrace_[left]->index; + const auto rightIndex = pendingDataTrace_[right]->index; + return leftIndex == rightIndex ? left < right : leftIndex < rightIndex; + }); + for (const auto comparator : comparators) { + flushPending(comparator, status, tcyc, output); + } + return output; +} + +void DwtPacketDecoder::reset() +{ + for (auto& pending : pendingDataTrace_) { + pending.reset(); + } +} + +void DwtPacketDecoder::decodeDataTrace(const DwtPayloadPacket& payload, std::vector& output) +{ + const auto discriminator = payload.discriminator; + const auto comparator = + static_cast((discriminator >> kDataTraceComparatorShift) & kDataTraceComparatorMask); + const auto packetType = + static_cast((discriminator >> kDataTracePacketTypeShift) & kDataTracePacketTypeMask); + const auto secondarySubtype = (discriminator & kDataTraceSubtypeMask) != 0U; + + PendingDataTrace event; + event.index = payload.index; + event.traceBusId = payload.traceBusId; + event.status = payload.status; + + if (packetType == DwtDataPacketType::Address) { + const auto expectedSize = secondarySubtype ? kArmv7MAddressOffsetBytes : kArmv7MFullPcBytes; + if (payload.size != expectedSize) { + auto flushed = flush(payload.status, payload.tcyc); + output.insert(output.end(), std::make_move_iterator(flushed.begin()), std::make_move_iterator(flushed.end())); + TraceEvent error{TraceIssueEvent{ + "unsupported-dwt-address-payload", + TraceIssueSeverity::Error, + "unsupported DWT " + std::string(secondarySubtype ? "address" : "PC or match") + " payload size " + + std::to_string(payload.size) + + "; the current ctrace-run format does not provide the " + "architecture and reconstruction data needed to decode it safely", + std::nullopt, + std::nullopt, + }}; + error.index = payload.index; + error.traceBusId = payload.traceBusId; + error.tcyc = payload.tcyc; + applyDwtTraceStatus(error, payload.status); + output.push_back(std::move(error)); + return; + } + if (secondarySubtype) { + event.addressLo16 = payload.value; + event.hasAddressLo16 = true; + } else { + event.pc = payload.value; + event.hasPc = true; + } + sendDataTraceEvent(comparator, event, payload.status, payload.tcyc, output); + return; + } + if (packetType == DwtDataPacketType::Value) { + event.value = payload.value; + event.size = payload.size; + event.isRead = !secondarySubtype; + event.hasValue = true; + sendDataTraceEvent(comparator, event, payload.status, payload.tcyc, output); + return; + } + + auto flushed = flush(payload.status, payload.tcyc); + output.insert(output.end(), std::make_move_iterator(flushed.begin()), std::make_move_iterator(flushed.end())); +} + +void DwtPacketDecoder::sendDataTraceEvent(std::uint32_t comparator, const PendingDataTrace& event, + const DwtTraceStatus& status, std::uint64_t tcyc, + std::vector& output) +{ + auto& pending = pendingDataTrace_[comparator]; + if (!pending.has_value()) { + pending = event; + return; + } + + const auto repeatsFragmentKind = (pending->hasPc && event.hasPc) || + (pending->hasAddressLo16 && event.hasAddressLo16) || + (pending->hasValue && event.hasValue); + if (!repeatsFragmentKind) { + pending->index = event.index; + pending->traceBusId = event.traceBusId; + pending->pc = event.hasPc ? event.pc : pending->pc; + pending->addressLo16 = event.hasAddressLo16 ? event.addressLo16 : pending->addressLo16; + pending->value = event.hasValue ? event.value : pending->value; + pending->size = event.hasValue ? event.size : pending->size; + pending->isRead = event.hasValue ? event.isRead : pending->isRead; + pending->hasPc = pending->hasPc || event.hasPc; + pending->hasAddressLo16 = pending->hasAddressLo16 || event.hasAddressLo16; + pending->hasValue = pending->hasValue || event.hasValue; + pending->status.overflow = pending->status.overflow || event.status.overflow; + pending->status.timestampReliable = pending->status.timestampReliable && event.status.timestampReliable; + pending->status.overflowCount = std::max(pending->status.overflowCount, event.status.overflowCount); + if (pending->hasValue) { + flushPending(comparator, statusForPendingFlush(*pending, status), tcyc, output); + } + return; + } + + flushPending(comparator, statusForPendingFlush(*pending, status), tcyc, output); + pending = event; +} + +void DwtPacketDecoder::flushPending(std::uint32_t comparator, const DwtTraceStatus& status, std::uint64_t tcyc, + std::vector& output) +{ + auto& pending = pendingDataTrace_[comparator]; + if (!pending.has_value()) { + return; + } + + const auto makePacket = [&]() -> TraceEvent { + if (pending->hasValue) { + return TraceEvent(DwtDataTraceEvent{ + comparator, + pending->size, + pending->value, + pending->isRead ? AccessType::Read : AccessType::Write, + pending->hasAddressLo16 ? std::optional(pending->addressLo16) : std::nullopt, + pending->hasPc ? std::optional(pending->pc) : std::nullopt, + }); + } + + DwtAddressTraceLocation location = DwtOffsetTraceLocation{pending->addressLo16}; + if (pending->hasPc && pending->hasAddressLo16) { + location = DwtPcAndOffsetTraceLocation{pending->pc, pending->addressLo16}; + } else if (pending->hasPc) { + location = DwtPcTraceLocation{pending->pc}; + } + return TraceEvent(DwtAddressTraceEvent{comparator, location}); + }; + + TraceEvent packet = makePacket(); + packet.index = pending->index; + packet.traceBusId = pending->traceBusId; + packet.tcyc = tcyc; + applyDwtTraceStatus(packet, status); + output.push_back(std::move(packet)); + pending.reset(); +} + +DwtTraceStatus DwtPacketDecoder::statusForPendingFlush(const PendingDataTrace& pending, + const DwtTraceStatus& current) const +{ + DwtTraceStatus status = current; + status.overflow = status.overflow || pending.status.overflow; + status.timestampReliable = status.timestampReliable && !pending.status.overflow && pending.status.timestampReliable; + status.overflowCount = std::max(status.overflowCount, pending.status.overflowCount); + return status; +} + +ExceptionAction DwtPacketDecoder::exceptionAction(std::uint32_t value) +{ + switch (static_cast(value)) { + case DwtExceptionActionCode::Entered: + return ExceptionAction::Entered; + case DwtExceptionActionCode::Exited: + return ExceptionAction::Exited; + case DwtExceptionActionCode::Returned: + return ExceptionAction::Returned; + default: + return ExceptionAction::Unknown; + } +} diff --git a/tools/ctrace/src/decode/DwtPacketDecoder.hpp b/tools/ctrace/src/decode/DwtPacketDecoder.hpp new file mode 100644 index 000000000..5873f5de6 --- /dev/null +++ b/tools/ctrace/src/decode/DwtPacketDecoder.hpp @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "TraceEvent.hpp" + +#include +#include +#include +#include +#include + +using DwtTraceStatus = TraceQuality; + +void applyDwtTraceStatus(TraceEvent& packet, const DwtTraceStatus& status); + +struct DwtPayloadPacket { + std::uint64_t index = 0; + std::uint8_t traceBusId = 0U; + std::uint8_t discriminator = 0; + std::uint8_t size = 0; + std::uint32_t value = 0; + std::uint64_t tcyc = 0; + DwtTraceStatus status; +}; + +class DwtPacketDecoder { +public: + std::vector decode(const DwtPayloadPacket& payload); + std::vector flush(const DwtTraceStatus& status, std::uint64_t tcyc); + void reset(); + +private: + static constexpr std::size_t kDataTraceComparatorCount = 4U; + + struct PendingDataTrace { + std::uint64_t index = 0; + std::uint8_t traceBusId = 0U; + std::uint32_t pc = 0; + std::uint32_t addressLo16 = 0; + std::uint32_t value = 0; + std::uint8_t size = 4; + bool isRead = false; + bool hasPc = false; + bool hasAddressLo16 = false; + bool hasValue = false; + DwtTraceStatus status; + }; + + void decodeDataTrace(const DwtPayloadPacket& payload, std::vector& output); + void sendDataTraceEvent(std::uint32_t comparator, const PendingDataTrace& event, const DwtTraceStatus& status, + std::uint64_t tcyc, std::vector& output); + void flushPending(std::uint32_t comparator, const DwtTraceStatus& status, std::uint64_t tcyc, + std::vector& output); + DwtTraceStatus statusForPendingFlush(const PendingDataTrace& pending, const DwtTraceStatus& current) const; + + static ExceptionAction exceptionAction(std::uint32_t value); + std::array, kDataTraceComparatorCount> pendingDataTrace_; +}; diff --git a/tools/ctrace/src/decode/OpenCsdErrorController.cpp b/tools/ctrace/src/decode/OpenCsdErrorController.cpp new file mode 100644 index 000000000..1c8f0188e --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdErrorController.cpp @@ -0,0 +1,403 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdErrorController.hpp" + +#include "common/ocsd_error.h" +#include "common/ocsd_msg_logger.h" +#include "opencsd/ocsd_if_types.h" + +#include +#include +#include +#include +#include + +namespace { + +std::string trimTrailingWhitespace(std::string value) +{ + while (!value.empty() && + (value.back() == '\n' || value.back() == '\r' || value.back() == ' ' || value.back() == '\t')) { + value.pop_back(); + } + return value; +} + +} // namespace + +void OpenCsdErrorController::beginDataPathCall() +{ + callErrors_.clear(); +} + +OpenCsdErrorController::Decision OpenCsdErrorController::decide(ocsd_datapath_resp_t response) const +{ + Decision decision; + decision.response = response; + decision.errors = callErrors_; + + const auto recoverable = std::find_if(callErrors_.begin(), callErrors_.end(), [](const auto& error) { + return error.severity == OCSD_ERR_SEV_ERROR && isRecoverableStreamError(error.code); + }); + const auto nonRecoverableError = std::find_if(callErrors_.rbegin(), callErrors_.rend(), [](const auto& error) { + return error.severity == OCSD_ERR_SEV_ERROR && !isRecoverableStreamError(error.code); + }); + if (OCSD_DATA_RESP_IS_FATAL(response) && nonRecoverableError != callErrors_.rend()) { + decision.error = *nonRecoverableError; + } else if (recoverable != callErrors_.end()) { + decision.error = *recoverable; + } else if (!callErrors_.empty()) { + decision.error = callErrors_.back(); + } + + if (OCSD_DATA_RESP_IS_FATAL(response)) { + decision.action = response == OCSD_RESP_FATAL_INVALID_DATA && nonRecoverableError == callErrors_.rend() && + decision.error.has_value() && decision.error->severity == OCSD_ERR_SEV_ERROR && + isRecoverableStreamError(decision.error->code) + ? Action::RecoverStream + : Action::Abort; + return decision; + } + if (recoverable != callErrors_.end()) { + decision.action = Action::RecoverStream; + return decision; + } + if (OCSD_DATA_RESP_IS_WAIT(response)) { + decision.action = Action::Wait; + return decision; + } + decision.action = Action::Continue; + return decision; +} + +bool OpenCsdErrorController::isRecoverableStreamError(ocsd_err_t code) +{ + return code == OCSD_ERR_BAD_PACKET_SEQ || code == OCSD_ERR_INVALID_PCKT_HDR; +} + +bool OpenCsdErrorController::responseReportsError(ocsd_datapath_resp_t response) +{ + return response == OCSD_RESP_ERR_CONT || response == OCSD_RESP_ERR_WAIT || OCSD_DATA_RESP_IS_FATAL(response); +} + +std::uint64_t OpenCsdErrorController::errorOffset(const Decision& decision, std::uint64_t fallback) +{ + return decision.error.has_value() && decision.error->hasIndex ? decision.error->index : fallback; +} + +std::string OpenCsdErrorController::issueCode(const Decision& decision) +{ + if (!decision.error.has_value()) { + return "opencsd-decode-error"; + } + switch (decision.error->code) { + case OCSD_ERR_BAD_PACKET_SEQ: + return "opencsd-bad-packet-sequence"; + case OCSD_ERR_INVALID_PCKT_HDR: + return "opencsd-invalid-packet-header"; + default: + return "opencsd-decode-error"; + } +} + +std::string OpenCsdErrorController::describeApiError(ocsd_err_t code, const std::string& message) +{ + return message + " (" + errorCodeName(code) + ")"; +} + +std::string OpenCsdErrorController::describeSummary(const Decision& decision) +{ + std::string summary; + if (decision.error.has_value()) { + switch (decision.error->code) { + case OCSD_ERR_BAD_PACKET_SEQ: + summary = "OpenCSD detected an invalid ITM packet sequence"; + break; + case OCSD_ERR_INVALID_PCKT_HDR: + summary = "OpenCSD detected an invalid ITM packet header"; + break; + case OCSD_ERR_NOT_INIT: + summary = "OpenCSD decoder is not initialized"; + break; + case OCSD_ERR_MEM: + summary = "OpenCSD decoder ran out of memory"; + break; + case OCSD_ERR_INVALID_PARAM_VAL: + case OCSD_ERR_INVALID_PARAM_TYPE: + summary = "OpenCSD rejected a decoder parameter"; + break; + case OCSD_ERR_FILE_ERROR: + case OCSD_ERR_RDR_FILE_NOT_FOUND: + summary = "OpenCSD could not read required input data"; + break; + case OCSD_ERR_DATA_DECODE_FATAL: + summary = "OpenCSD could not decode the trace data"; + break; + default: + summary = "OpenCSD decoder error"; + break; + } + } else { + switch (decision.response) { + case OCSD_RESP_FATAL_NOT_INIT: + summary = "OpenCSD decoder is not initialized"; + break; + case OCSD_RESP_FATAL_INVALID_OP: + summary = "OpenCSD rejected a decoder operation"; + break; + case OCSD_RESP_FATAL_INVALID_PARAM: + summary = "OpenCSD rejected a decoder parameter"; + break; + case OCSD_RESP_FATAL_INVALID_DATA: + summary = "OpenCSD rejected invalid trace data"; + break; + case OCSD_RESP_FATAL_SYS_ERR: + summary = "OpenCSD reported a system error"; + break; + default: + summary = "OpenCSD decoder error"; + break; + } + } + const auto offset = errorOffset(decision, 0U); + if ((decision.error.has_value() && decision.error->hasIndex) || offset != 0U) { + summary += " at raw offset " + std::to_string(offset); + } + return summary + "."; +} + +std::string OpenCsdErrorController::responseName(ocsd_datapath_resp_t response) +{ + switch (response) { + case OCSD_RESP_CONT: + return "OCSD_RESP_CONT"; + case OCSD_RESP_WARN_CONT: + return "OCSD_RESP_WARN_CONT"; + case OCSD_RESP_ERR_CONT: + return "OCSD_RESP_ERR_CONT"; + case OCSD_RESP_WAIT: + return "OCSD_RESP_WAIT"; + case OCSD_RESP_WARN_WAIT: + return "OCSD_RESP_WARN_WAIT"; + case OCSD_RESP_ERR_WAIT: + return "OCSD_RESP_ERR_WAIT"; + case OCSD_RESP_FATAL_NOT_INIT: + return "OCSD_RESP_FATAL_NOT_INIT"; + case OCSD_RESP_FATAL_INVALID_OP: + return "OCSD_RESP_FATAL_INVALID_OP"; + case OCSD_RESP_FATAL_INVALID_PARAM: + return "OCSD_RESP_FATAL_INVALID_PARAM"; + case OCSD_RESP_FATAL_INVALID_DATA: + return "OCSD_RESP_FATAL_INVALID_DATA"; + case OCSD_RESP_FATAL_SYS_ERR: + return "OCSD_RESP_FATAL_SYS_ERR"; + } + return "OCSD_RESP_UNKNOWN(" + std::to_string(static_cast(response)) + ")"; +} + +std::string OpenCsdErrorController::errorCodeName(ocsd_err_t code) +{ + switch (code) { + case OCSD_OK: + return "OCSD_OK"; + case OCSD_ERR_FAIL: + return "OCSD_ERR_FAIL"; + case OCSD_ERR_MEM: + return "OCSD_ERR_MEM"; + case OCSD_ERR_NOT_INIT: + return "OCSD_ERR_NOT_INIT"; + case OCSD_ERR_INVALID_ID: + return "OCSD_ERR_INVALID_ID"; + case OCSD_ERR_BAD_HANDLE: + return "OCSD_ERR_BAD_HANDLE"; + case OCSD_ERR_INVALID_PARAM_VAL: + return "OCSD_ERR_INVALID_PARAM_VAL"; + case OCSD_ERR_INVALID_PARAM_TYPE: + return "OCSD_ERR_INVALID_PARAM_TYPE"; + case OCSD_ERR_FILE_ERROR: + return "OCSD_ERR_FILE_ERROR"; + case OCSD_ERR_NO_PROTOCOL: + return "OCSD_ERR_NO_PROTOCOL"; + case OCSD_ERR_ATTACH_TOO_MANY: + return "OCSD_ERR_ATTACH_TOO_MANY"; + case OCSD_ERR_ATTACH_INVALID_PARAM: + return "OCSD_ERR_ATTACH_INVALID_PARAM"; + case OCSD_ERR_ATTACH_COMP_NOT_FOUND: + return "OCSD_ERR_ATTACH_COMP_NOT_FOUND"; + case OCSD_ERR_RDR_FILE_NOT_FOUND: + return "OCSD_ERR_RDR_FILE_NOT_FOUND"; + case OCSD_ERR_RDR_INVALID_INIT: + return "OCSD_ERR_RDR_INVALID_INIT"; + case OCSD_ERR_RDR_NO_DECODER: + return "OCSD_ERR_RDR_NO_DECODER"; + case OCSD_ERR_DATA_DECODE_FATAL: + return "OCSD_ERR_DATA_DECODE_FATAL"; + case OCSD_ERR_DFMTR_NOTCONTTRACE: + return "OCSD_ERR_DFMTR_NOTCONTTRACE"; + case OCSD_ERR_DFMTR_BAD_FHSYNC: + return "OCSD_ERR_DFMTR_BAD_FHSYNC"; + case OCSD_ERR_BAD_PACKET_SEQ: + return "OCSD_ERR_BAD_PACKET_SEQ"; + case OCSD_ERR_INVALID_PCKT_HDR: + return "OCSD_ERR_INVALID_PCKT_HDR"; + case OCSD_ERR_PKT_INTERP_FAIL: + return "OCSD_ERR_PKT_INTERP_FAIL"; + case OCSD_ERR_UNSUPPORTED_ISA: + return "OCSD_ERR_UNSUPPORTED_ISA"; + case OCSD_ERR_HW_CFG_UNSUPP: + return "OCSD_ERR_HW_CFG_UNSUPP"; + case OCSD_ERR_UNSUPP_DECODE_PKT: + return "OCSD_ERR_UNSUPP_DECODE_PKT"; + case OCSD_ERR_BAD_DECODE_PKT: + return "OCSD_ERR_BAD_DECODE_PKT"; + case OCSD_ERR_COMMIT_PKT_OVERRUN: + return "OCSD_ERR_COMMIT_PKT_OVERRUN"; + case OCSD_ERR_MEM_NACC: + return "OCSD_ERR_MEM_NACC"; + case OCSD_ERR_RET_STACK_OVERFLOW: + return "OCSD_ERR_RET_STACK_OVERFLOW"; + case OCSD_ERR_DCDT_NO_FORMATTER: + return "OCSD_ERR_DCDT_NO_FORMATTER"; + case OCSD_ERR_MEM_ACC_OVERLAP: + return "OCSD_ERR_MEM_ACC_OVERLAP"; + case OCSD_ERR_MEM_ACC_FILE_NOT_FOUND: + return "OCSD_ERR_MEM_ACC_FILE_NOT_FOUND"; + case OCSD_ERR_MEM_ACC_FILE_DIFF_RANGE: + return "OCSD_ERR_MEM_ACC_FILE_DIFF_RANGE"; + case OCSD_ERR_MEM_ACC_RANGE_INVALID: + return "OCSD_ERR_MEM_ACC_RANGE_INVALID"; + case OCSD_ERR_MEM_ACC_BAD_LEN: + return "OCSD_ERR_MEM_ACC_BAD_LEN"; + case OCSD_ERR_TEST_SNAPSHOT_PARSE: + return "OCSD_ERR_TEST_SNAPSHOT_PARSE"; + case OCSD_ERR_TEST_SNAPSHOT_PARSE_INFO: + return "OCSD_ERR_TEST_SNAPSHOT_PARSE_INFO"; + case OCSD_ERR_TEST_SNAPSHOT_READ: + return "OCSD_ERR_TEST_SNAPSHOT_READ"; + case OCSD_ERR_TEST_SS_TO_DECODER: + return "OCSD_ERR_TEST_SS_TO_DECODER"; + case OCSD_ERR_DCDREG_NAME_REPEAT: + return "OCSD_ERR_DCDREG_NAME_REPEAT"; + case OCSD_ERR_DCDREG_NAME_UNKNOWN: + return "OCSD_ERR_DCDREG_NAME_UNKNOWN"; + case OCSD_ERR_DCDREG_TYPE_UNKNOWN: + return "OCSD_ERR_DCDREG_TYPE_UNKNOWN"; + case OCSD_ERR_DCDREG_TOOMANY: + return "OCSD_ERR_DCDREG_TOOMANY"; + case OCSD_ERR_DCD_INTERFACE_UNUSED: + return "OCSD_ERR_DCD_INTERFACE_UNUSED"; + case OCSD_ERR_INVALID_OPCODE: + return "OCSD_ERR_INVALID_OPCODE"; + case OCSD_ERR_I_RANGE_LIMIT_OVERRUN: + return "OCSD_ERR_I_RANGE_LIMIT_OVERRUN"; + case OCSD_ERR_BAD_DECODE_IMAGE: + return "OCSD_ERR_BAD_DECODE_IMAGE"; + case OCSD_ERR_LAST: + return "OCSD_ERR_LAST"; + default: + return "OCSD_ERR_UNKNOWN(" + std::to_string(static_cast(code)) + ")"; + } +} + +std::string OpenCsdErrorController::describe(const Decision& decision) +{ + std::ostringstream out; + out << responseName(decision.response); + if (!decision.error.has_value()) { + return out.str(); + } + const auto& error = *decision.error; + out << ": " << errorCodeName(error.code); + if (error.hasIndex) { + out << " at raw offset " << error.index; + } + if (!error.message.empty()) { + out << ": " << error.message; + } + return out.str(); +} + +OpenCsdErrorController::ErrorSourceHandleResult +OpenCsdErrorController::RegisterErrorSource(const std::string& componentName) +{ + const auto existing = std::find(sources_.begin(), sources_.end(), componentName); + if (existing != sources_.end()) { + return static_cast(std::distance(sources_.begin(), existing)); + } + const auto handle = static_cast(sources_.size()); + sources_.push_back(componentName); + return handle; +} + +OpenCsdErrorController::ErrorLogVerbosityResult OpenCsdErrorController::GetErrorLogVerbosity() const +{ + return OCSD_ERR_SEV_INFO; +} + +void OpenCsdErrorController::LogError(ocsd_hndl_err_log_t, const ocsdError* error) +{ + if (error == nullptr) { + return; + } + callErrors_.push_back(makeRecord(*error)); + lastError_ = *error; + if (OCSD_IS_VALID_CS_SRC_ID(error->getErrorChanID())) { + lastTraceErrors_[error->getErrorChanID()] = *error; + } +} + +void OpenCsdErrorController::LogMessage(ocsd_hndl_err_log_t handle, ocsd_err_severity_t filterLevel, + const std::string& message) +{ + if (outputLogger_ != nullptr && GetErrorLogVerbosity() >= filterLevel && outputLogger_->isLogging()) { + outputLogger_->LogMsg(sourceName(handle) + ": " + message); + } +} + +ocsdError* OpenCsdErrorController::GetLastError() +{ + return lastError_.has_value() ? &*lastError_ : nullptr; +} + +ocsdError* OpenCsdErrorController::GetLastIDError(std::uint8_t traceBusId) +{ + if (!OCSD_IS_VALID_CS_SRC_ID(traceBusId)) { + return nullptr; + } + auto& error = lastTraceErrors_[traceBusId]; + return error.has_value() ? &error.value() : nullptr; +} + +ocsdMsgLogger* OpenCsdErrorController::getOutputLogger() +{ + return outputLogger_; +} + +void OpenCsdErrorController::setOutputLogger(ocsdMsgLogger* logger) +{ + outputLogger_ = logger; +} + +OpenCsdErrorRecord OpenCsdErrorController::makeRecord(const ocsdError& error) +{ + OpenCsdErrorRecord record; + record.severity = error.getErrorSeverity(); + record.code = error.getErrorCode(); + record.hasIndex = error.getErrorIndex() != OCSD_BAD_TRC_INDEX; + record.index = record.hasIndex ? static_cast(error.getErrorIndex()) : 0U; + record.message = trimTrailingWhitespace(error.getMessage()); + return record; +} + +std::string OpenCsdErrorController::sourceName(ocsd_hndl_err_log_t handle) const +{ + if (handle < sources_.size()) { + return sources_[handle]; + } + return "OpenCSD"; +} diff --git a/tools/ctrace/src/decode/OpenCsdErrorController.hpp b/tools/ctrace/src/decode/OpenCsdErrorController.hpp new file mode 100644 index 000000000..7ca866203 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdErrorController.hpp @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "common/ocsd_error.h" +#include "common/ocsd_msg_logger.h" +#include "interfaces/trc_error_log_i.h" +#include "opencsd/ocsd_if_types.h" + +#include +#include +#include +#include +#include + +struct OpenCsdErrorRecord { + ocsd_err_severity_t severity = OCSD_ERR_SEV_ERROR; + ocsd_err_t code = OCSD_OK; + std::uint64_t index = 0; + bool hasIndex = false; + std::string message; +}; + +class OpenCsdErrorController final : public ITraceErrorLog { + // OpenCSD declares these scalar return types with top-level const. Keep + // the exact override type without repeating the ineffective qualifier at + // every declaration and definition. + using ErrorSourceHandleResult = const ocsd_hndl_err_log_t; + using ErrorLogVerbosityResult = const ocsd_err_severity_t; + +public: + enum class Action { + Continue, + Wait, + RecoverStream, + Abort, + }; + + struct Decision { + Action action = Action::Continue; + ocsd_datapath_resp_t response = OCSD_RESP_CONT; + std::optional error; + std::vector errors; + }; + + void beginDataPathCall(); + Decision decide(ocsd_datapath_resp_t response) const; + + static bool isRecoverableStreamError(ocsd_err_t code); + static bool responseReportsError(ocsd_datapath_resp_t response); + static std::string responseName(ocsd_datapath_resp_t response); + static std::string errorCodeName(ocsd_err_t code); + static std::uint64_t errorOffset(const Decision& decision, std::uint64_t fallback); + static std::string issueCode(const Decision& decision); + static std::string describeApiError(ocsd_err_t code, const std::string& message); + static std::string describeSummary(const Decision& decision); + static std::string describe(const Decision& decision); + + ErrorSourceHandleResult RegisterErrorSource(const std::string& componentName) override; + ErrorLogVerbosityResult GetErrorLogVerbosity() const override; + void LogError(ocsd_hndl_err_log_t, const ocsdError* error) override; + void LogMessage(ocsd_hndl_err_log_t handle, ocsd_err_severity_t filterLevel, const std::string& message) override; + ocsdError* GetLastError() override; + ocsdError* GetLastIDError(std::uint8_t traceBusId) override; + ocsdMsgLogger* getOutputLogger() override; + void setOutputLogger(ocsdMsgLogger* logger) override; + +private: + static OpenCsdErrorRecord makeRecord(const ocsdError& error); + std::string sourceName(ocsd_hndl_err_log_t handle) const; + + std::vector sources_{"Gen_Err", "Gen_Warn", "Gen_Info"}; + std::vector callErrors_; + std::optional lastError_; + std::array, 0x80> lastTraceErrors_{}; + ocsdMsgLogger* outputLogger_ = nullptr; +}; diff --git a/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp b/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp new file mode 100644 index 000000000..3248266c7 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdItmDecoder.hpp" + +#include "TraceEvent.hpp" +#include "OpenCsdErrorController.hpp" +#include "OpenCsdPacketCollector.hpp" +#include "OpenCsdItmSession.hpp" +#include "OpenCsdTraceElement.hpp" +#include "opencsd/ocsd_if_types.h" + +#include +#include +#include +#include +#include +#include + +static_assert(sizeof(ocsd_trc_index_t) == sizeof(std::uint64_t), "ctrace requires 64-bit OpenCSD trace indices"); + +class OpenCsdItmDecoderImpl { +public: + explicit OpenCsdItmDecoderImpl(OpenCsdTraceElementSink& elementSink) : collector_(elementSink) + { + try { + session_ = std::make_unique(collector_, errorController_); + } catch (const OpenCsdItmSessionError& error) { + failInitialization(error.what()); + } + } + + void push(const std::uint8_t* data, std::uint32_t size) + { + if (finished_) { + throw std::runtime_error("OpenCSD ITM decoder already finished"); + } + std::uint32_t offset = 0; + while (offset < size) { + const auto span = std::min(kMaxTraceDataInBytes, size - offset); + processBlock(data + offset, span); + offset += span; + } + result_.bytesIn = static_cast(traceIndex_); + } + + OpenCsdItmDecodeResult finish() + { + if (finished_) { + return result_; + } + completeConsumedDataLoss(traceIndex_); + collector_.beginTransaction(); + errorController_.beginDataPathCall(); + const auto response = session_->endOfTrace(); + collector_.rethrowOutputError(); + const auto decision = errorController_.decide(response); + if (decision.action == OpenCsdErrorController::Action::Abort) { + abortDecode(decision, 0U, traceIndex_, 0U, "OpenCSD aborted end-of-trace processing: "); + } + if (decision.action == OpenCsdErrorController::Action::RecoverStream) { + const auto sourceOffset = OpenCsdErrorController::errorOffset(decision, traceIndex_); + collector_.commitTransactionBefore(sourceOffset); + appendReportedErrors(decision, traceIndex_, true); + } else { + collector_.commitTransaction(); + appendReportedErrors(decision, traceIndex_, false); + if (decision.action == OpenCsdErrorController::Action::Wait) { + flushAfterWait(); + } + } + finished_ = true; + result_.bytesIn = static_cast(traceIndex_); + return result_; + } + +private: + static constexpr std::uint32_t kMaxTraceDataInBytes = 4U * 1024U; + + void appendReportedErrors(const OpenCsdErrorController::Decision& decision, std::uint64_t baseOffset, + bool discontinuity, bool force = false) + { + bool emittedError = false; + bool nextDiscontinuity = discontinuity; + for (const auto& error : decision.errors) { + if (error.severity != OCSD_ERR_SEV_ERROR && error.severity != OCSD_ERR_SEV_WARN) { + continue; + } + auto item = decision; + item.error = error; + const auto sourceOffset = OpenCsdErrorController::errorOffset(item, baseOffset); + const auto isError = error.severity == OCSD_ERR_SEV_ERROR; + collector_.appendDecodeError(static_cast(sourceOffset), + OpenCsdErrorController::describeSummary(item), + OpenCsdErrorController::issueCode(item), nextDiscontinuity && isError, + isError ? TraceIssueSeverity::Error : TraceIssueSeverity::Warning); + if (isError) { + emittedError = true; + nextDiscontinuity = false; + } + } + if (!emittedError && (force || OpenCsdErrorController::responseReportsError(decision.response))) { + collector_.appendDecodeError( + static_cast(OpenCsdErrorController::errorOffset(decision, baseOffset)), + OpenCsdErrorController::describeSummary(decision), OpenCsdErrorController::issueCode(decision), discontinuity, + TraceIssueSeverity::Error); + } + } + + [[noreturn]] void abortDecode(const OpenCsdErrorController::Decision& decision, std::uint32_t size, + std::uint64_t baseOffset, std::uint32_t bytesConsumed, const std::string& prefix) + { + collector_.rollbackTransaction(); + completeConsumedDataLoss(OpenCsdErrorController::errorOffset(decision, baseOffset)); + appendReportedErrors(decision, baseOffset, true); + const auto processed = baseOffset + std::min(bytesConsumed, size); + throw OpenCsdFatalError(prefix + OpenCsdErrorController::describe(decision), processed); + } + + void completeConsumedDataLoss(std::uint64_t resumeOffset) + { + if (!consumedDataLossStart_.has_value()) { + return; + } + const auto startOffset = *consumedDataLossStart_; + const auto endOffset = std::max(startOffset, resumeOffset); + const auto consumed = endOffset - startOffset; + consumedDataLossStart_.reset(); + const auto boundaryAlreadyMarked = consumedDataLossBoundaryMarked_; + consumedDataLossBoundaryMarked_ = false; + if (consumed == 0U) { + return; + } + const auto message = + "OpenCSD consumed " + std::to_string(consumed) + + " raw bytes while waiting for usable ITM trace packets; data loss until a later sync/recovery point"; + if (boundaryAlreadyMarked) { + collector_.prependDataLossError(static_cast(startOffset), message, consumed); + } else { + collector_.prependDiscontinuity(static_cast(startOffset), message, "data-loss", consumed); + } + } + + void processBlock(const std::uint8_t* data, std::uint32_t size) + { + std::uint32_t processed = 0; + std::optional noProgressRetryOffset; + while (processed < size) { + const auto callIndex = static_cast(traceIndex_); + const auto callSize = size - processed; + std::uint32_t processedThisPass = 0; + collector_.beginTransaction(); + errorController_.beginDataPathCall(); + const auto response = session_->pushData(traceIndex_, callSize, data + processed, processedThisPass); + collector_.rethrowOutputError(); + const auto decision = errorController_.decide(response); + if (decision.action == OpenCsdErrorController::Action::RecoverStream) { + const auto sourceOffset = OpenCsdErrorController::errorOffset(decision, callIndex); + completeConsumedDataLoss( + std::min(sourceOffset, collector_.transactionFirstSourceOffset().value_or(sourceOffset))); + // Callbacks before the bad packet remain valid; callbacks at or + // after its offset belong to the failed decode transaction. + collector_.commitTransactionBefore(sourceOffset); + appendReportedErrors(decision, callIndex, true); + const auto consumed = std::min(processedThisPass, callSize); + processed += consumed; + traceIndex_ += consumed; + dataLossActive_ = true; + consumedDataLossStart_ = static_cast(traceIndex_); + consumedDataLossBoundaryMarked_ = true; + resetDecoder(); + continue; + } + if (decision.action == OpenCsdErrorController::Action::Abort) { + abortDecode(decision, callSize, callIndex, processedThisPass, "OpenCSD aborted decode: "); + } + if (decision.action == OpenCsdErrorController::Action::Wait) { + if (collector_.transactionElementCount() == 0U) { + collector_.rollbackTransaction(); + } else { + completeConsumedDataLoss(collector_.transactionFirstSourceOffset().value_or(callIndex)); + collector_.commitTransaction(); + } + appendReportedErrors(decision, callIndex, false); + const auto consumed = std::min(processedThisPass, callSize); + processed += consumed; + traceIndex_ += consumed; + flushAfterWait(); + continue; + } + if (processedThisPass == 0) { + collector_.rollbackTransaction(); + if (noProgressRetryOffset.has_value() && *noProgressRetryOffset == traceIndex_) { + completeConsumedDataLoss(traceIndex_); + collector_.appendDecodeError(traceIndex_, "OpenCSD made no progress after decoder reset; decode aborted", + "opencsd-no-progress", false); + throw OpenCsdFatalError("OpenCSD made no progress after decoder reset", + static_cast(traceIndex_)); + } + collector_.appendDecodeError(traceIndex_, + "OpenCSD made no progress while raw data was present; decoder reset and searching " + "for next real ITM async sync", + "opencsd-no-progress"); + noProgressRetryOffset = static_cast(traceIndex_); + dataLossActive_ = true; + consumedDataLossStart_ = static_cast(traceIndex_); + consumedDataLossBoundaryMarked_ = true; + resetDecoder(); + continue; + } + if (collector_.transactionElementCount() == 0U) { + collector_.rollbackTransaction(); + appendReportedErrors(decision, callIndex, false); + if (!dataLossActive_) { + consumedDataLossStart_ = static_cast(traceIndex_); + consumedDataLossBoundaryMarked_ = false; + dataLossActive_ = true; + } + processed += processedThisPass; + traceIndex_ += processedThisPass; + continue; + } + completeConsumedDataLoss(collector_.transactionFirstSourceOffset().value_or(callIndex)); + collector_.commitTransaction(); + appendReportedErrors(decision, callIndex, false); + dataLossActive_ = false; + processed += processedThisPass; + traceIndex_ += processedThisPass; + noProgressRetryOffset.reset(); + } + } + + void flushAfterWait() + { + // Bound backpressure handling so a broken decoder cannot stall a file forever. + static constexpr std::uint32_t kMaxFlushCalls = 1024U; + for (std::uint32_t call = 0; call < kMaxFlushCalls; ++call) { + collector_.beginTransaction(); + errorController_.beginDataPathCall(); + const auto response = session_->flush(); + collector_.rethrowOutputError(); + const auto decision = errorController_.decide(response); + if (decision.action == OpenCsdErrorController::Action::Abort) { + abortDecode(decision, 0U, traceIndex_, 0U, "OpenCSD aborted while flushing a WAIT response: "); + } + if (decision.action == OpenCsdErrorController::Action::RecoverStream) { + const auto sourceOffset = OpenCsdErrorController::errorOffset(decision, traceIndex_); + collector_.commitTransactionBefore(sourceOffset); + appendReportedErrors(decision, traceIndex_, true); + resetDecoder(); + return; + } + if (collector_.transactionElementCount() == 0U) { + collector_.rollbackTransaction(); + } else { + collector_.commitTransaction(); + } + appendReportedErrors(decision, traceIndex_, false); + if (decision.action == OpenCsdErrorController::Action::Continue) { + return; + } + } + const auto limit = std::to_string(kMaxFlushCalls); + collector_.appendDecodeError(traceIndex_, + "OpenCSD WAIT did not clear after " + limit + " FLUSH operations; decode aborted", + "opencsd-wait-timeout"); + throw OpenCsdFatalError("OpenCSD WAIT did not clear after " + limit + " FLUSH operations", + static_cast(traceIndex_)); + } + + void resetDecoder() + { + errorController_.beginDataPathCall(); + const auto response = session_->reset(); + collector_.rethrowOutputError(); + const auto decision = errorController_.decide(response); + if (decision.action != OpenCsdErrorController::Action::Continue) { + appendReportedErrors(decision, traceIndex_, true, true); + throw OpenCsdFatalError("OpenCSD decoder reset failed: " + OpenCsdErrorController::describe(decision), + static_cast(traceIndex_)); + } + appendReportedErrors(decision, traceIndex_, false); + } + + [[noreturn]] void failInitialization(const std::string& message) + { + collector_.appendDecodeError(traceIndex_, message, "opencsd-initialization-error", false); + throw OpenCsdFatalError(message, static_cast(traceIndex_)); + } + + OpenCsdPacketCollector collector_; + OpenCsdErrorController errorController_; + std::unique_ptr session_; + ocsd_trc_index_t traceIndex_ = 0; + bool dataLossActive_ = false; + std::optional consumedDataLossStart_; + bool consumedDataLossBoundaryMarked_ = false; + OpenCsdItmDecodeResult result_; + bool finished_ = false; +}; + +OpenCsdItmDecoder::OpenCsdItmDecoder(OpenCsdTraceElementSink& elementSink) + : impl_(std::make_unique(elementSink)) +{ +} + +OpenCsdItmDecoder::~OpenCsdItmDecoder() = default; + +void OpenCsdItmDecoder::push(const std::uint8_t* data, std::uint32_t size) +{ + impl_->push(data, size); +} + +OpenCsdItmDecodeResult OpenCsdItmDecoder::finish() +{ + return impl_->finish(); +} diff --git a/tools/ctrace/src/decode/OpenCsdItmDecoder.hpp b/tools/ctrace/src/decode/OpenCsdItmDecoder.hpp new file mode 100644 index 000000000..fdaa3f7ec --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdItmDecoder.hpp @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "OpenCsdTraceElement.hpp" + +#include +#include +#include +#include + +struct OpenCsdItmDecodeResult { + std::uint64_t bytesIn = 0; +}; + +class OpenCsdFatalError final : public std::runtime_error { +public: + OpenCsdFatalError(const std::string& message, std::uint64_t bytesProcessed) + : std::runtime_error(message), bytesProcessed_(bytesProcessed) + { + } + + std::uint64_t bytesProcessed() const noexcept + { + return bytesProcessed_; + } + +private: + std::uint64_t bytesProcessed_ = 0; +}; + +class OpenCsdItmDecoderImpl; + +class OpenCsdItmDecoder { +public: + OpenCsdItmDecoder(OpenCsdTraceElementSink& elementSink); + ~OpenCsdItmDecoder(); + + OpenCsdItmDecoder(const OpenCsdItmDecoder&) = delete; + OpenCsdItmDecoder& operator=(const OpenCsdItmDecoder&) = delete; + + void push(const std::uint8_t* data, std::uint32_t size); + OpenCsdItmDecodeResult finish(); + +private: + std::unique_ptr impl_; +}; diff --git a/tools/ctrace/src/decode/OpenCsdItmSession.cpp b/tools/ctrace/src/decode/OpenCsdItmSession.cpp new file mode 100644 index 000000000..5a28413e7 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdItmSession.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdItmSession.hpp" + +#include "OpenCsdErrorController.hpp" +#include "OpenCsdPacketCollector.hpp" +#include "common/ocsd_dcd_mngr_i.h" +#include "common/ocsd_lib_dcd_register.h" +#include "common/trc_component.h" +#include "interfaces/trc_data_raw_in_i.h" +#include "opencsd/itm/trc_pkt_types_itm.h" +#include "opencsd/ocsd_if_types.h" + +#include + +namespace { + +constexpr std::uint32_t kItmTcrSwoEnable = 1U << 4U; + +} // namespace + +OpenCsdItmSession::OpenCsdItmSession(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController) +{ + ocsd_itm_cfg config{}; + // Keep OpenCSD timestamps in raw ITM ticks. They are scaled after decode, + // where the originating CoreSight stream and processor are known. + config.reg_tcr = kItmTcrSwoEnable; + config_ = &config; + + try { + createDecoder(collector, errorController); + } catch (...) { + destroyDecoder(); + throw; + } +} + +OpenCsdItmSession::~OpenCsdItmSession() noexcept +{ + destroyDecoder(); +} + +ocsd_datapath_resp_t OpenCsdItmSession::pushData(ocsd_trc_index_t index, std::uint32_t size, const std::uint8_t* data, + std::uint32_t& processed) +{ + return input_->TraceDataIn(OCSD_OP_DATA, index, size, data, &processed); +} + +ocsd_datapath_resp_t OpenCsdItmSession::flush() +{ + return input_->TraceDataIn(OCSD_OP_FLUSH, 0, 0, nullptr, nullptr); +} + +ocsd_datapath_resp_t OpenCsdItmSession::reset() +{ + return input_->TraceDataIn(OCSD_OP_RESET, 0, 0, nullptr, nullptr); +} + +ocsd_datapath_resp_t OpenCsdItmSession::endOfTrace() +{ + return input_->TraceDataIn(OCSD_OP_EOT, 0, 0, nullptr, nullptr); +} + +void OpenCsdItmSession::checkOcsd(ocsd_err_t error, const char* message) +{ + if (error != OCSD_OK) { + throw OpenCsdItmSessionError(OpenCsdErrorController::describeApiError(error, message)); + } +} + +void OpenCsdItmSession::createDecoder(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController) +{ + auto* registry = OcsdLibDcdRegister::getDecoderRegister(); + if (registry == nullptr) { + throw OpenCsdItmSessionError("OpenCSD decoder registry is not initialized"); + } + + auto error = registry->getDecoderMngrByName(OCSD_BUILTIN_DCD_ITM, &manager_); + checkOcsd(error, "failed to get OpenCSD ITM decoder manager"); + + error = manager_->createDecoder(OCSD_CREATE_FLG_FULL_DECODER, 0, &config_, &component_); + checkOcsd(error, "failed to create OpenCSD ITM decoder"); + + error = manager_->attachErrorLogger(component_, &errorController); + checkOcsd(error, "failed to attach OpenCSD packet-decoder error logger"); + if (component_->getAssocComponent() != nullptr) { + error = manager_->attachErrorLogger(component_->getAssocComponent(), &errorController); + checkOcsd(error, "failed to attach OpenCSD packet-processor error logger"); + } + + error = manager_->attachOutputSink(component_, &collector); + checkOcsd(error, "failed to attach OpenCSD ITM output sink"); + + error = manager_->getDataInputI(component_, &input_); + checkOcsd(error, "failed to get OpenCSD ITM input interface"); + + error = manager_->attachPktMonitor(component_, &collector); + checkOcsd(error, "failed to attach OpenCSD ITM packet monitor"); +} + +void OpenCsdItmSession::destroyDecoder() noexcept +{ + input_ = nullptr; + if (manager_ != nullptr && component_ != nullptr) { + manager_->destroyDecoder(component_); + component_ = nullptr; + } +} diff --git a/tools/ctrace/src/decode/OpenCsdItmSession.hpp b/tools/ctrace/src/decode/OpenCsdItmSession.hpp new file mode 100644 index 000000000..26ebc724a --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdItmSession.hpp @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "opencsd/itm/trc_cmp_cfg_itm.h" +#include "opencsd/ocsd_if_types.h" + +#include +#include + +class IDecoderMngr; +class ITrcDataIn; +class OpenCsdErrorController; +class OpenCsdPacketCollector; +class TraceComponent; + +class OpenCsdItmSessionError final : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +// Owns one fully wired OpenCSD ITM callback decoder. Feed/recovery policy stays +// in OpenCsdItmDecoder; this class only manages the external decoder session. +class OpenCsdItmSession final { +public: + OpenCsdItmSession(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController); + ~OpenCsdItmSession() noexcept; + + OpenCsdItmSession(const OpenCsdItmSession&) = delete; + OpenCsdItmSession& operator=(const OpenCsdItmSession&) = delete; + + ocsd_datapath_resp_t pushData(ocsd_trc_index_t index, std::uint32_t size, const std::uint8_t* data, + std::uint32_t& processed); + ocsd_datapath_resp_t flush(); + ocsd_datapath_resp_t reset(); + ocsd_datapath_resp_t endOfTrace(); + +private: + static void checkOcsd(ocsd_err_t error, const char* message); + + void createDecoder(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController); + void destroyDecoder() noexcept; + + ITMConfig config_; + IDecoderMngr* manager_ = nullptr; + TraceComponent* component_ = nullptr; + ITrcDataIn* input_ = nullptr; +}; diff --git a/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp b/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp new file mode 100644 index 000000000..c055b8831 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp @@ -0,0 +1,329 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdPacketCollector.hpp" + +#include "TraceEvent.hpp" +#include "TraceStreamId.hpp" +#include "OpenCsdTraceElement.hpp" +#include "common/trc_gen_elem.h" +#include "opencsd/itm/trc_pkt_elem_itm.h" +#include "opencsd/itm/trc_pkt_types_itm.h" +#include "opencsd/ocsd_if_types.h" +#include "opencsd/trc_gen_elem_types.h" + +#include +#include +#include +#include +#include +#include +#include + +OpenCsdPacketCollector::OpenCsdPacketCollector(OpenCsdTraceElementSink& elementSink) : elementSink_(elementSink) {} + +void OpenCsdPacketCollector::beginTransaction() +{ + transactionActive_ = true; + transactionElements_.clear(); +} + +void OpenCsdPacketCollector::commitTransaction() +{ + for (auto& element : transactionElements_) { + appendCommitted(std::move(element)); + } + transactionElements_.clear(); + transactionActive_ = false; +} + +void OpenCsdPacketCollector::commitTransactionBefore(std::uint64_t sourceOffset) +{ + for (auto& element : transactionElements_) { + const auto elementOffset = element.sourceIndex; + if (elementOffset < sourceOffset && element.kind != OpenCsdTraceElement::Kind::Error) { + appendCommitted(std::move(element)); + } + } + transactionElements_.clear(); + transactionActive_ = false; +} + +void OpenCsdPacketCollector::rollbackTransaction() +{ + transactionElements_.clear(); + transactionActive_ = false; +} + +void OpenCsdPacketCollector::rethrowOutputError() +{ + if (!outputError_) { + return; + } + auto error = outputError_; + outputError_ = nullptr; + std::rethrow_exception(error); +} + +std::size_t OpenCsdPacketCollector::transactionElementCount() const +{ + return transactionElements_.size(); +} + +std::optional OpenCsdPacketCollector::transactionFirstSourceOffset() const +{ + std::optional firstOffset; + for (const auto& element : transactionElements_) { + const auto offset = element.sourceIndex; + if (!firstOffset.has_value() || offset < *firstOffset) { + firstOffset = offset; + } + } + return firstOffset; +} + +void OpenCsdPacketCollector::appendDecodeError(ocsd_trc_index_t index, const std::string& message, + const std::string& issueCode, bool discontinuity, + TraceIssueSeverity severity) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Error; + element.sourceIndex = static_cast(index); + element.discontinuity = discontinuity; + element.issueCode = issueCode; + element.issueSeverity = severity; + element.errorMessage = message; + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::prependDiscontinuity(ocsd_trc_index_t index, const std::string& message, + const std::string& issueCode, + std::optional rawBytesConsumed) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Discontinuity; + element.sourceIndex = static_cast(index); + element.discontinuity = true; + element.issueCode = issueCode; + element.errorMessage = message; + element.rawBytesConsumed = rawBytesConsumed; + if (transactionActive_) { + transactionElements_.insert(transactionElements_.begin(), std::move(element)); + return; + } + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::prependDataLossError(ocsd_trc_index_t index, const std::string& message, + std::uint64_t rawBytesConsumed) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Error; + element.sourceIndex = static_cast(index); + element.issueCode = "data-loss"; + element.errorMessage = message; + element.rawBytesConsumed = rawBytesConsumed; + element.awaitingResumeTimestamp = true; + if (transactionActive_) { + transactionElements_.insert(transactionElements_.begin(), std::move(element)); + return; + } + appendElement(std::move(element)); +} + +ocsd_datapath_resp_t OpenCsdPacketCollector::TraceElemIn(const ocsd_trc_index_t index_sop, + const std::uint8_t trc_chan_id, const OcsdTraceElement& elem) +{ + try { + if (elem.getType() != OCSD_GEN_TRC_ELEM_ITMTRACE) { + return OCSD_RESP_CONT; + } + + const auto& info = elem.swt_itm; + const auto traceBusId = CoreSight::isTraceBusId(trc_chan_id) ? trc_chan_id : CoreSight::kUnformattedTraceBusId; + switch (info.pkt_type) { + case SWIT_PAYLOAD: + appendSoftware(index_sop, traceBusId, elem); + break; + case DWT_PAYLOAD: + appendDwt(index_sop, traceBusId, elem); + break; + case TS_SYNC: + case TS_DELAY: + case TS_PKT_DELAY: + case TS_PKT_TS_DELAY: + appendTimestamp(index_sop, traceBusId, elem); + break; + case TS_GLOBAL: + appendGlobalTimestamp(index_sop, traceBusId, elem); + break; + } + } catch (...) { + if (!outputError_) { + outputError_ = std::current_exception(); + } + return OCSD_RESP_FATAL_SYS_ERR; + } + return OCSD_RESP_CONT; +} + +void OpenCsdPacketCollector::RawPacketDataMon(const ocsd_datapath_op_t op, const ocsd_trc_index_t index_sop, + const ItmTrcPacket* pkt, const std::uint32_t, const std::uint8_t*) +{ + try { + if (pkt == nullptr) { + return; + } + + // OpenCSD publishes the incomplete packet through the raw monitor as DATA + // while processing EOT; its following EOT monitor notification has no packet. + if (pkt->getPktType() == ITM_PKT_INCOMPLETE_EOT) { + appendDecodeError(index_sop, "incomplete ITM packet at end of input", "opencsd-incomplete-tail", true, + TraceIssueSeverity::Error); + return; + } + if (op != OCSD_OP_DATA) { + return; + } + + switch (pkt->getPktType()) { + case ITM_PKT_ASYNC: + appendSync(index_sop); + break; + case ITM_PKT_OVERFLOW: + appendOverflow(index_sop); + break; + case ITM_PKT_TS_GLOBAL_1: + case ITM_PKT_TS_GLOBAL_2: + // The ITM decoder combines GTS1/GTS2 and publishes the complete + // 64-bit value as a generic TS_GLOBAL element. Raw fragments are + // intentionally not forwarded as independent timestamps. + break; + case ITM_PKT_BAD_SEQUENCE: + case ITM_PKT_RESERVED: + appendError(index_sop, *pkt); + break; + default: + break; + } + } catch (...) { + if (!outputError_) { + outputError_ = std::current_exception(); + } + } +} + +void OpenCsdPacketCollector::appendSync(ocsd_trc_index_t index) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Sync; + element.sourceIndex = static_cast(index); + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::appendOverflow(ocsd_trc_index_t index) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Overflow; + element.sourceIndex = static_cast(index); + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::appendGlobalTimestamp(ocsd_trc_index_t index, std::uint8_t traceBusId, + const OcsdTraceElement& elem) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::GlobalTimestamp; + element.sourceIndex = static_cast(index); + element.traceBusId = traceBusId; + element.timestampValue = elem.timestamp; + element.clockChange = elem.cpu_freq_change != 0U; + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::appendError(ocsd_trc_index_t index, const ItmTrcPacket& pkt) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Error; + element.sourceIndex = static_cast(index); + element.issueCode = "opencsd-decode-error"; + element.errorMessage = pkt.getPktType() == ITM_PKT_RESERVED ? "Reserved ITM packet" : "Bad ITM packet sequence"; + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::appendSoftware(ocsd_trc_index_t index, std::uint8_t traceBusId, + const OcsdTraceElement& elem) +{ + const auto& info = elem.swt_itm; + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Software; + element.sourceIndex = static_cast(index); + element.traceBusId = traceBusId; + element.channel = info.payload_src_id; + element.size = info.payload_size; + element.value = info.value; + element.overflow = info.overflow; + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::appendDwt(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem) +{ + const auto& info = elem.swt_itm; + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Hardware; + element.sourceIndex = static_cast(index); + element.traceBusId = traceBusId; + element.discriminator = info.payload_src_id; + element.size = info.payload_size; + element.value = info.value; + element.overflow = info.overflow; + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::appendTimestamp(ocsd_trc_index_t index, std::uint8_t traceBusId, + const OcsdTraceElement& elem) +{ + const auto& info = elem.swt_itm; + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::LocalTimestamp; + element.sourceIndex = static_cast(index); + element.traceBusId = traceBusId; + element.timestampRelation = timestampRelation(info.pkt_type); + element.tcyc = elem.timestamp; + element.overflow = info.overflow; + appendElement(std::move(element)); +} + +LocalTimestampRelation OpenCsdPacketCollector::timestampRelation(swt_itm_type type) +{ + switch (type) { + case TS_SYNC: + return LocalTimestampRelation::Synchronous; + case TS_DELAY: + return LocalTimestampRelation::TimestampDelayed; + case TS_PKT_DELAY: + return LocalTimestampRelation::PayloadDelayed; + case TS_PKT_TS_DELAY: + return LocalTimestampRelation::TimestampAndPayloadDelayed; + default: + return LocalTimestampRelation::Synchronous; + } +} + +void OpenCsdPacketCollector::appendElement(OpenCsdTraceElement element) +{ + if (transactionActive_) { + transactionElements_.push_back(std::move(element)); + return; + } + appendCommitted(std::move(element)); +} + +void OpenCsdPacketCollector::appendCommitted(OpenCsdTraceElement element) +{ + elementSink_.append(std::move(element)); +} diff --git a/tools/ctrace/src/decode/OpenCsdPacketCollector.hpp b/tools/ctrace/src/decode/OpenCsdPacketCollector.hpp new file mode 100644 index 000000000..5a5cdebb7 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdPacketCollector.hpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "TraceEvent.hpp" +#include "OpenCsdTraceElement.hpp" +#include "common/trc_gen_elem.h" +#include "interfaces/trc_gen_elem_in_i.h" +#include "interfaces/trc_pkt_raw_in_i.h" +#include "opencsd/itm/trc_pkt_elem_itm.h" +#include "opencsd/ocsd_if_types.h" +#include "opencsd/trc_gen_elem_types.h" + +#include +#include +#include +#include +#include + +class OpenCsdPacketCollector : public ITrcGenElemIn, public IPktRawDataMon { +public: + explicit OpenCsdPacketCollector(OpenCsdTraceElementSink& elementSink); + + void beginTransaction(); + void commitTransaction(); + void commitTransactionBefore(std::uint64_t sourceOffset); + void rollbackTransaction(); + void rethrowOutputError(); + std::size_t transactionElementCount() const; + std::optional transactionFirstSourceOffset() const; + void appendDecodeError(ocsd_trc_index_t index, const std::string& message, + const std::string& issueCode = "opencsd-decode-error", bool discontinuity = true, + TraceIssueSeverity severity = TraceIssueSeverity::Error); + void prependDiscontinuity(ocsd_trc_index_t index, const std::string& message, const std::string& issueCode, + std::optional rawBytesConsumed = std::nullopt); + void prependDataLossError(ocsd_trc_index_t index, const std::string& message, std::uint64_t rawBytesConsumed); + ocsd_datapath_resp_t TraceElemIn(ocsd_trc_index_t index_sop, std::uint8_t trc_chan_id, + const OcsdTraceElement& elem) override; + void RawPacketDataMon(ocsd_datapath_op_t op, ocsd_trc_index_t index_sop, const ItmTrcPacket* pkt, std::uint32_t size, + const std::uint8_t* data) override; + +private: + void appendSync(ocsd_trc_index_t index); + void appendOverflow(ocsd_trc_index_t index); + void appendGlobalTimestamp(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem); + void appendError(ocsd_trc_index_t index, const ItmTrcPacket& pkt); + void appendSoftware(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem); + void appendDwt(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem); + void appendTimestamp(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem); + static LocalTimestampRelation timestampRelation(swt_itm_type type); + void appendElement(OpenCsdTraceElement element); + void appendCommitted(OpenCsdTraceElement element); + + OpenCsdTraceElementSink& elementSink_; + bool transactionActive_ = false; + std::vector transactionElements_; + std::exception_ptr outputError_; +}; diff --git a/tools/ctrace/src/decode/OpenCsdTraceElement.hpp b/tools/ctrace/src/decode/OpenCsdTraceElement.hpp new file mode 100644 index 000000000..d76dcb679 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdTraceElement.hpp @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "TraceEvent.hpp" + +#include +#include + +enum class LocalTimestampRelation { + Synchronous, + TimestampDelayed, + PayloadDelayed, + TimestampAndPayloadDelayed, +}; + +struct OpenCsdTraceElement { + enum class Kind { + Software, + Hardware, + LocalTimestamp, + GlobalTimestamp, + Sync, + Overflow, + Discontinuity, + Error, + }; + + Kind kind = Kind::Error; + std::uint64_t sourceIndex = 0; + + // CoreSight Trace Bus ID reported by OpenCSD. ID 0 identifies input for + // which no formatted source ID exists, such as the current SWO path. + std::uint8_t traceBusId = 0U; + std::uint32_t channel = 0; + std::uint32_t discriminator = 0; + std::uint8_t size = 0; + std::uint32_t value = 0; + LocalTimestampRelation timestampRelation = LocalTimestampRelation::Synchronous; + std::uint64_t timestampValue = 0; + std::optional tcyc; + std::optional rawBytesConsumed; + bool overflow = false; + bool discontinuity = false; + bool awaitingResumeTimestamp = false; + + bool clockChange = false; + + std::string issueCode; + TraceIssueSeverity issueSeverity = TraceIssueSeverity::Error; + std::string errorMessage; +}; + +class OpenCsdTraceElementSink { +public: + virtual ~OpenCsdTraceElementSink() = default; + + virtual void append(OpenCsdTraceElement element) = 0; +}; diff --git a/tools/ctrace/src/diagnostics/DiagnosticSink.cpp b/tools/ctrace/src/diagnostics/DiagnosticSink.cpp new file mode 100644 index 000000000..389c8f3cf --- /dev/null +++ b/tools/ctrace/src/diagnostics/DiagnosticSink.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "DiagnosticSink.hpp" + +#include +#include +#include +#include +#include +#include + +static std::string formatDiagnosticEvent(const DiagnosticSink::Event& event) +{ + std::ostringstream out; + out << "[" << (event.impact == DiagnosticSink::Impact::Fatal ? toString(event.impact) : toString(event.severity)) + << "] " << toString(event.category); + if (!event.code.empty()) { + out << "/" << event.code; + } + out << ": " << event.compactMessage.value_or(event.message); + for (const auto& item : event.context) { + out << " " << item.first << "=" << item.second; + } + out << "\n"; + return out.str(); +} + +void DiagnosticSink::report(const Event& event) +{ + if (event.impact == Impact::Fatal) { + ++fatalCount_; + } + write(event); +} + +std::uint64_t DiagnosticSink::fatalCount() const noexcept +{ + return fatalCount_; +} + +bool DiagnosticSink::hasFatalSince(std::uint64_t checkpoint) const noexcept +{ + return fatalCount_ > checkpoint; +} + +std::string_view toString(DiagnosticSink::Severity severity) +{ + switch (severity) { + case DiagnosticSink::Severity::Info: + return "info"; + case DiagnosticSink::Severity::Warning: + return "warning"; + case DiagnosticSink::Severity::Error: + return "error"; + } + return "unknown"; +} + +std::string_view toString(DiagnosticSink::Category category) +{ + switch (category) { + case DiagnosticSink::Category::Cli: + return "cli"; + case DiagnosticSink::Category::Input: + return "input"; + case DiagnosticSink::Category::Decode: + return "decode"; + case DiagnosticSink::Category::Output: + return "output"; + } + return "unknown"; +} + +std::string_view toString(DiagnosticSink::Impact impact) +{ + switch (impact) { + case DiagnosticSink::Impact::NonFatal: + return "nonfatal"; + case DiagnosticSink::Impact::Fatal: + return "fatal"; + } + return "unknown"; +} + +void StderrDiagnosticSink::write(const Event& event) +{ + std::cerr << formatDiagnosticEvent(event); +} diff --git a/tools/ctrace/src/diagnostics/DiagnosticSink.hpp b/tools/ctrace/src/diagnostics/DiagnosticSink.hpp new file mode 100644 index 000000000..a30670b3c --- /dev/null +++ b/tools/ctrace/src/diagnostics/DiagnosticSink.hpp @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +class DiagnosticSink { +public: + enum class Severity { + Info, + Warning, + Error, + }; + + enum class Category { + Cli, + Input, + Decode, + Output, + }; + + enum class Impact { + NonFatal, + Fatal, + }; + + struct Event { + Event() = default; + + Event(Severity eventSeverity, Category eventCategory, std::string eventCode, std::string eventMessage, + std::vector> eventContext = {}, + std::optional eventCompactMessage = std::nullopt, + std::optional eventImpact = std::nullopt) + : severity(eventSeverity), category(eventCategory), code(std::move(eventCode)), message(std::move(eventMessage)), + context(std::move(eventContext)), compactMessage(std::move(eventCompactMessage)), + impact(eventImpact.value_or(eventSeverity == Severity::Error ? Impact::Fatal : Impact::NonFatal)) + { + } + + Severity severity = Severity::Info; + Category category = Category::Cli; + std::string code; + std::string message; + std::vector> context; + std::optional compactMessage; + Impact impact = Impact::NonFatal; + }; + + virtual ~DiagnosticSink() = default; + + void report(const Event& event); + std::uint64_t fatalCount() const noexcept; + bool hasFatalSince(std::uint64_t checkpoint) const noexcept; + +protected: + virtual void write(const Event& event) = 0; + +private: + std::uint64_t fatalCount_ = 0; +}; + +class StderrDiagnosticSink final : public DiagnosticSink { +protected: + void write(const Event& event) override; +}; + +std::string_view toString(DiagnosticSink::Severity severity); +std::string_view toString(DiagnosticSink::Category category); +std::string_view toString(DiagnosticSink::Impact impact); diff --git a/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp b/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp new file mode 100644 index 000000000..08fd66739 --- /dev/null +++ b/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TraceIssueReporter.hpp" + +#include "DiagnosticSink.hpp" +#include "TraceEvent.hpp" + +#include +#include +#include +#include + +namespace { + +std::string atRawOffset(const std::string& message, const TraceEvent& event) +{ + return message + " at raw offset " + std::to_string(event.index); +} + +std::string compactErrorMessage(const TraceEvent& event, const TraceIssueEvent& issue, const std::string& code) +{ + if (code == "data-loss") { + if (issue.rawBytesConsumed.has_value()) { + return std::to_string(*issue.rawBytesConsumed) + " raw bytes from raw offset " + std::to_string(event.index) + + " could not be decoded before the next hardware ITM sync"; + } + return "trace data at raw offset " + std::to_string(event.index) + + " could not be decoded before the next hardware ITM sync"; + } + if (code == "opencsd-bad-packet-sequence") { + return atRawOffset("invalid ITM packet sequence", event); + } + if (code == "opencsd-invalid-packet-header") { + return atRawOffset("invalid ITM packet header", event); + } + if (code == "opencsd-incomplete-tail") { + return "incomplete ITM packet starting at raw offset " + std::to_string(event.index) + " at end of input"; + } + if (code == "opencsd-no-progress") { + return atRawOffset("OpenCSD made no decode progress", event); + } + if (code == "opencsd-wait-timeout") { + return "OpenCSD remained blocked while flushing pending data"; + } + if (code == "opencsd-initialization-error") { + return "OpenCSD initialization failed"; + } + return atRawOffset("trace decode error", event); +} + +} // namespace + +TraceIssueReporter::TraceIssueReporter(DiagnosticSink& diagnostics) : diagnostics_(diagnostics) {} + +void TraceIssueReporter::append(const TraceEvent& event) +{ + if (isTraceEvent(event)) { + reportOverflow(event); + return; + } + if (const auto* issue = traceEventPayload(event)) { + reportError(event, *issue); + } +} + +void TraceIssueReporter::finish() +{ + if (finished_) { + return; + } + finished_ = true; + if (!firstOverflowTimestamp_.has_value()) { + return; + } + + const auto additionalOverflows = overflowPackets_ - 1U; + const auto summary = "first overflow occurred at cycle timestamp " + std::to_string(*firstOverflowTimestamp_) + "; " + + std::to_string(additionalOverflows) + " more occurred"; + report(DiagnosticSink::Severity::Warning, "overflow", + "SWO " + summary + + ". Payload trace was lost at the overflow boundaries, and timestamp continuity after " + "the first overflow may be incomplete or incorrect.", + summary); +} + +void TraceIssueReporter::reportOverflow(const TraceEvent& event) +{ + if (!firstOverflowTimestamp_.has_value()) { + firstOverflowTimestamp_ = event.tcyc.value_or(0U); + } + ++overflowPackets_; +} + +void TraceIssueReporter::reportError(const TraceEvent& event, const TraceIssueEvent& issue) +{ + const auto code = issue.code.empty() ? std::string("decode-error") : issue.code; + if (code == "data-loss") { + report(issue.severity == TraceIssueSeverity::Warning ? DiagnosticSink::Severity::Warning + : DiagnosticSink::Severity::Error, + code, + issue.message.empty() ? "Trace data loss detected while the decoder was not synchronized. Raw bytes were " + "present, but OpenCSD could not turn them into reliable trace packets until a later " + "sync/recovery point." + : issue.message, + compactErrorMessage(event, issue, code)); + return; + } + + report(issue.severity == TraceIssueSeverity::Warning ? DiagnosticSink::Severity::Warning + : DiagnosticSink::Severity::Error, + code, issue.message.empty() ? "decode error detected" : issue.message, + compactErrorMessage(event, issue, code)); +} + +void TraceIssueReporter::report(DiagnosticSink::Severity severity, std::string code, std::string message, + std::optional compactMessage) +{ + diagnostics_.report({ + severity, + DiagnosticSink::Category::Decode, + std::move(code), + std::move(message), + {}, + std::move(compactMessage), + }); +} diff --git a/tools/ctrace/src/diagnostics/TraceIssueReporter.hpp b/tools/ctrace/src/diagnostics/TraceIssueReporter.hpp new file mode 100644 index 000000000..b930ccbe5 --- /dev/null +++ b/tools/ctrace/src/diagnostics/TraceIssueReporter.hpp @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "DiagnosticSink.hpp" +#include "TraceEvent.hpp" + +#include +#include +#include + +class TraceIssueReporter final : public TraceEventSink { +public: + explicit TraceIssueReporter(DiagnosticSink& diagnostics); + + void append(const TraceEvent& event) override; + void finish(); + +private: + void reportOverflow(const TraceEvent& event); + void reportError(const TraceEvent& event, const TraceIssueEvent& issue); + void report(DiagnosticSink::Severity severity, std::string code, std::string message, + std::optional compactMessage = std::nullopt); + + DiagnosticSink& diagnostics_; + std::optional firstOverflowTimestamp_; + std::uint64_t overflowPackets_ = 0; + bool finished_ = false; +}; diff --git a/tools/ctrace/src/main.cpp b/tools/ctrace/src/main.cpp index 4abde435d..c764cf4da 100644 --- a/tools/ctrace/src/main.cpp +++ b/tools/ctrace/src/main.cpp @@ -4,8 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "CtraceMain.h" +#include "CtraceMain.hpp" -int main() { - return CtraceMain(); +int main(int argc, const char* const argv[]) +{ + return CtraceMain(argc, argv); } diff --git a/tools/ctrace/src/model/TraceEvent.hpp b/tools/ctrace/src/model/TraceEvent.hpp new file mode 100644 index 000000000..52315f257 --- /dev/null +++ b/tools/ctrace/src/model/TraceEvent.hpp @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include +#include +#include +#include +#include + +enum class AccessType { + Read, + Write, +}; + +enum class ExceptionAction { + Entered, + Exited, + Returned, + Unknown, +}; + +enum class TraceIssueSeverity { + Warning, + Error, +}; + +struct SoftwareTraceEvent { + std::uint32_t channel = 0; + std::uint8_t size = 0; + std::uint32_t value = 0; +}; + +struct DwtDataTraceEvent { + std::uint32_t comparator = 0; + std::uint8_t size = 0; + std::uint32_t value = 0; + AccessType access = AccessType::Read; + std::optional addressLo16 = std::nullopt; + std::optional pc = std::nullopt; +}; + +struct DwtPcTraceLocation { + std::uint32_t pc; +}; + +struct DwtOffsetTraceLocation { + std::uint32_t addressLo16; +}; + +struct DwtPcAndOffsetTraceLocation { + std::uint32_t pc; + std::uint32_t addressLo16; +}; + +using DwtAddressTraceLocation = std::variant; + +struct DwtAddressTraceEvent { + std::uint32_t comparator; + DwtAddressTraceLocation location; +}; + +inline std::optional dwtAddressPc(const DwtAddressTraceEvent& event) +{ + if (const auto* pc = std::get_if(&event.location)) { + return pc->pc; + } + if (const auto* combined = std::get_if(&event.location)) { + return combined->pc; + } + return std::nullopt; +} + +inline std::optional dwtAddressOffset(const DwtAddressTraceEvent& event) +{ + if (const auto* offset = std::get_if(&event.location)) { + return offset->addressLo16; + } + if (const auto* combined = std::get_if(&event.location)) { + return combined->addressLo16; + } + return std::nullopt; +} + +struct ExceptionTraceEvent { + std::uint32_t number = 0; + ExceptionAction action = ExceptionAction::Unknown; +}; + +struct DwtEventTraceEvent { + std::uint32_t discriminator = 0; + std::uint8_t size = 0; + std::uint32_t value = 0; +}; + +struct PmuTraceEvent { + std::uint32_t discriminator = 0; + std::uint8_t size = 0; + std::uint32_t value = 0; +}; + +struct LocalTimestampTraceEvent {}; + +struct GlobalTimestampTraceEvent { + std::uint64_t value = 0; + bool clockChange = false; +}; + +struct OverflowTraceEvent { + std::string message; +}; + +struct SyncTraceEvent {}; + +struct TraceIssueEvent { + std::string code; + TraceIssueSeverity severity = TraceIssueSeverity::Error; + std::string message; + std::optional rawBytesConsumed = std::nullopt; + std::optional lastValidTcyc = std::nullopt; +}; + +using TraceEventPayload = std::variant; + +struct TraceQuality { + bool overflow = false; + bool timestampReliable = false; + std::uint64_t overflowCount = 0; +}; + +struct TraceEvent { + template explicit TraceEvent(Payload eventPayload) : payload(std::move(eventPayload)) {} + + std::uint64_t index = 0; + // CoreSight Trace Bus ID. ID 0 identifies unformatted single-source input. + std::uint8_t traceBusId = 0U; + std::optional tcyc; + + // Quality is assigned atomically by the post-decoder. Wire/control events + // without reconstructed DWT status leave it absent. + std::optional quality; + + TraceEventPayload payload; +}; + +template const Payload* traceEventPayload(const TraceEvent& event) +{ + return std::get_if(&event.payload); +} + +template Payload* traceEventPayload(TraceEvent& event) +{ + return std::get_if(&event.payload); +} + +template bool isTraceEvent(const TraceEvent& event) +{ + return std::holds_alternative(event.payload); +} + +class TraceEventSink { +public: + virtual ~TraceEventSink() = default; + virtual void append(const TraceEvent& event) = 0; +}; diff --git a/tools/ctrace/src/model/TraceSelection.cpp b/tools/ctrace/src/model/TraceSelection.cpp new file mode 100644 index 000000000..609cbb117 --- /dev/null +++ b/tools/ctrace/src/model/TraceSelection.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TraceSelection.hpp" + +#include "TraceEvent.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +std::optional typeFor(const SoftwareTraceEvent&) +{ + return TraceEventType::Itm; +} + +std::optional typeFor(const DwtDataTraceEvent&) +{ + return TraceEventType::Dwt; +} + +std::optional typeFor(const DwtAddressTraceEvent&) +{ + return TraceEventType::Dwt; +} + +std::optional typeFor(const ExceptionTraceEvent&) +{ + return TraceEventType::Exception; +} + +std::optional typeFor(const DwtEventTraceEvent&) +{ + return std::nullopt; +} + +std::optional typeFor(const PmuTraceEvent&) +{ + return std::nullopt; +} + +std::optional typeFor(const LocalTimestampTraceEvent&) +{ + return std::nullopt; +} + +std::optional typeFor(const GlobalTimestampTraceEvent&) +{ + return TraceEventType::GlobalTimestamp; +} + +std::optional typeFor(const OverflowTraceEvent&) +{ + return TraceEventType::Overflow; +} + +std::optional typeFor(const SyncTraceEvent&) +{ + return std::nullopt; +} + +std::optional typeFor(const TraceIssueEvent&) +{ + return TraceEventType::Error; +} + +} // namespace + +std::string_view traceEventTypeName(TraceEventType type) +{ + return kTraceEventTypeNames[static_cast(type)]; +} + +std::string traceEventTypeList(std::string_view separator) +{ + std::string result; + for (const auto name : kTraceEventTypeNames) { + if (!result.empty()) { + result += separator; + } + result += name; + } + return result; +} + +std::optional parseTraceEventType(std::string_view value) +{ + for (std::size_t index = 0; index < kTraceEventTypeNames.size(); ++index) { + if (kTraceEventTypeNames[index] == value) { + return static_cast(index); + } + } + return std::nullopt; +} + +std::optional traceEventType(const TraceEvent& event) +{ + return std::visit([](const auto& payload) { return typeFor(payload); }, event.payload); +} + +bool TraceSelection::empty() const +{ + return types.empty() && streams.empty(); +} + +bool TraceSelection::includesType(std::string_view type) const +{ + return types.empty() || std::find(types.begin(), types.end(), type) != types.end(); +} + +bool TraceSelection::includesStream(std::uint8_t traceBusId) const +{ + return streams.empty() || std::find(streams.begin(), streams.end(), traceBusId) != streams.end(); +} + +bool traceEventSelectedForOutput(const TraceEvent& event, const TraceSelection& selection) +{ + const auto type = traceEventType(event); + const auto* software = traceEventPayload(event); + if (software != nullptr && software->channel == 0U) { + return false; + } + if (!selection.includesStream(event.traceBusId)) { + return false; + } + return type.has_value() && selection.includesType(traceEventTypeName(*type)); +} diff --git a/tools/ctrace/src/model/TraceSelection.hpp b/tools/ctrace/src/model/TraceSelection.hpp new file mode 100644 index 000000000..c3bc331da --- /dev/null +++ b/tools/ctrace/src/model/TraceSelection.hpp @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +struct TraceEvent; + +enum class TraceEventType : std::size_t { + Itm, + Dwt, + Exception, + GlobalTimestamp, + Overflow, + Error, + Count, +}; + +inline constexpr std::array(TraceEventType::Count)> kTraceEventTypeNames{{ + "itm", + "dwt", + "exception", + "global_ts", + "overflow", + "error", +}}; + +std::string_view traceEventTypeName(TraceEventType type); +std::string traceEventTypeList(std::string_view separator); +std::optional parseTraceEventType(std::string_view value); +std::optional traceEventType(const TraceEvent& event); + +struct TraceSelection { + std::vector types; + std::vector streams; + + bool empty() const; + bool includesType(std::string_view type) const; + bool includesStream(std::uint8_t traceBusId) const; +}; + +bool traceEventSelectedForOutput(const TraceEvent& event, const TraceSelection& selection); diff --git a/tools/ctrace/src/model/TraceStreamId.hpp b/tools/ctrace/src/model/TraceStreamId.hpp new file mode 100644 index 000000000..7f9a81161 --- /dev/null +++ b/tools/ctrace/src/model/TraceStreamId.hpp @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include + +namespace CoreSight { + +inline constexpr std::uint8_t kMinAtbTraceId = 0x01U; +inline constexpr std::uint8_t kMaxAtbTraceId = 0x6fU; +inline constexpr std::uint8_t kUnformattedTraceBusId = 0x00U; + +constexpr bool isAtbTraceId(std::uint32_t value) +{ + return value >= kMinAtbTraceId && value <= kMaxAtbTraceId; +} + +constexpr bool isTraceBusId(std::uint32_t value) +{ + return value <= kMaxAtbTraceId; +} + +} // namespace CoreSight diff --git a/tools/ctrace/src/output/OutputRequirements.cpp b/tools/ctrace/src/output/OutputRequirements.cpp new file mode 100644 index 000000000..c77f36546 --- /dev/null +++ b/tools/ctrace/src/output/OutputRequirements.cpp @@ -0,0 +1,385 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OutputRequirements.hpp" + +#include "CtraceRunMeta.hpp" +#include "ctf/CtfSchema.hpp" +#include "DiagnosticSink.hpp" +#include "TraceSelection.hpp" +#include "TraceOutputConfig.hpp" +#include "TraceRunConfig.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct OutputPaths { + std::filesystem::path csv; + std::filesystem::path ctf; + std::filesystem::path traceCompassXml; +}; + +OutputPaths outputPaths(const std::filesystem::path& rawInputPath) +{ + const auto captureName = rawInputPath.filename().stem(); + const auto solutionSetName = captureName.stem(); + if (captureName.empty() || solutionSetName.empty()) { + throw std::runtime_error("cannot derive trace artifact names from " + rawInputPath.string()); + } + const auto outputDirectory = rawInputPath.parent_path(); + auto csvPath = outputDirectory / captureName; + csvPath += ".csv"; + auto ctfPath = outputDirectory / solutionSetName; + ctfPath += ".ctf"; + auto traceCompassXmlPath = outputDirectory / captureName; + traceCompassXmlPath += ".traceanalysis.xml"; + return { + std::move(csvPath), + std::move(ctfPath), + std::move(traceCompassXmlPath), + }; +} + +bool routeMatchesSelection(const CtraceRunSourceMeta& source, const TraceSelection& selection) +{ + return selection.includesType(source.type) && selection.includesStream(source.traceBusId); +} + +std::vector> +routeContext(std::string_view backend, const CtraceRunMeta& ctraceRunMeta, const CtraceRunSourceMeta& source) +{ + std::vector> context{ + {"backend", std::string(backend)}, + {"channel", std::string(source.type == "itm" ? "ITM" : "DWT") + std::to_string(source.source)}, + {"stream", std::to_string(source.traceBusId)}, + }; + if (!ctraceRunMeta.configPath().empty()) { + context.emplace_back("config", ctraceRunMeta.configPath()); + } + return context; +} + +void reportRequirementError(DiagnosticSink& diagnostics, std::string code, std::string message, + std::vector> context) +{ + diagnostics.report({ + DiagnosticSink::Severity::Error, + DiagnosticSink::Category::Output, + std::move(code), + std::move(message), + std::move(context), + }); +} + +bool validateCtfRouteIdentity(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, + DiagnosticSink& diagnostics) +{ + bool valid = true; + std::map, const CtraceRunSourceMeta*> routes; + std::set> reported; + for (const auto& source : ctraceRunMeta.sources()) { + if (!routeMatchesSelection(source, selection)) { + continue; + } + const auto key = std::make_pair(source.type, source.source); + const auto [found, inserted] = routes.emplace(key, &source); + if (inserted) { + continue; + } + const auto& first = *found->second; + const auto sameMetadata = first.label == source.label && first.symbolAddress == source.symbolAddress && + first.valueType == source.valueType && first.valueSize == source.valueSize && + first.symbolTypeError == source.symbolTypeError && + first.symbolSizeError == source.symbolSizeError; + const auto indistinguishableProcessors = + first.traceBusId == source.traceBusId && first.processorName != source.processorName; + if ((sameMetadata && !indistinguishableProcessors) || !reported.insert(key).second) { + continue; + } + + valid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("type", source.type); + context.emplace_back("firstProcessor", first.processorName.value_or("")); + context.emplace_back("otherProcessor", source.processorName.value_or("")); + context.emplace_back("firstStream", std::to_string(first.traceBusId)); + reportRequirementError( + diagnostics, "ctf-trace-route-ambiguous", + "CTF metadata cannot describe conflicting active type/source routes from different processors or Trace Bus IDs", + std::move(context)); + } + return valid; +} + +std::optional resolveCtfClock(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, + DiagnosticSink& diagnostics) +{ + std::vector> routes; + for (const auto& [traceBusId, timestamp] : ctraceRunMeta.timestampsByTraceBusId()) { + if (!selection.includesStream(traceBusId)) { + continue; + } + routes.emplace_back(traceBusId, timestamp); + } + + std::optional clockHz; + bool valid = true; + for (const auto& [traceBusId, timestamp] : routes) { + if (timestamp.clockError.has_value()) { + valid = false; + reportRequirementError(diagnostics, "ctf-timestamp-clock-invalid", + "CTF output cannot use the configured timestamps.clock", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + {"stream", std::to_string(traceBusId)}, + {"pname", timestamp.processorName.value_or("")}, + {"error", *timestamp.clockError}, + }); + continue; + } + if (!timestamp.clockHz.has_value()) { + valid = false; + reportRequirementError(diagnostics, "ctf-timestamp-clock-missing", + "CTF output requires timestamps.clock for the processor assigned to this Trace Bus ID", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + {"stream", std::to_string(traceBusId)}, + {"pname", timestamp.processorName.value_or("")}, + }); + continue; + } + if (*timestamp.clockHz == 0U) { + valid = false; + reportRequirementError(diagnostics, "ctf-timestamp-clock-invalid", + "CTF output requires timestamps.clock to be greater than zero", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + {"stream", std::to_string(traceBusId)}, + {"pname", timestamp.processorName.value_or("")}, + }); + continue; + } + if (clockHz.has_value() && *clockHz != *timestamp.clockHz) { + valid = false; + reportRequirementError(diagnostics, "ctf-timestamp-clock-ambiguous", + "CTF output cannot combine selected Trace Bus IDs with different timestamps.clock values", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + {"stream", std::to_string(traceBusId)}, + {"clock", std::to_string(*timestamp.clockHz)}, + }); + continue; + } + clockHz = timestamp.clockHz; + } + if (!valid) { + return std::nullopt; + } + if (clockHz.has_value()) { + return clockHz; + } + + for (const auto& error : ctraceRunMeta.timestampClockErrors()) { + reportRequirementError(diagnostics, "ctf-timestamp-clock-invalid", + "CTF output cannot use the configured timestamps.clock", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + {"error", error}, + }); + } + if (!ctraceRunMeta.timestampClockErrors().empty()) { + return std::nullopt; + } + if (!ctraceRunMeta.timestampClockHz().has_value()) { + if (!selection.streams.empty() && !ctraceRunMeta.timestampsByTraceBusId().empty()) { + reportRequirementError(diagnostics, "ctf-timestamp-clock-ambiguous", + "CTF output cannot assign unformatted or unknown Trace Bus IDs to processors with " + "different timestamps.clock values", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + }); + return std::nullopt; + } + reportRequirementError(diagnostics, "ctf-timestamp-clock-missing", + "CTF output requires timestamps.clock from an active ctrace-setup; no default is assumed", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + }); + return std::nullopt; + } + if (*ctraceRunMeta.timestampClockHz() == 0U) { + reportRequirementError(diagnostics, "ctf-timestamp-clock-invalid", + "CTF output requires timestamps.clock to be greater than zero", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + }); + return std::nullopt; + } + return ctraceRunMeta.timestampClockHz(); +} + +bool validateCtfDwtMetadata(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, + DiagnosticSink& diagnostics) +{ + bool valid = true; + for (const auto& source : ctraceRunMeta.sources()) { + if (source.type != "dwt" || !routeMatchesSelection(source, selection)) { + continue; + } + bool sourceValid = true; + if (source.symbolTypeError.has_value()) { + valid = false; + sourceValid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("error", *source.symbolTypeError); + reportRequirementError(diagnostics, "ctf-dwt-symbol-type-invalid", + "CTF output cannot use the configured ctrace-run data.symbol-type", std::move(context)); + } + if (source.symbolSizeError.has_value()) { + valid = false; + sourceValid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("error", *source.symbolSizeError); + reportRequirementError(diagnostics, "ctf-dwt-symbol-size-invalid", + "CTF output cannot use the configured ctrace-run data.symbol-size", std::move(context)); + } + if (!sourceValid) { + continue; + } + const auto validType = TraceRunSchema::isDwtDataType(source.valueType); + const auto* valueVariant = CtfSchema::valueVariantForTraceRunType(source.valueType, source.valueSize); + if (!validType) { + valid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("dataType", source.valueType); + reportRequirementError(diagnostics, "ctf-dwt-symbol-type-invalid", + "CTF output cannot use ctrace-run data.symbol-type '" + source.valueType + "'; " + + std::string(CtfSchema::ValueTypeRequirements), + std::move(context)); + } + if (!TraceRunSchema::isDwtDataSize(source.valueSize) || (validType && valueVariant == nullptr)) { + valid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("dataType", source.valueType); + context.emplace_back("dataSize", std::to_string(source.valueSize)); + reportRequirementError(diagnostics, "ctf-dwt-symbol-size-invalid", + "CTF output cannot use ctrace-run data.symbol-size " + std::to_string(source.valueSize) + + " with data.symbol-type '" + source.valueType + "'; " + + std::string(CtfSchema::ValueTypeRequirements), + std::move(context)); + } + } + return valid; +} + +std::optional> +resolveCtfSources(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, DiagnosticSink& diagnostics) +{ + bool valid = true; + std::set> resolvedKeys; + std::vector sources; + for (const auto& route : ctraceRunMeta.sources()) { + if ((route.type != "itm" && route.type != "dwt") || (route.type == "itm" && route.source == 0U) || + !routeMatchesSelection(route, selection) || + !resolvedKeys.emplace(route.type, route.source, route.traceBusId).second) { + continue; + } + + try { + const auto* source = + ctraceRunMeta.resolveSource(route.type, std::optional(route.traceBusId), route.source); + if (source == nullptr) { + continue; + } + sources.push_back({ + source->type, + source->source, + source->traceBusId, + source->label, + source->symbolAddress, + source->valueType, + static_cast(source->valueSize), + }); + } catch (const std::exception& error) { + valid = false; + auto context = routeContext("ctf", ctraceRunMeta, route); + context.emplace_back("type", route.type); + context.emplace_back("error", error.what()); + reportRequirementError( + diagnostics, "ctf-trace-route-ambiguous", + "CTF output cannot combine conflicting metadata for the same trace route across active streams", + std::move(context)); + } + } + return valid ? std::optional>(std::move(sources)) : std::nullopt; +} + +} // namespace + +bool TraceOutputPlan::hasRequestedOutputs() const +{ + return csvRequested || ctfRequested; +} + +bool TraceOutputPlan::hasEnabledOutputs() const +{ + return csv.has_value() || ctf.has_value(); +} + +TraceOutputPlan planTraceOutputs(const TraceOutputRequest& request, const std::filesystem::path& rawInputPath, + const CtraceRunMeta& ctraceRunMeta, DiagnosticSink& diagnostics) +{ + TraceOutputPlan plan; + plan.csvRequested = request.csv; + plan.ctfRequested = request.ctf; + if (!plan.hasRequestedOutputs()) { + return plan; + } + + const auto paths = outputPaths(rawInputPath); + if (plan.csvRequested) { + plan.csv = CsvOutputConfig{ + paths.csv, + request.selection, + }; + } + if (plan.ctfRequested) { + auto clock = resolveCtfClock(ctraceRunMeta, request.selection, diagnostics); + const auto validRoutes = validateCtfRouteIdentity(ctraceRunMeta, request.selection, diagnostics); + const auto validTypes = validateCtfDwtMetadata(ctraceRunMeta, request.selection, diagnostics); + auto sources = clock.has_value() && validRoutes && validTypes + ? resolveCtfSources(ctraceRunMeta, request.selection, diagnostics) + : std::nullopt; + if (clock.has_value() && validRoutes && validTypes && sources.has_value()) { + plan.ctf = CtfOutputConfig{ + paths.ctf, paths.traceCompassXml, *clock, request.selection, std::move(*sources), + }; + } + } + return plan; +} diff --git a/tools/ctrace/src/output/OutputRequirements.hpp b/tools/ctrace/src/output/OutputRequirements.hpp new file mode 100644 index 000000000..a331e1845 --- /dev/null +++ b/tools/ctrace/src/output/OutputRequirements.hpp @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "TraceOutputConfig.hpp" + +#include +#include + +class CtraceRunMeta; +class DiagnosticSink; + +struct TraceOutputPlan { + bool csvRequested = false; + bool ctfRequested = false; + std::optional csv; + std::optional ctf; + + bool hasRequestedOutputs() const; + bool hasEnabledOutputs() const; +}; + +TraceOutputPlan planTraceOutputs(const TraceOutputRequest& request, const std::filesystem::path& rawInputPath, + const CtraceRunMeta& ctraceRunMeta, DiagnosticSink& diagnostics); diff --git a/tools/ctrace/src/output/TraceOutput.hpp b/tools/ctrace/src/output/TraceOutput.hpp new file mode 100644 index 000000000..bc9c5bb6c --- /dev/null +++ b/tools/ctrace/src/output/TraceOutput.hpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "TraceEvent.hpp" + +#include +#include + +class TraceOutput { +public: + virtual ~TraceOutput() = default; + + virtual std::string_view backendName() const noexcept + { + return "trace"; + } + virtual std::string targetPath() const + { + return {}; + } + + // start() prepares the final target, stop() completes it, and abort() discards + // an incomplete active output. TraceOutputLifecycle owns this call order. + virtual void start() {} + virtual void stop() {} + virtual void abort() = 0; + // Called synchronously in decode order. + virtual void writeEvent(const TraceEvent& event) = 0; +}; diff --git a/tools/ctrace/src/output/TraceOutputConfig.hpp b/tools/ctrace/src/output/TraceOutputConfig.hpp new file mode 100644 index 000000000..1a4877a31 --- /dev/null +++ b/tools/ctrace/src/output/TraceOutputConfig.hpp @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "TraceSelection.hpp" + +#include +#include +#include +#include +#include +#include + +struct TraceOutputRequest { + bool csv = false; + bool ctf = false; + TraceSelection selection; +}; + +struct ResolvedTraceSource { + std::string type; + std::uint32_t source = 0; + std::uint8_t traceBusId = 0U; + std::optional label; + std::optional symbolAddress; + std::string valueType = "unsigned int"; + std::uint8_t valueSize = 4U; +}; + +struct CsvOutputConfig { + std::filesystem::path outputPath; + TraceSelection selection; +}; + +struct CtfOutputConfig { + CtfOutputConfig(std::filesystem::path outputDirectory, std::filesystem::path traceCompassXmlPath, + std::uint64_t clockHz, TraceSelection selection, std::vector sources) + : outputDirectory(std::move(outputDirectory)), traceCompassXmlPath(std::move(traceCompassXmlPath)), + coreClockHz(clockHz), selection(std::move(selection)), sources(std::move(sources)) + { + } + + std::filesystem::path outputDirectory; + std::filesystem::path traceCompassXmlPath; + std::uint64_t coreClockHz = 0; + TraceSelection selection; + std::vector sources; +}; diff --git a/tools/ctrace/src/output/TraceOutputLifecycle.cpp b/tools/ctrace/src/output/TraceOutputLifecycle.cpp new file mode 100644 index 000000000..23685165f --- /dev/null +++ b/tools/ctrace/src/output/TraceOutputLifecycle.cpp @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TraceOutputLifecycle.hpp" + +#include "DiagnosticSink.hpp" +#include "TraceEvent.hpp" +#include "TraceOutput.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +std::string exceptionMessage(const std::exception_ptr& error) +{ + try { + if (error) { + std::rethrow_exception(error); + } + } catch (const std::exception& ex) { + return ex.what(); + } catch (...) { + // Preserve noexcept error reporting even for non-standard exceptions. + (void)0; + } + return "unknown exception"; +} + +} // namespace + +TraceOutputLifecycle::TraceOutputLifecycle(std::vector> outputs, + DiagnosticSink& diagnostics) + : outputs_(std::move(outputs)), diagnostics_(diagnostics), states_(outputs_.size(), State::Inactive) +{ + for (std::size_t index = 0; index < outputs_.size(); ++index) { + try { + outputs_[index]->start(); + states_[index] = State::Active; + } catch (...) { + fail(index, "start", std::current_exception()); + abortNoexcept(index); + } + } +} + +TraceOutputLifecycle::~TraceOutputLifecycle() noexcept +{ + abortActiveNoexcept(); +} + +void TraceOutputLifecycle::append(const TraceEvent& event) +{ + for (std::size_t index = 0; index < outputs_.size(); ++index) { + if (states_[index] != State::Active) { + continue; + } + try { + outputs_[index]->writeEvent(event); + } catch (...) { + fail(index, "write", std::current_exception()); + abortNoexcept(index); + } + } +} + +void TraceOutputLifecycle::abort() noexcept +{ + abortActiveNoexcept(); +} + +void TraceOutputLifecycle::finish() noexcept +{ + if (finished_) { + return; + } + + for (std::size_t index = outputs_.size(); index > 0U; --index) { + const auto outputIndex = index - 1U; + if (states_[outputIndex] != State::Active) { + continue; + } + try { + outputs_[outputIndex]->stop(); + states_[outputIndex] = State::Completed; + } catch (...) { + fail(outputIndex, "stop", std::current_exception()); + abortNoexcept(outputIndex); + } + } + finished_ = true; +} + +void TraceOutputLifecycle::fail(std::size_t index, const char* phase, const std::exception_ptr& error) noexcept +{ + states_[index] = State::Failed; + try { + const auto message = exceptionMessage(error); + const auto backend = std::string(outputs_[index]->backendName()); + const auto target = outputs_[index]->targetPath(); + diagnostics_.report({ + DiagnosticSink::Severity::Error, + DiagnosticSink::Category::Output, + "output-failed", + "trace output failed", + { + {"outputIndex", std::to_string(index)}, + {"backend", backend}, + {"path", target}, + {"phase", phase}, + {"error", message}, + }, + backend + " output" + (target.empty() ? std::string() : " '" + target + "'") + " failed during " + phase + + ": " + message, + }); + } catch (...) { + // Diagnostics must never escape this noexcept failure path. + (void)0; + } +} + +void TraceOutputLifecycle::abortNoexcept(std::size_t index) noexcept +{ + try { + outputs_[index]->abort(); + } catch (...) { + fail(index, "abort", std::current_exception()); + } + states_[index] = State::Failed; +} + +void TraceOutputLifecycle::abortActiveNoexcept() noexcept +{ + for (std::size_t index = 0; index < outputs_.size(); ++index) { + if (states_[index] == State::Active) { + abortNoexcept(index); + } + } + finished_ = true; +} diff --git a/tools/ctrace/src/output/TraceOutputLifecycle.hpp b/tools/ctrace/src/output/TraceOutputLifecycle.hpp new file mode 100644 index 000000000..f1cd1d8d7 --- /dev/null +++ b/tools/ctrace/src/output/TraceOutputLifecycle.hpp @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "DiagnosticSink.hpp" +#include "TraceEvent.hpp" +#include "TraceOutput.hpp" + +#include +#include +#include +#include + +class TraceOutputLifecycle final : public TraceEventSink { +public: + explicit TraceOutputLifecycle(std::vector> outputs, DiagnosticSink& diagnostics); + ~TraceOutputLifecycle() noexcept; + + void append(const TraceEvent& event) override; + void finish() noexcept; + void abort() noexcept; + + TraceOutputLifecycle(const TraceOutputLifecycle&) = delete; + TraceOutputLifecycle& operator=(const TraceOutputLifecycle&) = delete; + +private: + enum class State { + Inactive, + Active, + Completed, + Failed, + }; + + void fail(std::size_t index, const char* phase, const std::exception_ptr& error) noexcept; + void abortNoexcept(std::size_t index) noexcept; + void abortActiveNoexcept() noexcept; + + std::vector> outputs_; + DiagnosticSink& diagnostics_; + std::vector states_; + bool finished_ = false; +}; diff --git a/tools/ctrace/src/output/csv/CsvFileOutput.cpp b/tools/ctrace/src/output/csv/CsvFileOutput.cpp new file mode 100644 index 000000000..ecd492091 --- /dev/null +++ b/tools/ctrace/src/output/csv/CsvFileOutput.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CsvFileOutput.hpp" + +#include "CsvRowMapper.hpp" +#include "TraceEvent.hpp" +#include "TraceSelection.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void removeExistingCsv(const std::filesystem::path& path) +{ + const auto normalized = path.lexically_normal(); + if (path.empty() || normalized == normalized.root_path() || normalized.filename().empty() || + normalized.filename() == "." || normalized.filename() == "..") { + throw std::invalid_argument("CSV output path must identify a file"); + } + 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 CSV output " + path.string() + ": " + error.message()); + } + if (!error && std::filesystem::is_directory(status)) { + throw std::runtime_error("Refusing to replace CSV output because the target is a directory: " + path.string()); + } + error.clear(); + std::filesystem::remove(path, error); + if (error) { + throw std::runtime_error("Failed to remove existing CSV output " + path.string() + ": " + error.message()); + } +} + +void createParentDirectory(const std::filesystem::path& path) +{ + const auto parent = path.parent_path(); + if (parent.empty()) { + return; + } + std::error_code error; + std::filesystem::create_directories(parent, error); + if (error) { + throw std::runtime_error("Failed to create CSV output directory " + parent.string() + ": " + error.message()); + } +} + +} // namespace + +CsvFileOutput::CsvFileOutput(std::filesystem::path outputFile, TraceSelection selection) + : outputFile_(std::move(outputFile)), selection_(std::move(selection)) +{ +} + +CsvFileOutput::~CsvFileOutput() +{ + try { + abort(); + } catch (...) { + // Destructors cannot report a best-effort cleanup failure. + (void)0; + } +} + +std::string_view CsvFileOutput::backendName() const noexcept +{ + return "csv"; +} + +std::string CsvFileOutput::targetPath() const +{ + return outputFile_.string(); +} + +void CsvFileOutput::start() +{ + abort(); + const auto& outputPath = outputFile_; + removeExistingCsv(outputPath); + createParentDirectory(outputPath); + active_ = true; + stream_.clear(); + stream_.open(outputPath, std::ios::out | std::ios::trunc); + if (!stream_.is_open()) { + abort(); + throw std::runtime_error("Failed to open CSV output " + outputPath.string()); + } + + stream_ << CsvRowMapper::header() << "\n"; + if (!stream_) { + abort(); + throw std::runtime_error("Failed to write CSV output " + outputFile_.string()); + } +} + +void CsvFileOutput::stop() +{ + if (stream_.is_open()) { + stream_.close(); + } + if (!stream_) { + abort(); + throw std::runtime_error("Failed to write CSV output " + outputFile_.string()); + } + active_ = false; +} + +void CsvFileOutput::abort() +{ + if (stream_.is_open()) { + stream_.close(); + } + stream_.clear(); + if (active_) { + removeExistingCsv(outputFile_); + active_ = false; + } +} + +void CsvFileOutput::writeEvent(const TraceEvent& event) +{ + if (!stream_.is_open()) { + return; + } + if (!traceEventSelectedForOutput(event, selection_)) { + return; + } + + stream_ << CsvRowMapper::row(event) << "\n"; +} diff --git a/tools/ctrace/src/output/csv/CsvFileOutput.hpp b/tools/ctrace/src/output/csv/CsvFileOutput.hpp new file mode 100644 index 000000000..7962ffe62 --- /dev/null +++ b/tools/ctrace/src/output/csv/CsvFileOutput.hpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "TraceEvent.hpp" +#include "TraceSelection.hpp" +#include "TraceOutput.hpp" + +#include +#include +#include + +class CsvFileOutput final : public TraceOutput { +public: + explicit CsvFileOutput(std::filesystem::path outputFile, TraceSelection selection = {}); + ~CsvFileOutput() override; + + void start() override; + void stop() override; + void abort() override; + void writeEvent(const TraceEvent& event) override; + std::string_view backendName() const noexcept override; + std::string targetPath() const override; + +private: + std::filesystem::path outputFile_; + TraceSelection selection_; + std::ofstream stream_; + bool active_ = false; +}; diff --git a/tools/ctrace/src/output/csv/CsvRowMapper.cpp b/tools/ctrace/src/output/csv/CsvRowMapper.cpp new file mode 100644 index 000000000..87319736f --- /dev/null +++ b/tools/ctrace/src/output/csv/CsvRowMapper.cpp @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CsvRowMapper.hpp" + +#include "TraceEvent.hpp" +#include "TraceSelection.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +enum class CsvColumn : std::size_t { + Cycles, + Stream, + Type, + Source, + Value, + Pc, + Offset, + Note, + Count, +}; + +constexpr std::array(CsvColumn::Count)> kCsvColumnNames{{ + "cycles", + "stream", + "type", + "source", + "value", + "pc", + "offset", + "note", +}}; + +using CsvRow = std::array(CsvColumn::Count)>; + +template std::string joinColumns(const Columns& columns) +{ + std::ostringstream out; + for (std::size_t index = 0; index < columns.size(); ++index) { + if (index != 0U) { + out << ","; + } + out << columns[index]; + } + return out.str(); +} + +std::size_t column(CsvColumn value) +{ + return static_cast(value); +} + +std::string escapeCsvField(const std::string& value) +{ + if (value.find_first_of("\",\r\n") == std::string::npos) { + return value; + } + std::string escaped = "\""; + for (const auto ch : value) { + if (ch == '"') { + escaped += "\"\""; + } else { + escaped += ch; + } + } + escaped += "\""; + return escaped; +} + +std::string renderCsvRow(const CsvRow& fields) +{ + std::ostringstream out; + for (std::size_t index = 0; index < fields.size(); ++index) { + if (index != 0U) { + out << ","; + } + out << escapeCsvField(fields[index]); + } + return out.str(); +} + +std::string hexValue(std::uint64_t value, std::uint32_t widthBytes) +{ + static constexpr char digits[] = "0123456789abcdef"; + const auto width = std::max(2U, widthBytes * 2U); + std::string out(width, '0'); + for (std::uint32_t index = 0; index < width; ++index) { + out[width - 1U - index] = digits[value & 0x0fU]; + value >>= 4U; + } + return "0x" + out; +} + +std::string_view exceptionActionCsvValue(ExceptionAction action) +{ + switch (action) { + case ExceptionAction::Entered: + return "0x1"; + case ExceptionAction::Exited: + return "0x2"; + case ExceptionAction::Returned: + return "0x3"; + case ExceptionAction::Unknown: + return "0x0"; + } + return "0x0"; +} + +CsvRow eventToCsvRow(const TraceEvent& event) +{ + CsvRow row{}; + if (event.tcyc.has_value()) { + row[column(CsvColumn::Cycles)] = std::to_string(*event.tcyc); + } + row[column(CsvColumn::Stream)] = std::to_string(event.traceBusId); + if (const auto type = traceEventType(event)) { + row[column(CsvColumn::Type)] = traceEventTypeName(*type); + } + + if (const auto* software = traceEventPayload(event)) { + row[column(CsvColumn::Source)] = std::to_string(software->channel); + row[column(CsvColumn::Value)] = hexValue(software->value, software->size); + } else if (const auto* data = traceEventPayload(event)) { + row[column(CsvColumn::Source)] = std::to_string(data->comparator); + row[column(CsvColumn::Value)] = hexValue(data->value, data->size); + if (data->pc.has_value()) { + row[column(CsvColumn::Pc)] = hexValue(*data->pc, 4); + } + if (data->addressLo16.has_value()) { + row[column(CsvColumn::Offset)] = hexValue(*data->addressLo16, 2); + } + } else if (const auto* address = traceEventPayload(event)) { + row[column(CsvColumn::Source)] = std::to_string(address->comparator); + if (const auto pc = dwtAddressPc(*address)) { + row[column(CsvColumn::Pc)] = hexValue(*pc, 4); + } + if (const auto offset = dwtAddressOffset(*address)) { + row[column(CsvColumn::Offset)] = hexValue(*offset, 2); + } + } else if (const auto* exception = traceEventPayload(event)) { + row[column(CsvColumn::Source)] = std::to_string(exception->number); + if (exception->action != ExceptionAction::Unknown) { + row[column(CsvColumn::Value)] = exceptionActionCsvValue(exception->action); + } + } else if (const auto* timestamp = traceEventPayload(event)) { + row[column(CsvColumn::Cycles)] = std::to_string(timestamp->value); + } else if (const auto* overflow = traceEventPayload(event)) { + row[column(CsvColumn::Note)] = overflow->message.empty() + ? "overflow: new timestamp segment; time across boundary may be unreliable" + : overflow->message; + } else if (const auto* issue = traceEventPayload(event)) { + row[column(CsvColumn::Note)] = issue->message; + } + + return row; +} + +} // namespace + +std::string CsvRowMapper::header() +{ + return joinColumns(kCsvColumnNames); +} + +std::string CsvRowMapper::row(const TraceEvent& event) +{ + return renderCsvRow(eventToCsvRow(event)); +} diff --git a/tools/ctrace/src/output/csv/CsvRowMapper.hpp b/tools/ctrace/src/output/csv/CsvRowMapper.hpp new file mode 100644 index 000000000..396903a9c --- /dev/null +++ b/tools/ctrace/src/output/csv/CsvRowMapper.hpp @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include + +struct TraceEvent; + +namespace CsvRowMapper { + +std::string header(); +std::string row(const TraceEvent& event); + +} // namespace CsvRowMapper diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp new file mode 100644 index 000000000..76df7e34f --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfBundleOutput.hpp" + +#include "CtfEncoder.hpp" +#include "TraceCompassXmlWriter.hpp" +#include "TraceEvent.hpp" +#include "TraceOutputConfig.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#endif + +namespace { + +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"); + } +} + +std::filesystem::path normalizedAbsolutePath(const std::filesystem::path& path) +{ + std::error_code error; + const auto absolute = std::filesystem::absolute(path, error); + return (error ? path : absolute).lexically_normal(); +} + +bool pathComponentEquals(const std::filesystem::path& lhs, const std::filesystem::path& rhs) +{ +#ifdef _WIN32 + const auto& lhsNative = lhs.native(); + const auto& rhsNative = rhs.native(); + return lhsNative.size() == rhsNative.size() && + std::equal(lhsNative.begin(), lhsNative.end(), rhsNative.begin(), + [](wchar_t lhsCharacter, wchar_t rhsCharacter) { + return std::towlower(lhsCharacter) == std::towlower(rhsCharacter); + }); +#else + return lhs == rhs; +#endif +} + +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; +} + +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"); + } +} + +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()); + } +} + +void validateExistingOutputTypes(const std::filesystem::path& ctfDirectory, + const std::filesystem::path& traceCompassXml) +{ + 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)) { + 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()); + } +} + +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()); + } +} + +void createOutputDirectory(const std::filesystem::path& path) +{ + std::error_code error; + std::filesystem::create_directories(path, error); + if (error) { + throw std::runtime_error("Failed to create CTF output directory " + path.string() + ": " + error.message()); + } +} + +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()); + } +} + +} // namespace + +CtfBundleOutput::CtfBundleOutput(CtfOutputConfig config, DiagnosticSink* diagnostics) + : ctfOutputDirectory_(std::move(config.outputDirectory)), traceCompassXmlPath_(std::move(config.traceCompassXmlPath)), + encoder_(CtfEncoderConfig{ + config.coreClockHz, + std::move(config.selection), + std::move(config.sources), + diagnostics, + }) +{ + validateOutputTargets(ctfOutputDirectory_, traceCompassXmlPath_); +} + +CtfBundleOutput::~CtfBundleOutput() +{ + try { + abort(); + } catch (...) { + // Destructors cannot report a best-effort cleanup failure. + (void)0; + } +} + +std::string_view CtfBundleOutput::backendName() const noexcept +{ + return "ctf"; +} + +std::string CtfBundleOutput::targetPath() const +{ + return ctfOutputDirectory_.string(); +} + +void CtfBundleOutput::start() +{ + abort(); + validateExistingOutputTypes(ctfOutputDirectory_, traceCompassXmlPath_); + removeOutputDirectory(ctfOutputDirectory_); + removeOutputFile(traceCompassXmlPath_); + createOutputDirectory(ctfOutputDirectory_); + active_ = true; + try { + encoder_.start(ctfOutputDirectory_); + TraceCompassXmlWriter::writeFile(traceCompassXmlPath_); + } catch (...) { + abort(); + throw; + } +} + +void CtfBundleOutput::stop() +{ + if (!active_) { + return; + } + try { + encoder_.stop(); + active_ = false; + } catch (...) { + abort(); + throw; + } +} + +void CtfBundleOutput::abort() +{ + encoder_.abort(); + if (active_) { + removeIncompleteOutputs(ctfOutputDirectory_, traceCompassXmlPath_); + active_ = false; + } +} + +void CtfBundleOutput::writeEvent(const TraceEvent& event) +{ + encoder_.writeEvent(event); +} diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.hpp b/tools/ctrace/src/output/ctf/CtfBundleOutput.hpp new file mode 100644 index 000000000..fde4ffdf9 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.hpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "CtfEncoder.hpp" +#include "TraceEvent.hpp" +#include "TraceOutput.hpp" +#include "TraceOutputConfig.hpp" + +#include + +class DiagnosticSink; + +class CtfBundleOutput final : public TraceOutput { +public: + explicit CtfBundleOutput(CtfOutputConfig config, DiagnosticSink* diagnostics = nullptr); + ~CtfBundleOutput() override; + + void start() override; + void stop() override; + void abort() override; + void writeEvent(const TraceEvent& event) override; + std::string_view backendName() const noexcept override; + std::string targetPath() const override; + +private: + std::filesystem::path ctfOutputDirectory_; + std::filesystem::path traceCompassXmlPath_; + CtfEncoder encoder_; + bool active_ = false; +}; diff --git a/tools/ctrace/src/output/ctf/CtfEncoder.cpp b/tools/ctrace/src/output/ctf/CtfEncoder.cpp new file mode 100644 index 000000000..52524eb71 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfEncoder.cpp @@ -0,0 +1,449 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfEncoder.hpp" + +#include "CtfExceptionLaneTracker.hpp" +#include "CtfMetadataWriter.hpp" +#include "CtfSchema.hpp" +#include "CtfStreamWriter.hpp" +#include "DiagnosticSink.hpp" +#include "TraceEvent.hpp" +#include "TraceOutputConfig.hpp" +#include "TraceSelection.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::uint32_t ctfOverflowCount(std::uint64_t count) +{ + return static_cast(std::min(count, std::numeric_limits::max())); +} +std::uint64_t holdMonotonicTimestamp(std::uint64_t timestamp, std::optional& lastTimestamp) +{ + if (lastTimestamp.has_value() && timestamp < *lastTimestamp) { + timestamp = *lastTimestamp; + } + lastTimestamp = timestamp; + return timestamp; +} + +const CtfSchema::ValueVariant& dwtValueVariant(const ResolvedTraceSource* source, std::uint32_t comparator) +{ + static const ResolvedTraceSource defaults; + const auto& resolved = source != nullptr ? *source : defaults; + const auto* variant = CtfSchema::valueVariantForTraceRunType(resolved.valueType, resolved.valueSize); + if (variant == nullptr) { + throw std::runtime_error("CTF DWT value for comparator " + std::to_string(comparator) + + " has invalid ctrace-run data.symbol-type/data.symbol-size metadata"); + } + return *variant; +} + +const ResolvedTraceSource* resolvedTraceSource(const CtfEncoderConfig& config, const char* type, + std::uint8_t traceBusId, std::uint32_t source) +{ + const auto exact = + std::find_if(config.sources.begin(), config.sources.end(), [&](const ResolvedTraceSource& candidate) { + return candidate.type == type && candidate.source == source && candidate.traceBusId == traceBusId; + }); + if (exact != config.sources.end() || traceBusId != 0U) { + return exact == config.sources.end() ? nullptr : &*exact; + } + + const ResolvedTraceSource* unique = nullptr; + for (const auto& candidate : config.sources) { + if (candidate.type != type || candidate.source != source) { + continue; + } + if (unique != nullptr) { + return nullptr; + } + unique = &candidate; + } + return unique; +} + +std::uint32_t signExtendSample(std::uint32_t value, std::uint8_t sourceBytes) +{ + if (sourceBytes >= 4U) { + return value; + } + const auto bits = sourceBytes * 8U; + const auto signBit = 1U << (bits - 1U); + const auto mask = (signBit << 1U) - 1U; + const auto v = value & mask; + return (v ^ signBit) - signBit; +} + +void writeVariantValue(CtfStreamWriter::Record& record, std::uint32_t data, std::uint8_t sourceSize, + const CtfSchema::ValueVariant& info) +{ + if (info.floatingPoint) { + record.writeU32(data); + return; + } + const auto value = info.signedInteger ? signExtendSample(data, sourceSize) : data; + if (info.byteSize == 1U) { + record.writeU8(static_cast(value & 0xffU)); + } else if (info.byteSize == 2U) { + record.writeU16(static_cast(value & 0xffffU)); + } else { + record.writeU32(value); + } +} + +} // namespace + +CtfEncoder::CtfEncoder(CtfEncoderConfig config) : config_(std::move(config)) +{ + if (config_.coreClockHz == 0U) { + throw std::invalid_argument("CTF output requires a non-zero timestamps.clock"); + } +} + +CtfEncoder::~CtfEncoder() +{ + abort(); +} + +void CtfEncoder::start(const std::filesystem::path& outputDirectory) +{ + abort(); + outputDirectory_ = outputDirectory; + try { + resetEventTimestamps(); + overflowCounts_.clear(); + localTimestampTraceBusIds_.clear(); + reportedDwtSizeMismatches_.clear(); + exceptionLanes_.clear(); + stream_.open(outputDirectory_ / "stream_0", CtfSchema::SwoStreamId); + recording_ = true; + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart), 0U, config_.selection.empty()); + (void)exceptionLane(0U); + } catch (...) { + abort(); + throw; + } +} + +void CtfEncoder::stop() +{ + if (!recording_) { + return; + } + recording_ = false; + stream_.close(); + writeMetadataFile(); +} + +void CtfEncoder::abort() noexcept +{ + recording_ = false; + stream_.abort(); + outputDirectory_.clear(); +} + +void CtfEncoder::writeEvent(const TraceEvent& event) +{ + if (!recording_) { + return; + } + if (!isTraceEvent(event) && event.tcyc.has_value()) { + observeInputTimestamp(*event.tcyc); + if (event.quality.has_value() && event.quality->timestampReliable) { + localTimestampTraceBusIds_.insert(event.traceBusId); + } + } + + const auto selected = traceEventSelectedForOutput(event, config_.selection); + if (const auto* software = traceEventPayload(event)) { + if (selected) { + writeSoftwareEvent(event, *software); + } + } else if (const auto* exception = traceEventPayload(event)) { + if (exception->action != ExceptionAction::Unknown) { + writeExceptionEvent(event.traceBusId, *exception); + } + } else if (const auto* data = traceEventPayload(event)) { + if (selected) { + writeDwtValueEvent(event, *data); + } + } else if (const auto* address = traceEventPayload(event)) { + if (selected) { + writeDwtAddrEvent(event, *address); + } + } else if (isTraceEvent(event)) { + if (event.quality.has_value()) { + auto& overflowCount = overflowCounts_[event.traceBusId]; + overflowCount = std::max(overflowCount, event.quality->overflowCount); + } else { + ++overflowCounts_[event.traceBusId]; + } + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::Overflow), event.traceBusId, selected); + } else if (isTraceEvent(event)) { + localTimestampTraceBusIds_.insert(event.traceBusId); + if (event.tcyc.has_value()) { + observeInputTimestamp(*event.tcyc); + } + } else if (isTraceEvent(event)) { + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::Resync), event.traceBusId, + config_.selection.empty()); + } else if (const auto* timestamp = traceEventPayload(event)) { + if (selected) { + writeGlobalTimestampEvent(event, *timestamp); + } + } else if (const auto* issue = traceEventPayload(event)) { + if (issue->code == "data-loss") { + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss), event.traceBusId, selected); + } else { + if (event.quality.has_value() && event.quality->overflow) { + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss), event.traceBusId, selected); + } + if (selected) { + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError), event.traceBusId, true); + } + } + } +} + +void CtfEncoder::resetEventTimestamps() +{ + currentEventTimestamp_ = 0U; + lastInputTimestamp_.reset(); + lastEventTimestamp_.reset(); +} + +void CtfEncoder::observeInputTimestamp(std::uint64_t timestamp) +{ + currentEventTimestamp_ = holdMonotonicTimestamp(timestamp, lastInputTimestamp_); +} + +std::uint64_t CtfEncoder::allocateEventTimestamp() +{ + currentEventTimestamp_ = holdMonotonicTimestamp(currentEventTimestamp_, lastEventTimestamp_); + return currentEventTimestamp_; +} + +void CtfEncoder::writeSoftwareEvent(const TraceEvent& event, const SoftwareTraceEvent& software) +{ + const auto* variant = CtfSchema::valueVariantForTraceRunType("unsigned int", software.size); + if (variant == nullptr) { + throw std::runtime_error("CTF ITM value has an invalid SWO payload size"); + } + const auto quality = computeSampleQuality(event); + const auto eventTimestamp = allocateEventTimestamp(); + const auto payloadSize = 1U + 1U + variant->byteSize + 1U + 4U; + stream_.writeRecord(CtfSchema::value(CtfSchema::EventId::Itm), eventTimestamp, event.traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(static_cast(software.channel & 0xffU)); + record.writeU8(CtfSchema::value(variant->tag)); + writeVariantValue(record, software.value, software.size, *variant); + record.writeU8(quality.first); + record.writeU32(quality.second); + }); +} + +void CtfEncoder::writeDwtValueEvent(const TraceEvent& event, const DwtDataTraceEvent& data) +{ + const auto* source = resolvedTraceSource(config_, "dwt", event.traceBusId, data.comparator); + reportDwtSizeMismatch(event, data, source); + const auto& variant = dwtValueVariant(source, data.comparator); + const auto hasPc = data.pc.has_value() ? 1U : 0U; + const auto hasAddress = data.addressLo16.has_value() ? 1U : 0U; + const auto payloadSize = 1U + 1U + 1U + variant.byteSize + 1U + hasPc * 4U + 1U + hasAddress * 2U + 1U + 4U; + const auto eventTimestamp = allocateEventTimestamp(); + const auto quality = computeSampleQuality(event); + stream_.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtValue), eventTimestamp, event.traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(static_cast(data.comparator & 0xffU)); + record.writeU8(CtfSchema::value(data.access == AccessType::Read ? CtfSchema::DwtAccess::Read + : CtfSchema::DwtAccess::Write)); + record.writeU8(CtfSchema::value(variant.tag)); + writeVariantValue(record, data.value, data.size, variant); + record.writeU8(static_cast(hasPc)); + if (hasPc != 0U) { + record.writeU32(*data.pc); + } + record.writeU8(static_cast(hasAddress)); + if (hasAddress != 0U) { + record.writeU16(static_cast(*data.addressLo16 & 0xffffU)); + } + record.writeU8(quality.first); + record.writeU32(quality.second); + }); +} + +void CtfEncoder::reportDwtSizeMismatch(const TraceEvent& event, const DwtDataTraceEvent& data, + const ResolvedTraceSource* source) +{ + const auto configuredSize = source != nullptr ? source->valueSize : ResolvedTraceSource{}.valueSize; + if (configuredSize == data.size || config_.diagnostics == nullptr || + !reportedDwtSizeMismatches_.insert({event.traceBusId, data.comparator}).second) { + return; + } + + std::vector> context{ + {"backend", "ctf"}, + {"channel", "DWT" + std::to_string(data.comparator)}, + {"configuredSize", std::to_string(configuredSize)}, + {"swoSize", std::to_string(data.size)}, + }; + context.emplace_back("stream", std::to_string(event.traceBusId)); + config_.diagnostics->report({ + DiagnosticSink::Severity::Warning, + DiagnosticSink::Category::Output, + "dwt-symbol-size-mismatch", + "configured ctrace-run data.symbol-size does not match the decoded SWO payload size", + std::move(context), + }); +} + +void CtfEncoder::writeDwtAddrEvent(const TraceEvent& event, const DwtAddressTraceEvent& address) +{ + constexpr auto payloadSize = 1U + 1U + 1U + 4U + 2U + 1U + 4U; + const auto eventTimestamp = allocateEventTimestamp(); + const auto quality = computeSampleQuality(event); + const auto pc = dwtAddressPc(address); + const auto addressOffset = dwtAddressOffset(address); + const auto hasPc = pc.has_value() ? 1U : 0U; + const auto hasAddress = addressOffset.has_value() ? 1U : 0U; + stream_.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtAddress), eventTimestamp, event.traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(static_cast(address.comparator & 0xffU)); + record.writeU8(static_cast(hasPc)); + record.writeU8(static_cast(hasAddress)); + record.writeU32(pc.value_or(0U)); + record.writeU16(static_cast(addressOffset.value_or(0U) & 0xffffU)); + record.writeU8(quality.first); + record.writeU32(quality.second); + }); +} + +void CtfEncoder::writeGlobalTimestampEvent(const TraceEvent& event, const GlobalTimestampTraceEvent& timestamp) +{ + constexpr auto payloadSize = 8U + 1U; + const auto eventTimestamp = allocateEventTimestamp(); + stream_.writeRecord(CtfSchema::value(CtfSchema::EventId::GlobalTimestamp), eventTimestamp, event.traceBusId, + payloadSize, [&](CtfStreamWriter::Record& record) { + record.writeU64(timestamp.value); + record.writeU8(timestamp.clockChange ? 1U : 0U); + }); +} + +void CtfEncoder::writeTraceStatusEvent(std::uint8_t reason, std::uint8_t traceBusId, bool emitEvent) +{ + if (!recording_) { + return; + } + if (emitEvent) { + constexpr auto payloadSize = 1U + 4U; + const auto eventTimestamp = allocateEventTimestamp(); + stream_.writeRecord(CtfSchema::value(CtfSchema::EventId::TraceStatus), eventTimestamp, traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(reason); + record.writeU32(ctfOverflowCount(overflowCounts_[traceBusId])); + }); + } + + if (reason == CtfSchema::value(CtfSchema::TraceStatusReason::Overflow) || + reason == CtfSchema::value(CtfSchema::TraceStatusReason::Resync) || + reason == CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss)) { + const auto lane = exceptionLanes_.find(traceBusId); + if (lane != exceptionLanes_.end()) { + lane->second.resetForDiscontinuity( + [this, traceBusId](std::uint32_t number, CtfExceptionLaneTracker::RecordAction action) { + emitExceptionRecord(traceBusId, number, action); + }); + } + } +} + +void CtfEncoder::writeExceptionEvent(std::uint8_t traceBusId, const ExceptionTraceEvent& exception) +{ + exceptionLane(traceBusId) + .consume(exception, [this, traceBusId](std::uint32_t number, CtfExceptionLaneTracker::RecordAction action) { + emitExceptionRecord(traceBusId, number, action); + }); +} + +void CtfEncoder::emitExceptionRecord(std::uint8_t traceBusId, std::uint32_t number, + CtfExceptionLaneTracker::RecordAction action) +{ + TraceEvent selectionEvent{ExceptionTraceEvent{ + number, + action == CtfExceptionLaneTracker::RecordAction::Enter ? ExceptionAction::Entered : ExceptionAction::Exited, + }}; + selectionEvent.traceBusId = traceBusId; + if (!traceEventSelectedForOutput(selectionEvent, config_.selection)) { + return; + } + constexpr auto payloadSize = 2U + 1U + 2U; + const auto eventTimestamp = allocateEventTimestamp(); + const auto encodedAction = + CtfSchema::value(action == CtfExceptionLaneTracker::RecordAction::Enter ? CtfSchema::ExceptionAction::Entered + : CtfSchema::ExceptionAction::Exited); + stream_.writeRecord(CtfSchema::value(CtfSchema::EventId::Exception), eventTimestamp, traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU16(static_cast(number & 0xffffU)); + record.writeU8(encodedAction); + record.writeU16(static_cast(number & 0xffffU)); + }); +} + +CtfExceptionLaneTracker& CtfEncoder::exceptionLane(std::uint8_t traceBusId) +{ + const auto [lane, inserted] = exceptionLanes_.try_emplace(traceBusId); + if (inserted) { + lane->second.startThreadMode( + [this, traceBusId](std::uint32_t number, CtfExceptionLaneTracker::RecordAction action) { + emitExceptionRecord(traceBusId, number, action); + }); + } + return lane->second; +} + +std::pair CtfEncoder::computeSampleQuality(const TraceEvent& event) +{ + auto& currentOverflowCount = overflowCounts_[event.traceBusId]; + const auto previousOverflowCount = currentOverflowCount; + const auto overflowCount = event.quality.has_value() ? event.quality->overflowCount : currentOverflowCount; + const auto timestampReliable = event.quality.has_value() ? event.quality->timestampReliable : true; + const auto overflow = event.quality.has_value() ? event.quality->overflow : overflowCount > previousOverflowCount; + const auto flags = + static_cast((overflow ? CtfSchema::SampleFlagOverflow : 0U) | + (timestampReliable ? CtfSchema::SampleFlagTimestampReliable : 0U) | + (localTimestampTraceBusIds_.find(event.traceBusId) != localTimestampTraceBusIds_.end() + ? 0U + : CtfSchema::SampleFlagBeforeFirstTimestamp)); + currentOverflowCount = std::max(currentOverflowCount, overflowCount); + return {flags, ctfOverflowCount(overflowCount)}; +} + +void CtfEncoder::writeMetadataFile() +{ + std::set observedExceptionNumbers; + for (const auto& [traceBusId, lane] : exceptionLanes_) { + (void)traceBusId; + observedExceptionNumbers.insert(lane.observedExceptionNumbers().begin(), lane.observedExceptionNumbers().end()); + } + CtfMetadataWriter::write(outputDirectory_, stream_.uuidString(), config_.coreClockHz, config_.sources, + { + observedExceptionNumbers.begin(), + observedExceptionNumbers.end(), + }); +} diff --git a/tools/ctrace/src/output/ctf/CtfEncoder.hpp b/tools/ctrace/src/output/ctf/CtfEncoder.hpp new file mode 100644 index 000000000..e2eb095c9 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfEncoder.hpp @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "CtfExceptionLaneTracker.hpp" +#include "CtfStreamWriter.hpp" +#include "TraceSelection.hpp" +#include "TraceEvent.hpp" +#include "TraceOutputConfig.hpp" + +#include +#include +#include +#include +#include +#include +#include + +class DiagnosticSink; + +struct CtfEncoderConfig { + std::uint64_t coreClockHz = 0; + TraceSelection selection; + std::vector sources; + DiagnosticSink* diagnostics = nullptr; +}; + +class CtfEncoder final { +public: + explicit CtfEncoder(CtfEncoderConfig config); + ~CtfEncoder(); + + CtfEncoder(const CtfEncoder&) = delete; + CtfEncoder& operator=(const CtfEncoder&) = delete; + + void start(const std::filesystem::path& outputDirectory); + void stop(); + void abort() noexcept; + void writeEvent(const TraceEvent& event); + +private: + void resetEventTimestamps(); + void observeInputTimestamp(std::uint64_t timestamp); + std::uint64_t allocateEventTimestamp(); + void writeMetadataFile(); + void writeTraceStatusEvent(std::uint8_t reason, std::uint8_t traceBusId, bool emitEvent = true); + void writeSoftwareEvent(const TraceEvent& event, const SoftwareTraceEvent& software); + void writeDwtValueEvent(const TraceEvent& event, const DwtDataTraceEvent& data); + void reportDwtSizeMismatch(const TraceEvent& event, const DwtDataTraceEvent& data, const ResolvedTraceSource* source); + void writeDwtAddrEvent(const TraceEvent& event, const DwtAddressTraceEvent& address); + void writeGlobalTimestampEvent(const TraceEvent& event, const GlobalTimestampTraceEvent& timestamp); + void writeExceptionEvent(std::uint8_t traceBusId, const ExceptionTraceEvent& exception); + void emitExceptionRecord(std::uint8_t traceBusId, std::uint32_t number, CtfExceptionLaneTracker::RecordAction action); + CtfExceptionLaneTracker& exceptionLane(std::uint8_t traceBusId); + std::pair computeSampleQuality(const TraceEvent& event); + + CtfEncoderConfig config_; + std::filesystem::path outputDirectory_; + CtfStreamWriter stream_; + bool recording_ = false; + std::uint64_t currentEventTimestamp_ = 0; + std::optional lastInputTimestamp_; + std::optional lastEventTimestamp_; + std::map overflowCounts_; + std::set localTimestampTraceBusIds_; + std::set> reportedDwtSizeMismatches_; + std::map exceptionLanes_; +}; diff --git a/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.cpp b/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.cpp new file mode 100644 index 000000000..f534e34d1 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfExceptionLaneTracker.hpp" + +#include "TraceEvent.hpp" + +#include +#include +#include + +void CtfExceptionLaneTracker::reset() +{ + contextStack_.clear(); + activeContextNumber_.reset(); + observedExceptionNumbers_.clear(); +} + +void CtfExceptionLaneTracker::startThreadMode(const RecordEmitter& emit) +{ + setActiveContext(kThreadModeNumber, emit); +} + +void CtfExceptionLaneTracker::resetForDiscontinuity(const RecordEmitter& emit) +{ + contextStack_.clear(); + closeActiveContext(emit); +} + +void CtfExceptionLaneTracker::consume(const ExceptionTraceEvent& event, const RecordEmitter& emit) +{ + const auto number = event.number & 0x1ffU; + switch (event.action) { + case ExceptionAction::Entered: + enterContext(number); + break; + case ExceptionAction::Exited: + exitContext(number); + break; + case ExceptionAction::Returned: + returnToContext(number); + break; + case ExceptionAction::Unknown: + return; + } + updateActiveContext(emit); +} + +const std::vector& CtfExceptionLaneTracker::observedExceptionNumbers() const +{ + return observedExceptionNumbers_; +} + +void CtfExceptionLaneTracker::setActiveContext(std::uint32_t number, const RecordEmitter& emit) +{ + if (activeContextNumber_.has_value() && *activeContextNumber_ == number) { + return; + } + if (activeContextNumber_.has_value()) { + emitRecord(*activeContextNumber_, RecordAction::Exit, emit); + } + emitRecord(number, RecordAction::Enter, emit); + activeContextNumber_ = number; +} + +void CtfExceptionLaneTracker::closeActiveContext(const RecordEmitter& emit) +{ + if (!activeContextNumber_.has_value()) { + return; + } + emitRecord(*activeContextNumber_, RecordAction::Exit, emit); + activeContextNumber_.reset(); +} + +void CtfExceptionLaneTracker::updateActiveContext(const RecordEmitter& emit) +{ + setActiveContext(contextStack_.empty() ? kThreadModeNumber : contextStack_.back().number, emit); +} + +void CtfExceptionLaneTracker::emitRecord(std::uint32_t number, RecordAction action, const RecordEmitter& emit) +{ + if (std::find(observedExceptionNumbers_.begin(), observedExceptionNumbers_.end(), number) == + observedExceptionNumbers_.end()) { + observedExceptionNumbers_.push_back(number); + } + emit(number, action); +} + +void CtfExceptionLaneTracker::enterContext(std::uint32_t number) +{ + if (!contextStack_.empty() && contextStack_.back().state == ContextState::Running) { + contextStack_.back().state = ContextState::Preempted; + } + contextStack_.push_back({number, ContextState::Running}); +} + +void CtfExceptionLaneTracker::exitContext(std::uint32_t number) +{ + if (contextStack_.empty() || contextStack_.back().number != number || + contextStack_.back().state != ContextState::Running) { + return; + } + contextStack_.pop_back(); +} + +void CtfExceptionLaneTracker::returnToContext(std::uint32_t number) +{ + while (!contextStack_.empty() && contextStack_.back().number != number) { + contextStack_.pop_back(); + } + if (!contextStack_.empty()) { + contextStack_.back().state = ContextState::Running; + } else if (number != kThreadModeNumber) { + contextStack_.push_back({number, ContextState::Running}); + } +} diff --git a/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.hpp b/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.hpp new file mode 100644 index 000000000..c1253eb6e --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.hpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "TraceEvent.hpp" + +#include +#include +#include +#include + +class CtfExceptionLaneTracker { +public: + enum class RecordAction { + Enter, + Exit, + }; + + using RecordEmitter = std::function; + + void reset(); + void startThreadMode(const RecordEmitter& emit); + void resetForDiscontinuity(const RecordEmitter& emit); + void consume(const ExceptionTraceEvent& event, const RecordEmitter& emit); + const std::vector& observedExceptionNumbers() const; + +private: + static constexpr std::uint32_t kThreadModeNumber = 0; + + enum class ContextState : std::uint8_t { + Running, + Preempted, + }; + + struct ContextFrame { + std::uint32_t number = 0; + ContextState state = ContextState::Running; + }; + + void setActiveContext(std::uint32_t number, const RecordEmitter& emit); + void closeActiveContext(const RecordEmitter& emit); + void updateActiveContext(const RecordEmitter& emit); + void emitRecord(std::uint32_t number, RecordAction action, const RecordEmitter& emit); + void enterContext(std::uint32_t number); + void exitContext(std::uint32_t number); + void returnToContext(std::uint32_t number); + + std::vector contextStack_; + std::optional activeContextNumber_; + std::vector observedExceptionNumbers_; +}; diff --git a/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp b/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp new file mode 100644 index 000000000..e763debda --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp @@ -0,0 +1,423 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfMetadataWriter.hpp" + +#include "CtfSchema.hpp" +#include "TraceOutputConfig.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static std::string tsdlString(const std::string& value) +{ + static constexpr char hexDigits[] = "0123456789ABCDEF"; + std::string out = "\""; + for (const auto raw : value) { + const auto ch = static_cast(raw); + if (ch == '\\' || ch == '"') { + out.push_back('\\'); + out.push_back(static_cast(ch)); + } else if (ch == '\n') { + out += "\\n"; + } else if (ch == '\r') { + out += "\\r"; + } else if (ch == '\t') { + out += "\\t"; + } else if (ch < 0x20U || ch == 0x7fU) { + out += "\\x"; + out.push_back(hexDigits[(ch >> 4U) & 0x0fU]); + out.push_back(hexDigits[ch & 0x0fU]); + } else { + out.push_back(static_cast(ch)); + } + } + out.push_back('"'); + return out; +} + +static std::string hexValue(std::uint64_t value) +{ + std::ostringstream out; + out << std::hex << std::uppercase << value; + return out.str(); +} + +static std::string uniqueEnumLabel(const std::string& preferred, const std::string& fallback, + std::set& used) +{ + auto candidate = preferred.empty() ? fallback : preferred; + if (used.insert(candidate).second) { + return candidate; + } + if (used.insert(fallback).second) { + return fallback; + } + for (std::uint32_t suffix = 1;; ++suffix) { + const auto label = fallback + "_" + std::to_string(suffix); + if (used.insert(label).second) { + return label; + } + } +} + +static std::string mapValueOrEmpty(const std::map& values, std::uint32_t key) +{ + const auto found = values.find(key); + return found == values.end() ? "" : found->second; +} + +static std::vector +exceptionNumbersWithDefaults(const std::vector& observedExceptionNumbers) +{ + std::vector exceptions = {0, 1, 2, 3, 4, 5, 6, 7, 11, 12, 14, 15}; + for (const auto number : observedExceptionNumbers) { + if (std::find(exceptions.begin(), exceptions.end(), number) == exceptions.end()) { + exceptions.push_back(number); + } + } + std::sort(exceptions.begin(), exceptions.end()); + return exceptions; +} + +static std::string exceptionName(std::uint32_t number) +{ + switch (number) { + case 0: + return "ThreadMode"; + case 1: + return "Reset"; + case 2: + return "NMI"; + case 3: + return "HardFault"; + case 4: + return "MemManage"; + case 5: + return "BusFault"; + case 6: + return "UsageFault"; + case 7: + return "SecureFault"; + case 11: + return "SVCall"; + case 12: + return "DebugMonitor"; + case 14: + return "PendSV"; + case 15: + return "SysTick"; + default: + return "External IRQ " + std::to_string(number); + } +} + +static std::string_view tsdlValueType(const CtfSchema::ValueVariant& variant) +{ + if (variant.floatingPoint) { + return "ieee_float32_t"; + } + if (variant.tag == CtfSchema::ValueTag::Character8) { + return "uint8_t"; + } + if (variant.signedInteger) { + return variant.byteSize == 1U ? "int8_t" : variant.byteSize == 2U ? "int16_t" : "int32_t"; + } + return variant.byteSize == 1U ? "uint8_t" : variant.byteSize == 2U ? "uint16_t" : "uint32_t"; +} + +static std::string ctfValueFields(std::string_view prefix) +{ + std::ostringstream out; + out << " enum : uint8_t { "; + for (std::size_t index = 0; index < CtfSchema::ValueVariants.size(); ++index) { + const auto& variant = CtfSchema::ValueVariants[index]; + if (index > 0U) { + out << ", "; + } + out << variant.name << " = " << static_cast(CtfSchema::value(variant.tag)); + } + out << " } cmsis_" << prefix << "_value_type;\n" + << " variant {\n"; + for (const auto& variant : CtfSchema::ValueVariants) { + out << " " << tsdlValueType(variant) << " " << variant.name << ";\n"; + } + out << " } cmsis_" << prefix << "_value;\n"; + return out.str(); +} + +void CtfMetadataWriter::write(const std::filesystem::path& outputDir, const std::string& uuidString, + std::uint64_t coreClockHz, const std::vector& sources, + const std::vector& observedExceptionNumbers) +{ + std::map dwtValueTypes; + std::map itmNames; + std::map dwtNames; + std::map dwtAddressStarts; + std::map dwtAddressEnds; + + for (const auto& source : sources) { + const auto id = source.source; + if (source.type == "itm") { + if (source.label.has_value()) { + itmNames[id] = *source.label; + } + continue; + } + if (source.type != "dwt") { + continue; + } + dwtValueTypes[id] = source.valueType; + if (source.label.has_value()) { + dwtNames[id] = *source.label; + } + if (source.symbolAddress.has_value()) { + dwtAddressStarts[id] = *source.symbolAddress; + const auto extent = static_cast(source.valueSize - 1U); + if (*source.symbolAddress <= std::numeric_limits::max() - extent) { + dwtAddressEnds[id] = *source.symbolAddress + extent; + } + } + } + + std::ostringstream exceptionEnum; + for (const auto number : exceptionNumbersWithDefaults(observedExceptionNumbers)) { + exceptionEnum << " " << tsdlString(exceptionName(number)) << " = " << number << ",\n"; + } + + const auto metadataPath = outputDir / "metadata"; + std::ofstream out(metadataPath, std::ios::out | std::ios::trunc); + if (!out) { + throw std::runtime_error("Failed to write CTF metadata " + metadataPath.string()); + } + + out << R"(/* CTF 1.8 */ +trace { + major = 1; + minor = 8; + uuid = ")" + << uuidString << R"("; + byte_order = le; + packet.header := struct { + integer { size = 32; align = 8; signed = false; } magic; + integer { size = 8; align = 8; signed = false; } uuid[16]; + integer { size = 32; align = 8; signed = false; } stream_id; + }; +}; + +env { + cmsis_ctf_profile = "cmsis.ctf"; + cmsis_ctf_profile_version = 1; +)"; + for (const auto& entry : dwtValueTypes) { + out << " cmsis_dwt" << entry.first << "_value_type = " << tsdlString(entry.second) << ";\n"; + } + for (const auto& entry : dwtAddressStarts) { + out << " cmsis_dwt" << entry.first << "_address_start = " << tsdlString("0x" + hexValue(entry.second)) << ";\n"; + } + for (const auto& entry : dwtAddressEnds) { + out << " cmsis_dwt" << entry.first << "_address_end = " << tsdlString("0x" + hexValue(entry.second)) << ";\n"; + } + out << R"(}; + +clock { + name = swo_clock; + precision = 0; + offset_s = 0; + offset = 0; + absolute = false; + freq = )" + << coreClockHz << R"(; +}; + +typealias integer { size = 8; align = 8; signed = false; } := uint8_t; +typealias integer { size = 16; align = 8; signed = false; byte_order = le; } := uint16_t; +typealias integer { size = 32; align = 8; signed = false; byte_order = le; } := uint32_t; +typealias integer { size = 8; align = 8; signed = true; } := int8_t; +typealias integer { size = 16; align = 8; signed = true; byte_order = le; } := int16_t; +typealias integer { size = 32; align = 8; signed = true; byte_order = le; } := int32_t; +typealias floating_point { exp_dig = 8; mant_dig = 24; align = 8; byte_order = le; } := ieee_float32_t; +typealias integer { size = 64; align = 8; signed = false; byte_order = le; } := uint64_t; +typealias integer { size = 64; align = 8; signed = false; map = clock.swo_clock.value; } := swo_clock_t; + +typealias enum : uint8_t { + "read" = )" + << static_cast(CtfSchema::value(CtfSchema::DwtAccess::Read)) << R"(, + "write" = )" + << static_cast(CtfSchema::value(CtfSchema::DwtAccess::Write)) << R"( +} := cmsis_dwt_access_t; +typealias enum : uint8_t { + "trace_start" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart)) << R"(, + "resync" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::Resync)) << R"(, + "overflow" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::Overflow)) << R"(, + "decode_error" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError)) << R"(, + "data_loss" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss)) << R"( +} := cmsis_trace_status_reason_t; +typealias enum : uint8_t { + "entered" = )" + << static_cast(CtfSchema::value(CtfSchema::ExceptionAction::Entered)) << R"(, + "exited" = )" + << static_cast(CtfSchema::value(CtfSchema::ExceptionAction::Exited)) << R"( +} := cmsis_exception_action_t; +typealias enum : uint8_t { +)"; + std::set itmLabels; + for (std::uint32_t channel = 1U; channel < 32U; ++channel) { + const auto fallback = "ITM" + std::to_string(channel); + out << " " << tsdlString(uniqueEnumLabel(mapValueOrEmpty(itmNames, channel), fallback, itmLabels)) << " = " + << channel << ",\n"; + } + out << R"(} := cmsis_itm_channel_t; +typealias enum : uint8_t { +)"; + std::set dwtLabels; + for (std::uint32_t comparator = 0U; comparator < 4U; ++comparator) { + const auto label = mapValueOrEmpty(dwtNames, comparator); + const auto fallback = "DWT" + std::to_string(comparator); + out << " " << tsdlString(uniqueEnumLabel(label, fallback, dwtLabels)) << " = " << comparator << ",\n"; + } + out << R"( +} := cmsis_dwt_comparator_t; +typealias enum : uint16_t { +)" << exceptionEnum.str() + << R"(} := cmsis_exception_number_t; + +stream { + id = )" + << CtfSchema::SwoStreamId << R"(; + event.header := struct { + uint32_t id; + swo_clock_t timestamp; + }; + event.context := struct { + uint8_t cmsis_trace_bus_id; + }; + packet.context := struct { + uint32_t packet_size; + uint32_t content_size; + swo_clock_t timestamp_begin; + swo_clock_t timestamp_end; + uint32_t events_discarded; + uint32_t packet_seq_num; + }; +}; + +event { + id = )" + << CtfSchema::value(CtfSchema::EventId::Itm) << R"(; + name = ")" + << CtfSchema::eventName(CtfSchema::EventId::Itm) << R"("; + stream_id = )" + << CtfSchema::SwoStreamId << R"(; + fields := struct { + cmsis_itm_channel_t cmsis_itm_channel; +)" << ctfValueFields("itm") + << R"( uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; + +event { + id = )" + << CtfSchema::value(CtfSchema::EventId::DwtValue) << R"(; + name = ")" + << CtfSchema::eventName(CtfSchema::EventId::DwtValue) << R"("; + stream_id = )" + << CtfSchema::SwoStreamId << R"(; + fields := struct { + cmsis_dwt_comparator_t cmsis_dwt_comparator; + cmsis_dwt_access_t cmsis_dwt_access; +)" << ctfValueFields("dwt") + << R"( uint8_t cmsis_has_pc; + uint32_t cmsis_pc[cmsis_has_pc]; + uint8_t cmsis_has_address_lo16; + uint16_t cmsis_address_lo16[cmsis_has_address_lo16]; + uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; + +event { + id = )" + << CtfSchema::value(CtfSchema::EventId::DwtAddress) << R"(; + name = ")" + << CtfSchema::eventName(CtfSchema::EventId::DwtAddress) << R"("; + stream_id = )" + << CtfSchema::SwoStreamId << R"(; + fields := struct { + cmsis_dwt_comparator_t cmsis_dwt_comparator; + uint8_t cmsis_has_pc; + uint8_t cmsis_has_address_lo16; + uint32_t cmsis_pc; + uint16_t cmsis_address_lo16; + uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; + +event { + id = )" + << CtfSchema::value(CtfSchema::EventId::TraceStatus) << R"(; + name = ")" + << CtfSchema::eventName(CtfSchema::EventId::TraceStatus) << R"("; + stream_id = )" + << CtfSchema::SwoStreamId << R"(; + fields := struct { + cmsis_trace_status_reason_t cmsis_trace_status_reason; + uint32_t cmsis_overflow_count; + }; +}; + +event { + id = )" + << CtfSchema::value(CtfSchema::EventId::Exception) << R"(; + name = ")" + << CtfSchema::eventName(CtfSchema::EventId::Exception) << R"("; + stream_id = )" + << CtfSchema::SwoStreamId << R"(; + fields := struct { + cmsis_exception_number_t cmsis_exception_number; + cmsis_exception_action_t cmsis_exception_action; + uint16_t cmsis_exception_number_value; + }; +}; + +event { + id = )" + << CtfSchema::value(CtfSchema::EventId::GlobalTimestamp) << R"(; + name = ")" + << CtfSchema::eventName(CtfSchema::EventId::GlobalTimestamp) << R"("; + stream_id = )" + << CtfSchema::SwoStreamId << R"(; + fields := struct { + uint64_t cmsis_global_timestamp; + uint8_t cmsis_clock_change; + }; +}; +)"; + out.close(); + if (!out) { + throw std::runtime_error("Failed to write CTF metadata " + metadataPath.string()); + } +} diff --git a/tools/ctrace/src/output/ctf/CtfMetadataWriter.hpp b/tools/ctrace/src/output/ctf/CtfMetadataWriter.hpp new file mode 100644 index 000000000..9fefc3d62 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfMetadataWriter.hpp @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "TraceOutputConfig.hpp" + +#include +#include +#include +#include + +namespace CtfMetadataWriter { + +void write(const std::filesystem::path& outputDir, const std::string& uuidString, std::uint64_t coreClockHz, + const std::vector& sources, const std::vector& observedExceptionNumbers); + +} // namespace CtfMetadataWriter diff --git a/tools/ctrace/src/output/ctf/CtfSchema.hpp b/tools/ctrace/src/output/ctf/CtfSchema.hpp new file mode 100644 index 000000000..87c89c50d --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfSchema.hpp @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include +#include +#include +#include + +namespace CtfSchema { + +inline constexpr std::uint32_t Magic = 0xC1FC1FC1U; +inline constexpr std::uint32_t SwoStreamId = 0U; + +enum class EventId : std::uint32_t { + Itm = 0U, + DwtValue = 4U, + TraceStatus = 5U, + DwtAddress = 6U, + Exception = 7U, + GlobalTimestamp = 8U, +}; + +enum class TraceStatusReason : std::uint8_t { + TraceStart = 0U, + Resync = 1U, + Overflow = 2U, + DecodeError = 4U, + DataLoss = 5U, +}; + +enum class DwtAccess : std::uint8_t { + Read = 0U, + Write = 1U, +}; + +enum class ExceptionAction : std::uint8_t { + Entered = 1U, + Exited = 2U, +}; + +enum class ValueTag : std::uint8_t { + Character8 = 0U, + Signed8 = 1U, + Unsigned8 = 2U, + Signed16 = 3U, + Unsigned16 = 4U, + Signed32 = 5U, + Unsigned32 = 6U, + Float32 = 7U, +}; + +struct ValueVariant { + ValueTag tag; + std::string_view name; + std::string_view traceCompassType; + std::uint8_t byteSize; + bool floatingPoint; + bool signedInteger; +}; + +inline constexpr std::array ValueVariants{{ + {ValueTag::Character8, "c8", "long", 1U, false, false}, + {ValueTag::Signed8, "i8", "long", 1U, false, true}, + {ValueTag::Unsigned8, "u8", "long", 1U, false, false}, + {ValueTag::Signed16, "i16", "long", 2U, false, true}, + {ValueTag::Unsigned16, "u16", "long", 2U, false, false}, + {ValueTag::Signed32, "i32", "long", 4U, false, true}, + {ValueTag::Unsigned32, "u32", "long", 4U, false, false}, + {ValueTag::Float32, "f32", "double", 4U, true, false}, +}}; + +inline constexpr std::string_view ValueTypeRequirements = + "supported data.symbol-type values are 'unsigned int', 'signed int', and 'float'; " + "data.symbol-size must be 1, 2, or 4, and float requires size 4"; + +constexpr const ValueVariant& valueVariant(ValueTag tag) +{ + return ValueVariants[static_cast(tag)]; +} + +constexpr const ValueVariant* valueVariantForTraceRunType(std::string_view typeName, std::uint64_t byteSize) +{ + if (typeName == "float") { + return byteSize == 4U ? &valueVariant(ValueTag::Float32) : nullptr; + } + + const auto signedInteger = typeName == "signed int"; + if (!signedInteger && typeName != "unsigned int") { + return nullptr; + } + if (byteSize == 1U) { + return &valueVariant(signedInteger ? ValueTag::Signed8 : ValueTag::Unsigned8); + } + if (byteSize == 2U) { + return &valueVariant(signedInteger ? ValueTag::Signed16 : ValueTag::Unsigned16); + } + if (byteSize == 4U) { + return &valueVariant(signedInteger ? ValueTag::Signed32 : ValueTag::Unsigned32); + } + return nullptr; +} + +inline constexpr std::uint8_t SampleFlagOverflow = 1U << 0U; +inline constexpr std::uint8_t SampleFlagTimestampReliable = 1U << 1U; +inline constexpr std::uint8_t SampleFlagBeforeFirstTimestamp = 1U << 2U; + +constexpr std::uint32_t value(EventId id) +{ + return static_cast(id); +} + +constexpr std::uint8_t value(TraceStatusReason reason) +{ + return static_cast(reason); +} + +constexpr std::uint8_t value(ValueTag tag) +{ + return static_cast(tag); +} + +constexpr std::uint8_t value(DwtAccess access) +{ + return static_cast(access); +} + +constexpr std::uint8_t value(ExceptionAction action) +{ + return static_cast(action); +} + +constexpr std::string_view eventName(EventId id) +{ + switch (id) { + case EventId::Itm: + return "ITM"; + case EventId::DwtValue: + return "DWT_VALUE"; + case EventId::TraceStatus: + return "TRACE_STATUS"; + case EventId::DwtAddress: + return "DWT_ADDR"; + case EventId::Exception: + return "EXCEPTION"; + case EventId::GlobalTimestamp: + return "GLOBAL_TIMESTAMP"; + } + return "UNKNOWN"; +} + +} // namespace CtfSchema diff --git a/tools/ctrace/src/output/ctf/CtfStreamWriter.cpp b/tools/ctrace/src/output/ctf/CtfStreamWriter.cpp new file mode 100644 index 000000000..b90802362 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfStreamWriter.cpp @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfStreamWriter.hpp" + +#include "CtfSchema.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::size_t kPacketSizeBytes = 65536U; +constexpr std::size_t kPacketHeaderSize = 24U; +constexpr std::size_t kPacketContextSize = 32U; +constexpr std::size_t kPacketOverhead = kPacketHeaderSize + kPacketContextSize; +constexpr std::size_t kEventPrefixSize = 13U; +std::string formatUuid(const std::array& uuid) +{ + std::ostringstream out; + out << std::hex << std::setfill('0'); + for (std::size_t index = 0; index < uuid.size(); ++index) { + if (index == 4U || index == 6U || index == 8U || index == 10U) { + out << '-'; + } + out << std::setw(2) << static_cast(uuid[index]); + } + return out.str(); +} + +} // namespace + +CtfStreamWriter::Record::Record(std::vector& buffer, std::size_t offset, std::size_t endOffset) + : buffer_(buffer), offset_(offset), endOffset_(endOffset) +{ +} + +void CtfStreamWriter::Record::requireSpace(std::size_t size) const +{ + if (size > endOffset_ - offset_) { + throw std::logic_error("CTF record payload exceeds its declared size"); + } +} + +void CtfStreamWriter::Record::writeU8(std::uint8_t value) +{ + requireSpace(1U); + buffer_[offset_++] = value; +} + +void CtfStreamWriter::Record::writeU16(std::uint16_t value) +{ + requireSpace(2U); + writeU8(static_cast(value & 0xffU)); + writeU8(static_cast((value >> 8U) & 0xffU)); +} + +void CtfStreamWriter::Record::writeU32(std::uint32_t value) +{ + requireSpace(4U); + for (unsigned shift = 0U; shift < 32U; shift += 8U) { + writeU8(static_cast((value >> shift) & 0xffU)); + } +} + +void CtfStreamWriter::Record::writeU64(std::uint64_t value) +{ + requireSpace(8U); + for (unsigned shift = 0U; shift < 64U; shift += 8U) { + writeU8(static_cast((value >> shift) & 0xffU)); + } +} + +CtfStreamWriter::~CtfStreamWriter() +{ + abort(); +} + +void CtfStreamWriter::open(const std::filesystem::path& filePath, std::uint32_t streamId) +{ + abort(); + filePath_ = filePath; + streamId_ = streamId; + packetSequence_ = 0U; + lastTimestamp_.reset(); + uuid_.fill(0U); + + std::random_device random; + for (auto& byte : uuid_) { + byte = static_cast(random()); + } + uuid_[6] = static_cast((uuid_[6] & 0x0fU) | 0x40U); + uuid_[8] = static_cast((uuid_[8] & 0x3fU) | 0x80U); + uuidString_ = formatUuid(uuid_); + + packetBuffer_.assign(kPacketSizeBytes, 0U); + beginPacket(); + file_.open(filePath_, std::ios::binary | std::ios::out | std::ios::trunc); + if (!file_) { + abort(); + throw std::runtime_error("Failed to open CTF stream " + filePath.string()); + } + open_ = true; +} + +void CtfStreamWriter::close() +{ + if (!open_) { + return; + } + flushPacket(); + file_.close(); + open_ = false; + packetBuffer_.clear(); + if (!file_) { + throw std::runtime_error("Failed to write CTF stream in " + filePath_.parent_path().string()); + } +} + +void CtfStreamWriter::abort() noexcept +{ + if (file_.is_open()) { + file_.close(); + } + open_ = false; + packetBuffer_.clear(); + filePath_.clear(); +} + +void CtfStreamWriter::writeRecord(std::uint32_t eventId, std::uint64_t timestamp, std::uint8_t traceBusId, + std::size_t payloadSize, const RecordCallback& writePayload) +{ + if (!open_) { + return; + } + const auto totalSize = kEventPrefixSize + payloadSize; + if (totalSize > kPacketSizeBytes - kPacketOverhead) { + throw std::invalid_argument("CTF record does not fit into a packet"); + } + if (contentOffset_ + totalSize > kPacketSizeBytes) { + flushPacket(); + } + + timestamp = monotonicTimestamp(timestamp); + const auto recordEnd = contentOffset_ + totalSize; + Record record(packetBuffer_, contentOffset_, recordEnd); + record.writeU32(eventId); + record.writeU64(timestamp); + record.writeU8(traceBusId); + writePayload(record); + if (record.offset_ != recordEnd) { + throw std::logic_error("CTF record payload is shorter than its declared size"); + } + + contentOffset_ = recordEnd; + if (eventCount_ == 0U) { + timestampBegin_ = timestamp; + timestampEnd_ = timestamp; + } else { + timestampBegin_ = std::min(timestampBegin_, timestamp); + timestampEnd_ = std::max(timestampEnd_, timestamp); + } + ++eventCount_; +} + +const std::string& CtfStreamWriter::uuidString() const noexcept +{ + return uuidString_; +} + +void CtfStreamWriter::beginPacket() +{ + std::fill(packetBuffer_.begin(), packetBuffer_.end(), 0U); + contentOffset_ = kPacketOverhead; + eventCount_ = 0U; + timestampBegin_ = 0U; + timestampEnd_ = 0U; +} + +void CtfStreamWriter::flushPacket() +{ + if (eventCount_ == 0U) { + return; + } + + Record header(packetBuffer_, 0U, kPacketOverhead); + header.writeU32(CtfSchema::Magic); + for (const auto byte : uuid_) { + header.writeU8(byte); + } + header.writeU32(streamId_); + header.writeU32(static_cast(kPacketSizeBytes * 8U)); + header.writeU32(static_cast(contentOffset_ * 8U)); + header.writeU64(timestampBegin_); + header.writeU64(timestampEnd_); + header.writeU32(0U); + header.writeU32(packetSequence_); + if (header.offset_ != kPacketOverhead) { + throw std::logic_error("CTF packet header size mismatch"); + } + + file_.write(reinterpret_cast(packetBuffer_.data()), static_cast(packetBuffer_.size())); + ++packetSequence_; + beginPacket(); +} + +std::uint64_t CtfStreamWriter::monotonicTimestamp(std::uint64_t timestamp) +{ + if (lastTimestamp_.has_value() && timestamp < *lastTimestamp_) { + timestamp = *lastTimestamp_; + } + lastTimestamp_ = timestamp; + return timestamp; +} diff --git a/tools/ctrace/src/output/ctf/CtfStreamWriter.hpp b/tools/ctrace/src/output/ctf/CtfStreamWriter.hpp new file mode 100644 index 000000000..454d30525 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfStreamWriter.hpp @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class CtfStreamWriter final { +public: + class Record final { + public: + void writeU8(std::uint8_t value); + void writeU16(std::uint16_t value); + void writeU32(std::uint32_t value); + void writeU64(std::uint64_t value); + + private: + friend class CtfStreamWriter; + + Record(std::vector& buffer, std::size_t offset, std::size_t endOffset); + + void requireSpace(std::size_t size) const; + + std::vector& buffer_; + std::size_t offset_; + std::size_t endOffset_; + }; + + using RecordCallback = std::function; + + CtfStreamWriter() = default; + ~CtfStreamWriter(); + + CtfStreamWriter(const CtfStreamWriter&) = delete; + CtfStreamWriter& operator=(const CtfStreamWriter&) = delete; + + void open(const std::filesystem::path& filePath, std::uint32_t streamId); + void close(); + void abort() noexcept; + + void writeRecord(std::uint32_t eventId, std::uint64_t timestamp, std::uint8_t traceBusId, std::size_t payloadSize, + const RecordCallback& writePayload); + + const std::string& uuidString() const noexcept; + +private: + void beginPacket(); + void flushPacket(); + std::uint64_t monotonicTimestamp(std::uint64_t timestamp); + + std::filesystem::path filePath_; + std::ofstream file_; + std::vector packetBuffer_; + std::size_t contentOffset_ = 0; + std::uint32_t eventCount_ = 0; + std::uint64_t timestampBegin_ = 0; + std::uint64_t timestampEnd_ = 0; + std::optional lastTimestamp_; + std::uint32_t streamId_ = 0; + std::uint32_t packetSequence_ = 0; + std::array uuid_{}; + std::string uuidString_; + bool open_ = false; +}; diff --git a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp new file mode 100644 index 000000000..6a96f1b62 --- /dev/null +++ b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TraceCompassXmlWriter.hpp" + +#include "CtfSchema.hpp" + +#include +#include +#include +#include +#include +#include +#include + +constexpr const char* kTraceCompassAnalysisVersionPlaceholder = "__SWO_ANALYSIS_VERSION__"; + +static std::uint32_t traceCompassAnalysisVersion(const std::string& xml) +{ + constexpr std::uint32_t kFnvOffsetBasis = 2166136261U; + constexpr std::uint32_t kFnvPrime = 16777619U; + constexpr std::uint32_t kTraceCompassPositiveMask = 0x7fffffffU; + + std::uint32_t hash = kFnvOffsetBasis; + for (const auto ch : xml) { + hash ^= static_cast(ch); + hash *= kFnvPrime; + } + const auto version = hash & kTraceCompassPositiveMask; + return version == 0U ? 1U : version; +} + +static std::string withTraceCompassAnalysisVersion(std::string xml) +{ + const auto version = std::to_string(traceCompassAnalysisVersion(xml)); + const auto placeholder = std::string(kTraceCompassAnalysisVersionPlaceholder); + const auto position = xml.find(placeholder); + if (position == std::string::npos) { + return xml; + } + xml.replace(position, placeholder.size(), version); + return xml; +} + +void TraceCompassXmlWriter::writeFile(const std::filesystem::path& filePath) +{ + std::ostringstream dwtHandlers; + for (const auto& arm : CtfSchema::ValueVariants) { + dwtHandlers << R"( + + + + + + + + + + + + + +)"; + } + + std::ostringstream itmHandlers; + for (const auto& arm : CtfSchema::ValueVariants) { + itmHandlers << R"( + + + + + + + + + + + + + +)"; + } + + if (!filePath.parent_path().empty()) { + std::filesystem::create_directories(filePath.parent_path()); + } + std::ofstream out(filePath, std::ios::out | std::ios::trunc); + if (!out) { + throw std::runtime_error("Failed to write Trace Compass XML " + filePath.string()); + } + std::ostringstream xml; + xml << R"( + + + + + + + + + + + + + + +)"; + out << withTraceCompassAnalysisVersion(xml.str()); + out.close(); + if (!out) { + throw std::runtime_error("Failed to write Trace Compass XML " + filePath.string()); + } +} diff --git a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.hpp b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.hpp new file mode 100644 index 000000000..eb85cab81 --- /dev/null +++ b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.hpp @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include + +namespace TraceCompassXmlWriter { + +void writeFile(const std::filesystem::path& path); + +} // namespace TraceCompassXmlWriter diff --git a/tools/ctrace/src/tracerun/CtraceRunMeta.cpp b/tools/ctrace/src/tracerun/CtraceRunMeta.cpp new file mode 100644 index 000000000..82feae5cc --- /dev/null +++ b/tools/ctrace/src/tracerun/CtraceRunMeta.cpp @@ -0,0 +1,559 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtraceRunMeta.hpp" + +#include "TraceRunConfig.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct ProcessorMeta { + std::optional name; + bool timestampsEnabled = false; + std::optional timestampClockHz; + std::optional timestampClockError; + std::optional timestampPrescaler; + std::optional itmEnableMask; +}; + +struct ProcessorIdentity { + bool multipleProcessors = false; + std::optional singleProcessorName; + + std::optional canonicalName(const std::optional& name) const + { + if (!multipleProcessors) { + return singleProcessorName; + } + return TraceRunSchema::normalizedProcessorName(name); + } +}; + +using ReferenceProblem = TraceRunSchema::ReferenceProblem; + +std::string configError(const TraceRunConfig& config, std::size_t line, const std::string& message) +{ + auto location = config.path; + if (line > 0U) { + location += "(" + std::to_string(line) + ")"; + } + return location + ": " + message; +} + +std::string referenceProblemMessage(const TraceRunConfig& config, const TraceRunReference& reference, + ReferenceProblem problem) +{ + switch (problem) { + case ReferenceProblem::DuplicateSource: + return configError(config, reference.line, + reference.line > 0U ? "duplicate value in 'source' array" : "duplicate value in source array"); + case ReferenceProblem::InvalidStream: + return configError(config, reference.line, + reference.line > 0U ? "'stream' must be a CoreSight ATB trace ID between 1 and 111" + : "stream must be a CoreSight ATB trace ID between 1 and 111"); + case ReferenceProblem::UnsupportedSourceArray: + return configError(config, reference.line, "source arrays are only supported for DWT references"); + case ReferenceProblem::InvalidItmSource: + return configError(config, reference.line, + reference.line > 0U ? "ITM 'source' must be between 0 and 31" + : "ITM source must be between 0 and 31"); + case ReferenceProblem::None: + break; + } + return {}; +} + +bool consumesSetup(const TraceRunConfig& config, const TraceRunSetup& setup) +{ + if (setup.timestamps.has_value() || setup.itm.has_value()) { + return true; + } + for (const auto& reference : config.references) { + if (!TraceRunSchema::isUsableReference(reference) || reference.type != "dwt" || + !TraceRunSchema::processorNamesMayBind(setup.processorName, reference.processorName)) { + continue; + } + const auto index = reference.dataSetupIndex; + if (index.has_value() && *index < setup.data.size()) { + return true; + } + } + return false; +} + +ProcessorIdentity processorIdentity(const TraceRunConfig& config) +{ + std::set setupNames; + std::set> uniqueSetupNames; + bool unnamedSetup = false; + std::size_t setupCount = 0U; + const TraceRunSetup* singleActiveSetup = nullptr; + for (const auto& setup : config.setups) { + if (!consumesSetup(config, setup)) { + continue; + } + ++setupCount; + singleActiveSetup = &setup; + const auto name = TraceRunSchema::normalizedProcessorName(setup.processorName); + if (!uniqueSetupNames.insert(name).second) { + throw std::runtime_error(configError(config, setup.line, + (setup.line > 0U ? "duplicate active 'ctrace-setup' for pname '" + : "duplicate active ctrace-setup for pname '") + + name.value_or("") + "'")); + } + if (name.has_value()) { + setupNames.insert(*name); + } else { + unnamedSetup = true; + } + } + + std::set referenceNames; + bool unnamedReference = false; + for (const auto& reference : config.references) { + if (!TraceRunSchema::isUsableReference(reference) && !TraceRunSchema::contributesStreamBinding(reference)) { + continue; + } + const auto name = TraceRunSchema::normalizedProcessorName(reference.processorName); + if (name.has_value()) { + referenceNames.insert(*name); + } else { + unnamedReference = true; + } + } + + if (setupCount > 1U) { + if (unnamedSetup || unnamedReference) { + throw std::runtime_error(config.path + ": pname is required for every ctrace-setup and ctrace-ref " + "in a multi-processor configuration"); + } + for (const auto& name : referenceNames) { + if (setupNames.find(name) == setupNames.end()) { + throw std::runtime_error(config.path + ": ctrace-ref pname '" + name + "' has no matching ctrace-setup"); + } + } + return {true, std::nullopt}; + } + + if (setupCount == 1U) { + const auto setupName = TraceRunSchema::normalizedProcessorName(singleActiveSetup->processorName); + if (setupName.has_value()) { + for (const auto& name : referenceNames) { + if (name != *setupName) { + throw std::runtime_error(config.path + ": ctrace-ref pname '" + name + + "' has no matching ctrace-setup pname '" + *setupName + "'"); + } + } + return {false, setupName}; + } + if (referenceNames.size() > 1U) { + throw std::runtime_error(config.path + ": pname is required for ctrace-setup in a multi-processor configuration"); + } + return { + false, + referenceNames.empty() ? std::nullopt : std::optional(*referenceNames.begin()), + }; + } + + if (referenceNames.size() > 1U) { + if (unnamedReference) { + throw std::runtime_error(config.path + + ": pname is required for every ctrace-ref in a multi-processor configuration"); + } + return {true, std::nullopt}; + } + return { + false, + referenceNames.empty() ? std::nullopt : std::optional(*referenceNames.begin()), + }; +} + +const TraceRunDataSetup* referencedDataSetup(const TraceRunConfig& config, const TraceRunReference& reference, + const ProcessorIdentity& processorIdentity) +{ + const auto index = reference.dataSetupIndex; + if (!index.has_value()) { + return nullptr; + } + const auto referenceProcessorName = processorIdentity.canonicalName(reference.processorName); + const TraceRunDataSetup* result = nullptr; + for (const auto& setup : config.setups) { + if (!TraceRunSchema::processorNamesMayBind(setup.processorName, reference.processorName)) { + continue; + } + if (referenceProcessorName.has_value() && + processorIdentity.canonicalName(setup.processorName) != referenceProcessorName) { + continue; + } + const auto* candidate = *index < setup.data.size() ? &setup.data[*index] : nullptr; + if (candidate == nullptr) { + continue; + } + if (result != nullptr) { + return nullptr; + } + result = candidate; + } + return result; +} + +CtraceRunSourceMeta sourceMeta(const TraceRunConfig& config, const TraceRunReference& reference, std::uint32_t source, + const ProcessorIdentity& processorIdentity) +{ + const auto* dataSetup = reference.type == "dwt" ? referencedDataSetup(config, reference, processorIdentity) : nullptr; + return { + reference.type, + processorIdentity.canonicalName(reference.processorName), + static_cast(reference.stream.value_or(0U)), + source, + reference.label, + reference.type == "dwt" ? reference.symbolAddress : std::nullopt, + dataSetup != nullptr ? dataSetup->symbolType.value_or(std::string(TraceRunSchema::kDefaultDwtDataType)) + : std::string(TraceRunSchema::kDefaultDwtDataType), + dataSetup != nullptr ? dataSetup->symbolSize.value_or(TraceRunSchema::kDefaultDwtDataSize) + : TraceRunSchema::kDefaultDwtDataSize, + dataSetup != nullptr ? dataSetup->symbolTypeError : std::nullopt, + dataSetup != nullptr ? dataSetup->symbolSizeError : std::nullopt, + }; +} + +ProcessorMeta& processorMeta(std::vector& processors, const std::optional& name) +{ + const auto found = std::find_if(processors.begin(), processors.end(), + [&](const ProcessorMeta& processor) { return processor.name == name; }); + if (found != processors.end()) { + return *found; + } + processors.emplace_back(); + processors.back().name = name; + return processors.back(); +} + +template +std::optional commonProcessorSetting(const std::vector& processors, + const std::optional ProcessorMeta::* member) +{ + std::optional common; + for (const auto& processor : processors) { + if (!processor.timestampsEnabled) { + continue; + } + const auto& candidate = processor.*member; + if (!candidate.has_value()) { + return std::nullopt; + } + if (common.has_value() && common != candidate) { + return std::nullopt; + } + common = candidate; + } + return common; +} + +const ProcessorMeta* findProcessor(const std::vector& processors, const std::optional& name) +{ + const auto found = std::find_if(processors.begin(), processors.end(), + [&](const ProcessorMeta& processor) { return processor.name == name; }); + return found != processors.end() ? &*found : nullptr; +} + +struct ResolvedStreamBinding { + std::size_t line = 0U; + std::uint8_t traceBusId = 0U; + std::optional processorName; + const ProcessorMeta* processor = nullptr; +}; + +bool isUsableStreamBinding(const TraceRunReference& reference) +{ + return TraceRunSchema::contributesStreamBinding(reference) && + TraceRunSchema::referenceProblem(reference) == ReferenceProblem::None; +} + +std::vector resolveStreamBindings(const TraceRunConfig& config, + const ProcessorIdentity& processorIdentity, + const std::vector& processors) +{ + std::vector bindings; + for (const auto& reference : config.references) { + if (!isUsableStreamBinding(reference)) { + continue; + } + const auto processorName = processorIdentity.canonicalName(reference.processorName); + bindings.push_back({ + reference.line, + static_cast(reference.stream.value_or(0U)), + processorName, + findProcessor(processors, processorName), + }); + } + return bindings; +} + +std::map +buildTimestampsByTraceBusId(const std::vector& bindings) +{ + std::map result; + for (const auto& binding : bindings) { + CtraceRunTimestampMeta candidate; + candidate.processorName = binding.processorName; + if (binding.processor != nullptr && binding.processor->timestampsEnabled) { + candidate.clockHz = binding.processor->timestampClockHz; + candidate.clockError = binding.processor->timestampClockError; + } + + const auto [found, inserted] = result.emplace(binding.traceBusId, candidate); + if (inserted || (found->second.processorName == candidate.processorName && + found->second.clockHz == candidate.clockHz && found->second.clockError == candidate.clockError)) { + continue; + } + found->second.clockHz.reset(); + found->second.clockError = "CoreSight Trace Bus ID " + std::to_string(binding.traceBusId) + + " is assigned to multiple processors with different timestamps.clock settings"; + } + return result; +} + +std::map +buildTimestampPrescalersByTraceBusId(const TraceRunConfig& config, const std::vector& bindings) +{ + std::map result; + for (const auto& binding : bindings) { + const auto prescaler = binding.processor != nullptr && binding.processor->timestampPrescaler.has_value() + ? *binding.processor->timestampPrescaler + : TraceRunSchema::kDefaultTimestampPrescaler; + const auto [found, inserted] = result.emplace(binding.traceBusId, prescaler); + if (!inserted && found->second != prescaler) { + throw std::runtime_error(configError(config, binding.line, + "CoreSight Trace Bus ID " + std::to_string(binding.traceBusId) + + " is assigned to processors with different " + "timestamps.itm-prescaler values")); + } + } + return result; +} + +std::optional commonItmEnableMask(const std::vector& processors) +{ + std::optional common; + for (const auto& processor : processors) { + if (!processor.itmEnableMask.has_value()) { + return std::nullopt; + } + if (common.has_value() && common != processor.itmEnableMask) { + return std::nullopt; + } + common = processor.itmEnableMask; + } + return common; +} + +std::map +buildItmEnableMasksByTraceBusId(const std::vector& bindings) +{ + std::map> candidates; + std::set ambiguous; + for (const auto& binding : bindings) { + const auto enableMask = binding.processor != nullptr ? binding.processor->itmEnableMask : std::nullopt; + const auto [found, inserted] = candidates.emplace(binding.traceBusId, enableMask); + if (!inserted && found->second != enableMask) { + ambiguous.insert(binding.traceBusId); + } + } + + std::map result; + for (const auto& [traceBusId, enableMask] : candidates) { + if (enableMask.has_value() && ambiguous.find(traceBusId) == ambiguous.end()) { + result.emplace(traceBusId, *enableMask); + } + } + return result; +} + +bool containsDistinctProcessorPrescalers(const std::vector& processors) +{ + std::optional first; + for (const auto& processor : processors) { + if (!processor.timestampsEnabled || !processor.timestampPrescaler.has_value()) { + continue; + } + if (first.has_value() && first != processor.timestampPrescaler) { + return true; + } + first = processor.timestampPrescaler; + } + return false; +} + +bool equivalentSourceMetadata(const CtraceRunSourceMeta& left, const CtraceRunSourceMeta& right) +{ + return left.type == right.type && left.processorName == right.processorName && left.source == right.source && + left.label == right.label && left.symbolAddress == right.symbolAddress && left.valueType == right.valueType && + left.valueSize == right.valueSize && left.symbolTypeError == right.symbolTypeError && + left.symbolSizeError == right.symbolSizeError; +} + +} // namespace + +CtraceRunMeta CtraceRunMeta::fromConfig(const TraceRunConfig& config) +{ + CtraceRunMeta ctraceRunMeta; + ctraceRunMeta.configPath_ = config.path; + + for (const auto& reference : config.references) { + if (!TraceRunSchema::hasConsumedRouteShape(reference) && !TraceRunSchema::contributesStreamBinding(reference)) { + continue; + } + const auto problem = TraceRunSchema::referenceProblem(reference); + if (problem != ReferenceProblem::None && !reference.error.has_value()) { + throw std::runtime_error(referenceProblemMessage(config, reference, problem)); + } + } + for (const auto& setup : config.setups) { + if (!consumesSetup(config, setup) || !setup.timestamps.has_value() || + !setup.timestamps->timestampPrescaler.has_value() || + TraceRunSchema::isTimestampPrescaler(*setup.timestamps->timestampPrescaler)) { + continue; + } + throw std::runtime_error(configError(config, setup.timestamps->line, + setup.timestamps->line > 0U + ? "'timestamps.itm-prescaler' must be one of 1, 4, 16, or 64" + : "ctrace-setup timestamps.itm-prescaler must be one of 1, 4, 16, or 64")); + } + + const auto identity = processorIdentity(config); + std::vector processors; + + for (const auto& setup : config.setups) { + if (!consumesSetup(config, setup)) { + continue; + } + const auto processorName = identity.canonicalName(setup.processorName); + auto& processor = processorMeta(processors, processorName); + if (setup.timestamps.has_value()) { + processor.timestampsEnabled = true; + processor.timestampClockHz = setup.timestamps->clockHz; + processor.timestampClockError = setup.timestamps->clockError; + processor.timestampPrescaler = + setup.timestamps->timestampPrescaler.value_or(TraceRunSchema::kDefaultTimestampPrescaler); + if (setup.timestamps->clockError.has_value()) { + ctraceRunMeta.timestampClockErrors_.push_back(*setup.timestamps->clockError); + } + } + if (setup.itm.has_value()) { + processor.itmEnableMask = setup.itm->enableMask; + } + } + + for (const auto& reference : config.references) { + if (isUsableStreamBinding(reference)) { + (void)processorMeta(processors, identity.canonicalName(reference.processorName)); + } + if (!TraceRunSchema::isUsableReference(reference)) { + continue; + } + for (const auto source : reference.sources) { + ctraceRunMeta.sources_.push_back(sourceMeta(config, reference, source, identity)); + } + } + const auto streamBindings = resolveStreamBindings(config, identity, processors); + ctraceRunMeta.timestampClockHz_ = commonProcessorSetting(processors, &ProcessorMeta::timestampClockHz); + ctraceRunMeta.timestampsByTraceBusId_ = buildTimestampsByTraceBusId(streamBindings); + ctraceRunMeta.timestampPrescaler_ = commonProcessorSetting(processors, &ProcessorMeta::timestampPrescaler); + ctraceRunMeta.timestampPrescalersByTraceBusId_ = buildTimestampPrescalersByTraceBusId(config, streamBindings); + ctraceRunMeta.itmEnableMask_ = commonItmEnableMask(processors); + ctraceRunMeta.itmEnableMasksByTraceBusId_ = buildItmEnableMasksByTraceBusId(streamBindings); + ctraceRunMeta.processorCount_ = processors.size(); + ctraceRunMeta.distinctProcessorPrescalers_ = containsDistinctProcessorPrescalers(processors); + + return ctraceRunMeta; +} + +const std::string& CtraceRunMeta::configPath() const +{ + return configPath_; +} + +const std::optional& CtraceRunMeta::timestampClockHz() const +{ + return timestampClockHz_; +} + +const std::map& CtraceRunMeta::timestampsByTraceBusId() const +{ + return timestampsByTraceBusId_; +} + +const std::optional& CtraceRunMeta::timestampPrescaler() const +{ + return timestampPrescaler_; +} + +const std::map& CtraceRunMeta::timestampPrescalersByTraceBusId() const +{ + return timestampPrescalersByTraceBusId_; +} + +const std::optional& CtraceRunMeta::itmEnableMask() const +{ + return itmEnableMask_; +} + +const std::map& CtraceRunMeta::itmEnableMasksByTraceBusId() const +{ + return itmEnableMasksByTraceBusId_; +} + +const std::vector& CtraceRunMeta::timestampClockErrors() const +{ + return timestampClockErrors_; +} + +bool CtraceRunMeta::hasDistinctProcessorPrescalers() const +{ + return distinctProcessorPrescalers_; +} + +std::size_t CtraceRunMeta::processorCount() const +{ + return processorCount_; +} + +const std::vector& CtraceRunMeta::sources() const +{ + return sources_; +} + +const CtraceRunSourceMeta* CtraceRunMeta::resolveSource(std::string_view type, std::optional traceBusId, + std::uint32_t source) const +{ + const CtraceRunSourceMeta* result = nullptr; + for (const auto& candidate : sources_) { + if (candidate.type != type || candidate.source != source || + (traceBusId.has_value() && candidate.traceBusId != *traceBusId)) { + continue; + } + if (result != nullptr && !equivalentSourceMetadata(*result, candidate)) { + throw std::runtime_error(configPath_ + ": ambiguous metadata for trace route type '" + std::string(type) + + "', Trace Bus ID " + (traceBusId.has_value() ? std::to_string(*traceBusId) : "any") + + ", source " + std::to_string(source)); + } + result = &candidate; + } + return result; +} diff --git a/tools/ctrace/src/tracerun/CtraceRunMeta.hpp b/tools/ctrace/src/tracerun/CtraceRunMeta.hpp new file mode 100644 index 000000000..d32203a5d --- /dev/null +++ b/tools/ctrace/src/tracerun/CtraceRunMeta.hpp @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +struct TraceRunConfig; + +struct CtraceRunSourceMeta { + std::string type; + std::optional processorName; + std::uint8_t traceBusId = 0U; + std::uint32_t source = 0; + std::optional label; + std::optional symbolAddress; + std::string valueType = "unsigned int"; + std::uint64_t valueSize = 4U; + std::optional symbolTypeError; + std::optional symbolSizeError; +}; + +struct CtraceRunTimestampMeta { + std::optional processorName; + std::optional clockHz; + std::optional clockError; +}; + +class CtraceRunMeta { +public: + static CtraceRunMeta fromConfig(const TraceRunConfig& config); + + const std::string& configPath() const; + const std::optional& timestampClockHz() const; + const std::map& timestampsByTraceBusId() const; + const std::optional& timestampPrescaler() const; + const std::map& timestampPrescalersByTraceBusId() const; + const std::optional& itmEnableMask() const; + const std::map& itmEnableMasksByTraceBusId() const; + const std::vector& timestampClockErrors() const; + bool hasDistinctProcessorPrescalers() const; + std::size_t processorCount() const; + const std::vector& sources() const; + + const CtraceRunSourceMeta* resolveSource(std::string_view type, std::optional traceBusId, + std::uint32_t source) const; + +private: + std::string configPath_; + std::optional timestampClockHz_; + std::map timestampsByTraceBusId_; + std::optional timestampPrescaler_; + std::map timestampPrescalersByTraceBusId_; + std::optional itmEnableMask_; + std::map itmEnableMasksByTraceBusId_; + std::vector timestampClockErrors_; + std::size_t processorCount_ = 0; + bool distinctProcessorPrescalers_ = false; + std::vector sources_; +}; diff --git a/tools/ctrace/src/tracerun/TraceRunConfig.hpp b/tools/ctrace/src/tracerun/TraceRunConfig.hpp new file mode 100644 index 000000000..a96a4f383 --- /dev/null +++ b/tools/ctrace/src/tracerun/TraceRunConfig.hpp @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "TraceStreamId.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace TraceRunSchema { + +inline constexpr std::array kTimestampPrescalers{{ + 1U, + 4U, + 16U, + 64U, +}}; +inline constexpr std::uint32_t kDefaultTimestampPrescaler = 1U; +inline constexpr std::string_view kDefaultDwtDataType = "unsigned int"; +inline constexpr std::uint8_t kDefaultDwtDataSize = 4U; + +template +constexpr bool contains(const std::array& values, const Value& candidate) +{ + for (const auto& value : values) { + if (value == candidate) { + return true; + } + } + return false; +} + +constexpr bool isTimestampPrescaler(std::uint32_t prescaler) +{ + return contains(kTimestampPrescalers, prescaler); +} + +constexpr bool isDwtDataType(std::string_view type) +{ + return type == "unsigned int" || type == "signed int" || type == "float"; +} + +constexpr bool isDwtDataSize(std::uint64_t size) +{ + return size == 1U || size == 2U || size == 4U; +} + +constexpr bool supportsSource(std::string_view type) +{ + return type == "dwt" || type == "itm"; +} + +constexpr bool consumesReferenceMetadata(std::string_view type) +{ + return type == "dwt" || type == "itm" || type == "event" || type == "pmu" || type == "pcsample"; +} + +constexpr bool supportsSourceArray(std::string_view type) +{ + return type == "dwt"; +} + +constexpr bool isItmSource(std::uint32_t source) +{ + return source <= 31U; +} + +inline std::optional normalizedProcessorName(std::optional name) +{ + if (name.has_value() && name->empty()) { + name.reset(); + } + return name; +} + +inline bool processorNamesMayBind(const std::optional& left, const std::optional& right) +{ + const auto normalizedLeft = normalizedProcessorName(left); + const auto normalizedRight = normalizedProcessorName(right); + return !normalizedLeft.has_value() || !normalizedRight.has_value() || normalizedLeft == normalizedRight; +} + +} // namespace TraceRunSchema + +struct TraceRunReference { + std::string ctraceRef; + std::string type; + std::optional processorName; + std::optional info; + std::optional warning; + std::optional error; + std::optional symbolAddress; + std::optional label; + // The reader retains the complete YAML value. CtraceRunMeta validates and + // narrows it to the CoreSight ATB trace-ID domain. + std::optional stream; + std::vector sources; + // Index of the referenced ctrace-setup.data entry, derived from the + // specified ctrace-ref path form [/]data#. + std::optional dataSetupIndex; + std::size_t line = 0U; +}; + +namespace TraceRunSchema { + +enum class ReferenceProblem { + None, + DuplicateSource, + InvalidStream, + UnsupportedSourceArray, + InvalidItmSource, +}; + +inline bool isItmChannelZero(const TraceRunReference& reference) +{ + return reference.type == "itm" && reference.sources.size() == 1U && reference.sources.front() == 0U; +} + +inline bool hasConsumedRouteShape(const TraceRunReference& reference) +{ + if (!supportsSource(reference.type) || reference.sources.empty()) { + return false; + } + if (reference.type == "itm") { + return !isItmChannelZero(reference); + } + return true; +} + +inline bool isTimestampReference(const TraceRunReference& reference) +{ + constexpr std::string_view name = "timestamps"; + const auto separator = reference.ctraceRef.rfind('/'); + const auto leaf = separator == std::string::npos ? std::string_view(reference.ctraceRef) + : std::string_view(reference.ctraceRef).substr(separator + 1U); + return leaf == name; +} + +inline bool contributesStreamBinding(const TraceRunReference& reference) +{ + return (reference.type == "dwt" || reference.type == "itm") && + (!reference.sources.empty() || isTimestampReference(reference)); +} + +inline ReferenceProblem referenceProblem(const TraceRunReference& reference) +{ + for (std::size_t left = 0U; left < reference.sources.size(); ++left) { + for (std::size_t right = left + 1U; right < reference.sources.size(); ++right) { + if (reference.sources[left] == reference.sources[right]) { + return ReferenceProblem::DuplicateSource; + } + } + } + if (reference.stream.has_value() && !CoreSight::isAtbTraceId(*reference.stream)) { + return ReferenceProblem::InvalidStream; + } + if (!supportsSourceArray(reference.type) && reference.sources.size() > 1U) { + return ReferenceProblem::UnsupportedSourceArray; + } + if (reference.type == "itm") { + for (const auto source : reference.sources) { + if (!isItmSource(source)) { + return ReferenceProblem::InvalidItmSource; + } + } + } + return ReferenceProblem::None; +} + +inline bool isUsableReference(const TraceRunReference& reference) +{ + return hasConsumedRouteShape(reference) && referenceProblem(reference) == ReferenceProblem::None; +} + +} // namespace TraceRunSchema + +struct TraceRunTimestampSetup { + std::optional clockHz = std::nullopt; + std::optional timestampPrescaler = std::nullopt; + std::optional clockError = std::nullopt; + std::size_t line = 0U; +}; + +struct TraceRunDataSetup { + std::optional symbolType = std::nullopt; + std::optional symbolSize = std::nullopt; + std::optional symbolTypeError = std::nullopt; + std::optional symbolSizeError = std::nullopt; +}; + +struct TraceRunItmSetup { + std::uint32_t enableMask = 0U; +}; + +struct TraceRunSetup { + std::optional processorName; + std::optional timestamps; + std::optional itm; + std::vector data; + std::size_t line = 0U; +}; + +struct TraceRunConfig { + std::string path; + std::vector references; + std::vector setups; +}; diff --git a/tools/ctrace/src/tracerun/TraceRunConfigReader.hpp b/tools/ctrace/src/tracerun/TraceRunConfigReader.hpp new file mode 100644 index 000000000..82c1c2611 --- /dev/null +++ b/tools/ctrace/src/tracerun/TraceRunConfigReader.hpp @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "TraceRunConfig.hpp" + +#include + +class TraceRunConfigReader { +public: + virtual ~TraceRunConfigReader() = default; + + virtual TraceRunConfig read(const std::string& path) const = 0; +}; diff --git a/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp b/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp new file mode 100644 index 000000000..ce616cfee --- /dev/null +++ b/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TraceRunDiscovery.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::string_view ConfigSuffix = ".ctrace-run.yml"; + +bool endsWith(std::string_view value, std::string_view suffix) +{ + return value.size() >= suffix.size() && value.substr(value.size() - suffix.size()) == suffix; +} + +bool isTraceChannel(std::string_view value) +{ + return value == "SWO" || value == "TB" || value == "ER"; +} + +bool isWindowsReservedFilename(std::string_view value) +{ + const auto extension = value.find('.'); + const auto basename = value.substr(0U, extension); + std::string upper; + upper.reserve(basename.size()); + std::transform(basename.begin(), basename.end(), std::back_inserter(upper), [](const char character) { + return static_cast(std::toupper(static_cast(character))); + }); + + if (upper == "CON" || upper == "PRN" || upper == "AUX" || upper == "NUL" || upper == "CONIN$" || upper == "CONOUT$") { + return true; + } + return upper.size() == 4U && (upper.compare(0U, 3U, "COM") == 0 || upper.compare(0U, 3U, "LPT") == 0) && + upper[3] >= '1' && upper[3] <= '9'; +} + +bool isSafeSolutionSetName(std::string_view value) +{ + if (value.empty() || value == "." || value == ".." || value.back() == '.' || value.back() == ' ' || + isWindowsReservedFilename(value)) { + return false; + } + + constexpr std::string_view invalidFilenameCharacters = "<>:\"/\\|?*"; + return std::none_of(value.begin(), value.end(), [invalidFilenameCharacters](const char character) { + const auto byte = static_cast(character); + return byte < 0x20U || invalidFilenameCharacters.find(character) != std::string_view::npos; + }); +} + +void requireReadableConfigFile(const std::filesystem::path& path) +{ + if (!std::filesystem::is_regular_file(path)) { + throw std::runtime_error("trace-run configuration not found: " + path.string()); + } +} + +} // namespace + +std::vector TraceRunDiscovery::selectConfigFiles(const std::filesystem::path& traceDir, + const std::optional& target) +{ + if (!std::filesystem::is_directory(traceDir)) { + throw std::runtime_error("trace directory not found: " + traceDir.string()); + } + + if (target.has_value()) { + if (!isSafeSolutionSetName(*target)) { + throw std::runtime_error("--target must be a solution-set name, got " + *target); + } + auto path = traceDir / (*target + std::string(ConfigSuffix)); + requireReadableConfigFile(path); + return {std::move(path)}; + } + + std::vector configs; + for (const auto& entry : std::filesystem::directory_iterator(traceDir)) { + if (!entry.is_regular_file()) { + continue; + } + const auto filename = entry.path().filename().string(); + if (endsWith(filename, ConfigSuffix) && filename.size() > std::string_view(ConfigSuffix).size()) { + configs.push_back(entry.path()); + } + } + std::sort(configs.begin(), configs.end()); + if (configs.empty()) { + throw std::runtime_error("no *" + std::string(ConfigSuffix) + + " files found in trace directory: " + traceDir.string()); + } + return configs; +} + +std::string TraceRunDiscovery::solutionSetName(const std::filesystem::path& configFile) +{ + const auto filename = configFile.filename().string(); + const auto suffix = std::string_view(ConfigSuffix); + if (!endsWith(filename, suffix) || filename.size() == suffix.size()) { + throw std::runtime_error("expected " + std::string(ConfigSuffix) + ", got " + configFile.string()); + } + return filename.substr(0, filename.size() - suffix.size()); +} + +std::vector TraceRunDiscovery::rawInputs(const std::filesystem::path& configFile) +{ + const auto solutionSet = solutionSetName(configFile); + const auto prefix = solutionSet + "."; + constexpr std::string_view suffix = ".raw"; + const auto directory = configFile.has_parent_path() ? configFile.parent_path() : std::filesystem::path("."); + + std::vector inputs; + for (const auto& entry : std::filesystem::directory_iterator(directory)) { + if (!entry.is_regular_file()) { + continue; + } + const auto filename = entry.path().filename().string(); + if (filename.rfind(prefix, 0) != 0 || !endsWith(filename, suffix)) { + continue; + } + const auto channelSize = filename.size() - prefix.size() - suffix.size(); + if (channelSize == 0U) { + continue; + } + const auto channel = filename.substr(prefix.size(), channelSize); + if (!isTraceChannel(channel)) { + continue; + } + inputs.push_back({entry.path(), channel}); + } + std::sort(inputs.begin(), inputs.end(), + [](const TraceRunRawInput& left, const TraceRunRawInput& right) { return left.path < right.path; }); + return inputs; +} diff --git a/tools/ctrace/src/tracerun/TraceRunDiscovery.hpp b/tools/ctrace/src/tracerun/TraceRunDiscovery.hpp new file mode 100644 index 000000000..f4e521e96 --- /dev/null +++ b/tools/ctrace/src/tracerun/TraceRunDiscovery.hpp @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include +#include +#include +#include + +struct TraceRunRawInput { + std::filesystem::path path; + std::string channel; +}; + +namespace TraceRunDiscovery { + +std::vector selectConfigFiles(const std::filesystem::path& traceDir, + const std::optional& target); +std::string solutionSetName(const std::filesystem::path& configFile); +std::vector rawInputs(const std::filesystem::path& configFile); + +} // namespace TraceRunDiscovery diff --git a/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp b/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp new file mode 100644 index 000000000..bbf64a201 --- /dev/null +++ b/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp @@ -0,0 +1,633 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "YmlTraceRunConfigReader.hpp" + +#include "TraceRunConfig.hpp" +#include "yaml-cpp/exceptions.h" +#include "yaml-cpp/node/node.h" +#include "yaml-cpp/node/type.h" +#include "yaml-cpp/node/impl.h" // IWYU pragma: keep +#include "yaml-cpp/node/convert.h" // IWYU pragma: keep +#include "yaml-cpp/node/iterator.h" +#include "yaml-cpp/node/detail/impl.h" // IWYU pragma: keep +#include "yaml-cpp/node/parse.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Node = YAML::Node; + +std::size_t lineNumber(const Node& node) +{ + return node.Mark().line >= 0 ? static_cast(node.Mark().line + 1) : 0U; +} + +std::string errorMessage(const std::string& path, const Node& node, const std::string& message) +{ + std::ostringstream out; + out << path; + if (lineNumber(node) > 0U) { + out << '(' << lineNumber(node) << ')'; + } + out << ": " << message; + return out.str(); +} + +[[noreturn]] void fail(const std::string& path, const Node& node, const std::string& message) +{ + throw std::runtime_error(errorMessage(path, node, message)); +} + +struct NodeLookup { + Node value{YAML::NodeType::Undefined}; + Node duplicateKey{YAML::NodeType::Undefined}; +}; + +NodeLookup lookupNode(const Node& element, std::string_view tag) +{ + NodeLookup result; + if (!element.IsMap()) { + return result; + } + for (const auto& entry : element) { + if (!entry.first.IsScalar() || entry.first.Scalar() != tag) { + continue; + } + if (result.value) { + if (!result.duplicateKey) { + result.duplicateKey = entry.first; + } + continue; + } + result.value = entry.second; + } + return result; +} + +Node uniqueNode(const std::string& path, const Node& element, std::string_view tag) +{ + auto result = lookupNode(element, tag); + if (result.duplicateKey) { + fail(path, result.duplicateKey, "duplicate '" + std::string(tag) + "' node"); + } + return result.value; +} + +Node uniqueChild(const std::string& path, const Node& element, std::string_view tag) +{ + const auto node = uniqueNode(path, element, tag); + return node && (node.IsMap() || node.IsSequence()) ? node : Node(YAML::NodeType::Undefined); +} + +std::optional optionalAttribute(const std::string& path, const Node& element, std::string_view name) +{ + const auto node = uniqueNode(path, element, name); + if (!node) { + return std::nullopt; + } + if (node.IsNull()) { + return std::string{}; + } + if (!node.IsScalar()) { + return std::nullopt; + } + return node.Scalar(); +} + +std::optional processorNameAttribute(const std::string& path, const Node& element) +{ + const auto node = uniqueNode(path, element, "pname"); + if (!node || node.IsNull()) { + return std::nullopt; + } + if (!node.IsScalar()) { + fail(path, node, "'pname' must be a scalar string"); + } + return TraceRunSchema::normalizedProcessorName(std::optional(node.Scalar())); +} + +std::optional bestEffortProcessorName(const Node& element) +{ + const auto lookup = lookupNode(element, "pname"); + if (lookup.duplicateKey || !lookup.value || lookup.value.IsNull() || !lookup.value.IsScalar()) { + return std::nullopt; + } + return TraceRunSchema::normalizedProcessorName(std::optional(lookup.value.Scalar())); +} + +std::uint64_t unsignedValue(const std::string& path, const Node& element, std::string_view name, + const std::string& value, std::uint64_t maximum) +{ + std::string_view digits(value); + int base = 10; + if (digits.size() >= 2U && digits[0] == '0' && (digits[1] == 'x' || digits[1] == 'X')) { + base = 16; + digits.remove_prefix(2); + } + if (digits.empty()) { + fail(path, element, "'" + std::string(name) + "' must be an unsigned integer"); + } + + std::uint64_t parsed = 0; + const auto result = std::from_chars(digits.data(), digits.data() + digits.size(), parsed, base); + if (result.ec != std::errc{} || result.ptr != digits.data() + digits.size() || parsed > maximum) { + fail(path, element, "'" + std::string(name) + "' must be an unsigned integer in range"); + } + return parsed; +} + +std::optional optionalUnsignedAttribute(const std::string& path, const Node& element, + std::string_view name, std::uint64_t maximum) +{ + const auto value = optionalAttribute(path, element, name); + if (!value.has_value()) { + return std::nullopt; + } + return unsignedValue(path, element, name, *value, maximum); +} + +std::optional deferredUnsignedAttribute(const std::string& path, const Node& element, + std::string_view name, std::uint64_t maximum, + std::optional& error) +{ + try { + return optionalUnsignedAttribute(path, element, name, maximum); + } catch (const std::runtime_error& ex) { + error = ex.what(); + return std::nullopt; + } +} + +std::optional bestEffortUnsignedAttribute(const std::string& path, const Node& element, + std::string_view name, std::uint64_t maximum) +{ + try { + return optionalUnsignedAttribute(path, element, name, maximum); + } catch (const std::runtime_error&) { + return std::nullopt; + } +} + +// ctrace-ref identifies the originating ctrace.yml node. The data index links +// a generated DWT route to its copied ctrace-setup.data metadata. +std::optional dwtDataIndex(std::string_view ctraceRef) +{ + constexpr std::string_view marker = "data#"; + const auto markerPosition = ctraceRef.rfind(marker); + if (markerPosition == std::string_view::npos || (markerPosition != 0U && ctraceRef[markerPosition - 1U] != '/')) { + return std::nullopt; + } + const auto value = ctraceRef.substr(markerPosition + marker.size()); + if (value.empty()) { + return std::nullopt; + } + std::size_t index = 0U; + const auto parsed = std::from_chars(value.data(), value.data() + value.size(), index); + if (parsed.ec != std::errc{} || parsed.ptr != value.data() + value.size()) { + return std::nullopt; + } + return index; +} + +std::set referencedDataSetupIndices(const std::vector& references, + const std::optional& setupProcessorName) +{ + std::set indices; + for (const auto& reference : references) { + if (reference.type != "dwt" || !TraceRunSchema::isUsableReference(reference)) { + continue; + } + if (!TraceRunSchema::processorNamesMayBind(setupProcessorName, reference.processorName)) { + continue; + } + const auto index = reference.dataSetupIndex; + if (index.has_value()) { + indices.insert(*index); + } + } + return indices; +} + +bool hasReferencedDataForProcessor(const std::vector& references, + const std::optional& setupProcessorName) +{ + for (const auto& reference : references) { + if (reference.type == "dwt" && TraceRunSchema::isUsableReference(reference) && + TraceRunSchema::processorNamesMayBind(setupProcessorName, reference.processorName)) { + return true; + } + } + return false; +} + +bool setupDataMayBeConsumed(const Node& setup, const std::vector& references) +{ + bool hasProcessorName = false; + for (const auto& entry : setup) { + if (!entry.first.IsScalar() || entry.first.Scalar() != "pname") { + continue; + } + hasProcessorName = true; + if (!entry.second.IsScalar() && !entry.second.IsNull()) { + // It could name a referenced processor. Let strict parsing report + // it when this setup contains otherwise consumed data. + return true; + } + auto processorName = entry.second.IsScalar() ? std::optional(entry.second.Scalar()) + : std::optional(std::string{}); + processorName = TraceRunSchema::normalizedProcessorName(std::move(processorName)); + if (hasReferencedDataForProcessor(references, processorName)) { + return true; + } + } + return !hasProcessorName && hasReferencedDataForProcessor(references, std::nullopt); +} + +void requireSequence(const std::string& path, const Node& element, std::string_view name) +{ + if (!element.IsSequence()) { + fail(path, element, "'" + std::string(name) + "' must be an array"); + } +} + +Node traceRunRoot(const std::string& path, const Node& document) +{ + if (!document.IsMap()) { + fail(path, document, "expected a YAML map containing 'ctrace-run'"); + } + + const auto root = uniqueNode(path, document, "ctrace-run"); + if (!root) { + fail(path, document, "missing top-level 'ctrace-run' node"); + } + if (!root.IsMap()) { + fail(path, root, "top-level 'ctrace-run' node must be a map"); + } + return root; +} + +std::pair, bool> parseSources(const std::string& path, const Node& reference) +{ + const auto sourceNode = uniqueNode(path, reference, "source"); + if (!sourceNode) { + return {{}, false}; + } + if (sourceNode.IsScalar() || sourceNode.IsNull()) { + const auto scalarSource = sourceNode.IsScalar() ? sourceNode.Scalar() : std::string{}; + return {{static_cast( + unsignedValue(path, sourceNode, "source", scalarSource, std::numeric_limits::max()))}, + false}; + } + + requireSequence(path, sourceNode, "source"); + std::vector sources; + for (const auto& item : sourceNode) { + if (!item.IsScalar() || item.Scalar().empty()) { + fail(path, item, "each 'source' entry must be an unsigned integer"); + } + const auto source = static_cast( + unsignedValue(path, item, "source", item.Scalar(), std::numeric_limits::max())); + sources.push_back(source); + } + return {std::move(sources), true}; +} + +struct ReferenceDiagnostics { + std::optional info; + std::optional warning; + std::optional error; +}; + +ReferenceDiagnostics parseReferenceDiagnostics(const std::string& path, const Node& element) +{ + const auto message = [&](std::string_view name) { + const auto value = optionalAttribute(path, element, name); + if (uniqueChild(path, element, name)) { + fail(path, element, "'" + std::string(name) + "' must be a scalar message"); + } + return value; + }; + return { + message("info"), + message("warning"), + message("error"), + }; +} + +std::optional parseReference(const std::string& path, const Node& element) +{ + if (!element.IsMap()) { + return std::nullopt; + } + + const auto requiredScalar = [&](std::string_view name) { + const auto node = uniqueNode(path, element, name); + if (!node || !node.IsScalar() || node.Scalar().empty()) { + fail(path, node ? node : element, "missing required '" + std::string(name) + "' scalar in ctrace-ref entry"); + } + return node.Scalar(); + }; + + const auto typeLookup = lookupNode(element, "type"); + const auto typeNode = typeLookup.value; + if (typeLookup.duplicateKey || !typeNode || !typeNode.IsScalar() || typeNode.Scalar().empty()) { + return std::nullopt; + } + + TraceRunReference reference; + reference.type = typeNode.Scalar(); + if (!TraceRunSchema::consumesReferenceMetadata(reference.type)) { + return std::nullopt; + } + reference.ctraceRef = requiredScalar("ctrace-ref"); + reference.line = lineNumber(element); + if (reference.type == "dwt") { + reference.dataSetupIndex = dwtDataIndex(reference.ctraceRef); + } + + const auto diagnostics = parseReferenceDiagnostics(path, element); + reference.info = diagnostics.info; + reference.warning = diagnostics.warning; + reference.error = diagnostics.error; + + const auto parseStream = [&]() -> std::optional { + if (uniqueChild(path, element, "stream")) { + fail(path, element, "'stream' must be a scalar unsigned integer"); + } + const auto stream = optionalUnsignedAttribute(path, element, "stream", std::numeric_limits::max()); + if (!stream.has_value()) { + return std::nullopt; + } + return static_cast(*stream); + }; + + const auto parseRoute = [&]() { + reference.processorName = processorNameAttribute(path, element); + reference.stream = parseStream(); + auto parsedSources = parseSources(path, element); + if (reference.type == "itm" && parsedSources.second) { + fail(path, element, "ITM 'source' must be a single channel number"); + } + reference.sources = std::move(parsedSources.first); + if (reference.type == "dwt") { + reference.symbolAddress = + bestEffortUnsignedAttribute(path, element, "symbol-address", std::numeric_limits::max()); + } + reference.label = optionalAttribute(path, element, "label"); + }; + + if (!TraceRunSchema::supportsSource(reference.type)) { + // These source types currently contribute diagnostics only. Preserve + // a valid processor name for log context, but do not validate fields + // that no ctrace decoder or output consumes. + reference.processorName = bestEffortProcessorName(element); + return reference; + } + + if (diagnostics.error.has_value()) { + auto diagnosticReference = reference; + diagnosticReference.processorName = bestEffortProcessorName(element); + try { + parseRoute(); + if (TraceRunSchema::isUsableReference(reference) || TraceRunSchema::isItmChannelZero(reference)) { + return reference; + } + return diagnosticReference; + } catch (const std::runtime_error&) { + return diagnosticReference; + } + } + + parseRoute(); + return reference; +} + +std::vector parseReferences(const std::string& path, const Node& root) +{ + const auto referencesNode = uniqueNode(path, root, "ctrace-refs"); + if (!referencesNode) { + return {}; + } + requireSequence(path, referencesNode, "ctrace-refs"); + + std::vector references; + for (const auto& item : referencesNode) { + auto reference = parseReference(path, item); + if (reference.has_value()) { + references.push_back(std::move(*reference)); + } + } + return references; +} + +TraceRunSetup parseSetup(const std::string& path, const Node& element, const std::vector& references) +{ + if (!element.IsMap()) { + return {}; + } + + TraceRunSetup setup; + setup.line = lineNumber(element); + setup.processorName = processorNameAttribute(path, element); + const auto referencedDataIndices = referencedDataSetupIndices(references, setup.processorName); + + const auto timestampsLookup = lookupNode(element, "timestamps"); + const auto timestampsNode = timestampsLookup.value; + if (timestampsLookup.duplicateKey) { + setup.timestamps = TraceRunTimestampSetup{}; + setup.timestamps->line = lineNumber(timestampsLookup.duplicateKey); + setup.timestamps->clockError = errorMessage(path, timestampsLookup.duplicateKey, "duplicate 'timestamps' setting"); + } else if (timestampsNode && (timestampsNode.IsScalar() || timestampsNode.IsNull())) { + setup.timestamps = TraceRunTimestampSetup{}; + setup.timestamps->line = lineNumber(timestampsNode); + if (timestampsNode.IsScalar() && !timestampsNode.Scalar().empty()) { + setup.timestamps->clockError = "'timestamps' must be empty or a map"; + } + } else if (timestampsNode) { + if (!timestampsNode.IsMap()) { + setup.timestamps = TraceRunTimestampSetup{}; + setup.timestamps->line = lineNumber(timestampsNode); + setup.timestamps->clockError = "'timestamps' must be empty or a map"; + } else { + TraceRunTimestampSetup timestamps; + timestamps.line = lineNumber(timestampsNode); + const auto clock = lookupNode(timestampsNode, "clock"); + if (clock.duplicateKey) { + timestamps.clockError = errorMessage(path, clock.duplicateKey, "duplicate 'timestamps.clock' setting"); + } else if (clock.value && !clock.value.IsScalar() && !clock.value.IsNull()) { + timestamps.clockError = "'timestamps.clock' must be a scalar unsigned integer"; + } else { + timestamps.clockHz = deferredUnsignedAttribute( + path, timestampsNode, "clock", std::numeric_limits::max(), timestamps.clockError); + } + if (uniqueChild(path, timestampsNode, "itm-prescaler")) { + fail(path, timestampsNode, "'timestamps.itm-prescaler' must be a scalar unsigned integer"); + } + const auto prescaler = + optionalUnsignedAttribute(path, timestampsNode, "itm-prescaler", std::numeric_limits::max()); + if (prescaler.has_value()) { + timestamps.timestampPrescaler = static_cast(*prescaler); + } + setup.timestamps = std::move(timestamps); + } + } + + const auto itmNode = uniqueNode(path, element, "itm"); + if (itmNode) { + if (!itmNode.IsMap()) { + fail(path, itmNode, "'itm' must be a map containing 'enable'"); + } + const auto enableNode = uniqueNode(path, itmNode, "enable"); + if (!enableNode || !enableNode.IsScalar() || enableNode.Scalar().empty()) { + fail(path, enableNode ? enableNode : itmNode, "'itm.enable' is required and must be a scalar unsigned integer"); + } + setup.itm = TraceRunItmSetup{static_cast( + unsignedValue(path, enableNode, "itm.enable", enableNode.Scalar(), std::numeric_limits::max()))}; + } + + const auto dataLookup = referencedDataIndices.empty() ? NodeLookup{} : lookupNode(element, "data"); + if (dataLookup.duplicateKey) { + return setup; + } + const auto dataNode = dataLookup.value; + if (dataNode) { + if (!dataNode.IsSequence()) { + return setup; + } + std::size_t index = 0U; + bool foundReferencedEntry = false; + for (const auto& item : dataNode) { + if (referencedDataIndices.find(index++) == referencedDataIndices.end()) { + setup.data.emplace_back(); + continue; + } + foundReferencedEntry = true; + TraceRunDataSetup data; + if (!item.IsMap()) { + setup.data.push_back(std::move(data)); + continue; + } + const auto type = lookupNode(item, "symbol-type"); + if (type.duplicateKey) { + data.symbolTypeError = "duplicate 'data.symbol-type' setting"; + } else if (type.value && !type.value.IsScalar() && !type.value.IsNull()) { + data.symbolTypeError = "'data.symbol-type' must be a scalar string"; + } else if (type.value && !type.value.IsNull()) { + data.symbolType = optionalAttribute(path, item, "symbol-type"); + } + const auto size = lookupNode(item, "symbol-size"); + if (size.duplicateKey) { + data.symbolSizeError = "duplicate 'data.symbol-size' setting"; + } else if (size.value && !size.value.IsScalar() && !size.value.IsNull()) { + data.symbolSizeError = "'data.symbol-size' must be a scalar unsigned integer"; + } else if (size.value && !size.value.IsNull()) { + data.symbolSize = deferredUnsignedAttribute(path, item, "symbol-size", + std::numeric_limits::max(), data.symbolSizeError); + } + setup.data.push_back(std::move(data)); + } + if (!foundReferencedEntry) { + setup.data.clear(); + } + } + return setup; +} + +std::vector parseSetups(const std::string& path, const Node& root, + const std::vector& references) +{ + const auto setupLookup = lookupNode(root, "ctrace-setup"); + if (setupLookup.duplicateKey) { + return {}; + } + const auto setupNode = setupLookup.value; + if (!setupNode) { + return {}; + } + if (!setupNode.IsSequence()) { + return {}; + } + + std::vector setups; + for (const auto& item : setupNode) { + if (!item.IsMap()) { + continue; + } + const auto timestamps = lookupNode(item, "timestamps"); + const auto itm = lookupNode(item, "itm"); + const auto data = lookupNode(item, "data"); + const auto consumesData = (data.value || data.duplicateKey) && setupDataMayBeConsumed(item, references); + if (!timestamps.value && !timestamps.duplicateKey && !itm.value && !itm.duplicateKey && !consumesData) { + continue; + } + // The copied ctrace.yml setup semantics define the presence of + // 'disable' itself as sufficient to ignore the complete list entry. + // Its value therefore has no schema that ctrace needs to validate. + const auto disable = lookupNode(item, "disable"); + if (disable.value || disable.duplicateKey) { + continue; + } + auto setup = parseSetup(path, item, references); + if (!setup.timestamps.has_value() && !setup.itm.has_value() && setup.data.empty()) { + continue; + } + setups.push_back(std::move(setup)); + } + return setups; +} + +} // namespace + +TraceRunConfig YmlTraceRunConfigReader::read(const std::string& path) const +{ + if (path.empty()) { + throw std::runtime_error("trace-run configuration path is empty"); + } + + std::vector documents; + try { + documents = YAML::LoadAllFromFile(path); + } catch (const YAML::Exception& error) { + std::ostringstream message; + message << "failed to parse trace-run configuration: " << path; + if (error.mark.line >= 0) { + message << '(' << (error.mark.line + 1); + if (error.mark.column >= 0) { + message << ',' << (error.mark.column + 1); + } + message << ')'; + } + message << ": " << error.msg; + throw std::runtime_error(message.str()); + } + if (documents.size() != 1U) { + const auto location = documents.size() > 1U ? documents[1] : Node(YAML::NodeType::Undefined); + fail(path, location, "expected exactly one YAML document"); + } + + const auto root = traceRunRoot(path, documents.front()); + + TraceRunConfig config; + config.path = path; + config.references = parseReferences(path, root); + config.setups = parseSetups(path, root, config.references); + return config; +} diff --git a/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.hpp b/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.hpp new file mode 100644 index 000000000..c60c7dedf --- /dev/null +++ b/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.hpp @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "TraceRunConfig.hpp" +#include "TraceRunConfigReader.hpp" + +#include + +class YmlTraceRunConfigReader final : public TraceRunConfigReader { +public: + TraceRunConfig read(const std::string& path) const override; +}; diff --git a/tools/ctrace/test/CMakeLists.txt b/tools/ctrace/test/CMakeLists.txt index 342a2184d..f6a5e6283 100644 --- a/tools/ctrace/test/CMakeLists.txt +++ b/tools/ctrace/test/CMakeLists.txt @@ -3,3 +3,4 @@ # SPDX-License-Identifier: Apache-2.0 add_subdirectory(unit) +add_subdirectory(integration) diff --git a/tools/ctrace/test/data/Blinky+STM32H747I-EVAL/Blinky+STM32H747I-EVAL.SWO.csv b/tools/ctrace/test/data/Blinky+STM32H747I-EVAL/Blinky+STM32H747I-EVAL.SWO.csv new file mode 100644 index 000000000..3016a5a53 --- /dev/null +++ b/tools/ctrace/test/data/Blinky+STM32H747I-EVAL/Blinky+STM32H747I-EVAL.SWO.csv @@ -0,0 +1,532 @@ +cycles,stream,type,source,value,pc,offset,note +74999,0,dwt,0,0x000000c2,0x0800b8aa,, +149998,0,dwt,0,0x000000c3,0x0800b8aa,, +224997,0,dwt,0,0x000000c4,0x0800b8aa,, +299996,0,dwt,0,0x000000c5,0x0800b8aa,, +374995,0,dwt,0,0x000000c6,0x0800b8aa,, +449994,0,dwt,0,0x000000c7,0x0800b8aa,, +524993,0,dwt,0,0x000000c8,0x0800b8aa,, +599992,0,dwt,0,0x000000c9,0x0800b8aa,, +674996,0,dwt,0,0x000000ca,0x0800b8aa,, +749990,0,dwt,0,0x000000cb,0x0800b8aa,, +824989,0,dwt,0,0x000000cc,0x0800b8aa,, +899988,0,dwt,0,0x000000cd,0x0800b8aa,, +974987,0,dwt,0,0x000000ce,0x0800b8aa,, +1049986,0,dwt,0,0x000000cf,0x0800b8aa,, +1124985,0,dwt,0,0x000000d0,0x0800b8aa,, +1199984,0,dwt,0,0x000000d1,0x0800b8aa,, +1274983,0,dwt,0,0x000000d2,0x0800b8aa,, +1349982,0,dwt,0,0x000000d3,0x0800b8aa,, +1424981,0,dwt,0,0x000000d4,0x0800b8aa,, +1499980,0,dwt,0,0x000000d5,0x0800b8aa,, +1574979,0,dwt,0,0x000000d6,0x0800b8aa,, +1649978,0,dwt,0,0x000000d7,0x0800b8aa,, +1724977,0,dwt,0,0x000000d8,0x0800b8aa,, +1799976,0,dwt,0,0x000000d9,0x0800b8aa,, +1874975,0,dwt,0,0x000000da,0x0800b8aa,, +1949974,0,dwt,0,0x000000db,0x0800b8aa,, +2024973,0,dwt,0,0x000000dc,0x0800b8aa,, +2099972,0,dwt,0,0x000000dd,0x0800b8aa,, +2174971,0,dwt,0,0x000000de,0x0800b8aa,, +2249972,0,dwt,0,0x000000df,0x0800b8aa,, +2324969,0,dwt,0,0x000000e0,0x0800b8aa,, +2399968,0,dwt,0,0x000000e1,0x0800b8aa,, +2474967,0,dwt,0,0x000000e2,0x0800b8aa,, +2549966,0,dwt,0,0x000000e3,0x0800b8aa,, +2624965,0,dwt,0,0x000000e4,0x0800b8aa,, +2699964,0,dwt,0,0x000000e5,0x0800b8aa,, +2774963,0,dwt,0,0x000000e6,0x0800b8aa,, +2849962,0,dwt,0,0x000000e7,0x0800b8aa,, +2924961,0,dwt,0,0x000000e8,0x0800b8aa,, +2999960,0,dwt,0,0x000000e9,0x0800b8aa,, +3074959,0,dwt,0,0x000000ea,0x0800b8aa,, +3149958,0,dwt,0,0x000000eb,0x0800b8aa,, +3224957,0,dwt,0,0x000000ec,0x0800b8aa,, +3299956,0,dwt,0,0x000000ed,0x0800b8aa,, +3374955,0,dwt,0,0x000000ee,0x0800b8aa,, +3449954,0,dwt,0,0x000000ef,0x0800b8aa,, +3524953,0,dwt,0,0x000000f0,0x0800b8aa,, +3599952,0,dwt,0,0x000000f1,0x0800b8aa,, +3674951,0,dwt,0,0x000000f2,0x0800b8aa,, +3749950,0,dwt,0,0x000000f3,0x0800b8aa,, +3824951,0,dwt,0,0x000000f4,0x0800b8aa,, +3899948,0,dwt,0,0x000000f5,0x0800b8aa,, +3974947,0,dwt,0,0x000000f6,0x0800b8aa,, +4049946,0,dwt,0,0x000000f7,0x0800b8aa,, +4124945,0,dwt,0,0x000000f8,0x0800b8aa,, +4199944,0,dwt,0,0x000000f9,0x0800b8aa,, +4274943,0,dwt,0,0x000000fa,0x0800b8aa,, +4349942,0,dwt,0,0x000000fb,0x0800b8aa,, +4424941,0,dwt,0,0x000000fc,0x0800b8aa,, +4499940,0,dwt,0,0x000000fd,0x0800b8aa,, +4574939,0,dwt,0,0x000000fe,0x0800b8aa,, +4649938,0,dwt,0,0x000000ff,0x0800b8aa,, +4724937,0,dwt,0,0x00000100,0x0800b8aa,, +4799936,0,dwt,0,0x00000101,0x0800b8aa,, +4874935,0,dwt,0,0x00000102,0x0800b8aa,, +4949934,0,dwt,0,0x00000103,0x0800b8aa,, +5024933,0,dwt,0,0x00000104,0x0800b8aa,, +5099932,0,dwt,0,0x00000105,0x0800b8aa,, +5174931,0,dwt,0,0x00000106,0x0800b8aa,, +5249930,0,dwt,0,0x00000107,0x0800b8aa,, +5324929,0,dwt,0,0x00000108,0x0800b8aa,, +5399930,0,dwt,0,0x00000109,0x0800b8aa,, +5474927,0,dwt,0,0x0000010a,0x0800b8aa,, +5549926,0,dwt,0,0x0000010b,0x0800b8aa,, +5624925,0,dwt,0,0x0000010c,0x0800b8aa,, +5699924,0,dwt,0,0x0000010d,0x0800b8aa,, +5774923,0,dwt,0,0x0000010e,0x0800b8aa,, +5849922,0,dwt,0,0x0000010f,0x0800b8aa,, +5924921,0,dwt,0,0x00000110,0x0800b8aa,, +5999920,0,dwt,0,0x00000111,0x0800b8aa,, +6074919,0,dwt,0,0x00000112,0x0800b8aa,, +6149918,0,dwt,0,0x00000113,0x0800b8aa,, +6224917,0,dwt,0,0x00000114,0x0800b8aa,, +6299916,0,dwt,0,0x00000115,0x0800b8aa,, +6374915,0,dwt,0,0x00000116,0x0800b8aa,, +6449914,0,dwt,0,0x00000117,0x0800b8aa,, +6524913,0,dwt,0,0x00000118,0x0800b8aa,, +6599912,0,dwt,0,0x00000119,0x0800b8aa,, +6674911,0,dwt,0,0x0000011a,0x0800b8aa,, +6749910,0,dwt,0,0x0000011b,0x0800b8aa,, +6824909,0,dwt,0,0x0000011c,0x0800b8aa,, +6899908,0,dwt,0,0x0000011d,0x0800b8aa,, +6974909,0,dwt,0,0x0000011e,0x0800b8aa,, +7049906,0,dwt,0,0x0000011f,0x0800b8aa,, +7124905,0,dwt,0,0x00000120,0x0800b8aa,, +7199904,0,dwt,0,0x00000121,0x0800b8aa,, +7274903,0,dwt,0,0x00000122,0x0800b8aa,, +7349902,0,dwt,0,0x00000123,0x0800b8aa,, +7424901,0,dwt,0,0x00000124,0x0800b8aa,, +7424901,0,dwt,0,0x00000915,0x0800b8aa,, +7499900,0,dwt,0,0x00000916,0x0800b8aa,, +7574899,0,dwt,0,0x00000917,0x0800b8aa,, +7649898,0,dwt,0,0x00000918,0x0800b8aa,, +7724897,0,dwt,0,0x00000919,0x0800b8aa,, +7799896,0,dwt,0,0x0000091a,0x0800b8aa,, +7874895,0,dwt,0,0x0000091b,0x0800b8aa,, +7949894,0,dwt,0,0x0000091c,0x0800b8aa,, +8024893,0,dwt,0,0x0000091d,0x0800b8aa,, +8099892,0,dwt,0,0x0000091e,0x0800b8aa,, +8174891,0,dwt,0,0x0000091f,0x0800b8aa,, +8249890,0,dwt,0,0x00000920,0x0800b8aa,, +8324889,0,dwt,0,0x00000921,0x0800b8aa,, +8399888,0,dwt,0,0x00000922,0x0800b8aa,, +8474887,0,dwt,0,0x00000923,0x0800b8aa,, +8549886,0,dwt,0,0x00000924,0x0800b8aa,, +8624885,0,dwt,0,0x00000925,0x0800b8aa,, +8699884,0,dwt,0,0x00000926,0x0800b8aa,, +8774883,0,dwt,0,0x00000927,0x0800b8aa,, +8849884,0,dwt,0,0x00000928,0x0800b8aa,, +8924881,0,dwt,0,0x00000929,0x0800b8aa,, +8999880,0,dwt,0,0x0000092a,0x0800b8aa,, +9074879,0,dwt,0,0x0000092b,0x0800b8aa,, +9149878,0,dwt,0,0x0000092c,0x0800b8aa,, +9224877,0,dwt,0,0x0000092d,0x0800b8aa,, +9299876,0,dwt,0,0x0000092e,0x0800b8aa,, +9374875,0,dwt,0,0x0000092f,0x0800b8aa,, +9449874,0,dwt,0,0x00000930,0x0800b8aa,, +9524873,0,dwt,0,0x00000931,0x0800b8aa,, +9599872,0,dwt,0,0x00000932,0x0800b8aa,, +9674871,0,dwt,0,0x00000933,0x0800b8aa,, +9749870,0,dwt,0,0x00000934,0x0800b8aa,, +9824869,0,dwt,0,0x00000935,0x0800b8aa,, +9899868,0,dwt,0,0x00000936,0x0800b8aa,, +9974867,0,dwt,0,0x00000937,0x0800b8aa,, +10049866,0,dwt,0,0x00000938,0x0800b8aa,, +10124865,0,dwt,0,0x00000939,0x0800b8aa,, +10199864,0,dwt,0,0x0000093a,0x0800b8aa,, +10274863,0,dwt,0,0x0000093b,0x0800b8aa,, +10349862,0,dwt,0,0x0000093c,0x0800b8aa,, +10424863,0,dwt,0,0x0000093d,0x0800b8aa,, +10499860,0,dwt,0,0x0000093e,0x0800b8aa,, +10574859,0,dwt,0,0x0000093f,0x0800b8aa,, +10649858,0,dwt,0,0x00000940,0x0800b8aa,, +10724857,0,dwt,0,0x00000941,0x0800b8aa,, +10799856,0,dwt,0,0x00000942,0x0800b8aa,, +10874855,0,dwt,0,0x00000943,0x0800b8aa,, +10949854,0,dwt,0,0x00000944,0x0800b8aa,, +11024853,0,dwt,0,0x00000945,0x0800b8aa,, +11099852,0,dwt,0,0x00000946,0x0800b8aa,, +11174851,0,dwt,0,0x00000947,0x0800b8aa,, +11249850,0,dwt,0,0x00000948,0x0800b8aa,, +11324849,0,dwt,0,0x00000949,0x0800b8aa,, +11399848,0,dwt,0,0x0000094a,0x0800b8aa,, +11474847,0,dwt,0,0x0000094b,0x0800b8aa,, +11549846,0,dwt,0,0x0000094c,0x0800b8aa,, +11624845,0,dwt,0,0x0000094d,0x0800b8aa,, +11699844,0,dwt,0,0x0000094e,0x0800b8aa,, +11774843,0,dwt,0,0x0000094f,0x0800b8aa,, +11849842,0,dwt,0,0x00000950,0x0800b8aa,, +11924841,0,dwt,0,0x00000951,0x0800b8aa,, +11999842,0,dwt,0,0x00000952,0x0800b8aa,, +12074839,0,dwt,0,0x00000953,0x0800b8aa,, +12149838,0,dwt,0,0x00000954,0x0800b8aa,, +12224837,0,dwt,0,0x00000955,0x0800b8aa,, +12299836,0,dwt,0,0x00000956,0x0800b8aa,, +12374835,0,dwt,0,0x00000957,0x0800b8aa,, +12449834,0,dwt,0,0x00000958,0x0800b8aa,, +12524833,0,dwt,0,0x00000959,0x0800b8aa,, +12599832,0,dwt,0,0x0000095a,0x0800b8aa,, +12674831,0,dwt,0,0x0000095b,0x0800b8aa,, +12749830,0,dwt,0,0x0000095c,0x0800b8aa,, +12824829,0,dwt,0,0x0000095d,0x0800b8aa,, +12899828,0,dwt,0,0x0000095e,0x0800b8aa,, +12974827,0,dwt,0,0x0000095f,0x0800b8aa,, +13049826,0,dwt,0,0x00000960,0x0800b8aa,, +13124825,0,dwt,0,0x00000961,0x0800b8aa,, +13199829,0,dwt,0,0x00000962,0x0800b8aa,, +13274823,0,dwt,0,0x00000963,0x0800b8aa,, +13349822,0,dwt,0,0x00000964,0x0800b8aa,, +13424821,0,dwt,0,0x00000965,0x0800b8aa,, +13499820,0,dwt,0,0x00000966,0x0800b8aa,, +13574819,0,dwt,0,0x00000967,0x0800b8aa,, +13649818,0,dwt,0,0x00000968,0x0800b8aa,, +13724817,0,dwt,0,0x00000969,0x0800b8aa,, +13799816,0,dwt,0,0x0000096a,0x0800b8aa,, +13874815,0,dwt,0,0x0000096b,0x0800b8aa,, +13949814,0,dwt,0,0x0000096c,0x0800b8aa,, +14024813,0,dwt,0,0x0000096d,0x0800b8aa,, +14099812,0,dwt,0,0x0000096e,0x0800b8aa,, +14174811,0,dwt,0,0x0000096f,0x0800b8aa,, +14249810,0,dwt,0,0x00000970,0x0800b8aa,, +14324809,0,dwt,0,0x00000971,0x0800b8aa,, +14399808,0,dwt,0,0x00000972,0x0800b8aa,, +14474807,0,dwt,0,0x00000973,0x0800b8aa,, +14549806,0,dwt,0,0x00000974,0x0800b8aa,, +14624805,0,dwt,0,0x00000975,0x0800b8aa,, +14699804,0,dwt,0,0x00000976,0x0800b8aa,, +14774805,0,dwt,0,0x00000977,0x0800b8aa,, +14849802,0,dwt,0,0x00000978,0x0800b8aa,, +14924801,0,dwt,0,0x00000979,0x0800b8aa,, +14999800,0,dwt,0,0x0000097a,0x0800b8aa,, +15074799,0,dwt,0,0x0000097b,0x0800b8aa,, +15149798,0,dwt,0,0x0000097c,0x0800b8aa,, +15224797,0,dwt,0,0x0000097d,0x0800b8aa,, +15299796,0,dwt,0,0x0000097e,0x0800b8aa,, +15374795,0,dwt,0,0x0000097f,0x0800b8aa,, +15449794,0,dwt,0,0x00000980,0x0800b8aa,, +15524793,0,dwt,0,0x00000981,0x0800b8aa,, +15599792,0,dwt,0,0x00000982,0x0800b8aa,, +15674791,0,dwt,0,0x00000983,0x0800b8aa,, +15749790,0,dwt,0,0x00000984,0x0800b8aa,, +15824789,0,dwt,0,0x00000985,0x0800b8aa,, +15899788,0,dwt,0,0x00000986,0x0800b8aa,, +15974787,0,dwt,0,0x00000987,0x0800b8aa,, +16049786,0,dwt,0,0x00000988,0x0800b8aa,, +16124785,0,dwt,0,0x00000989,0x0800b8aa,, +16199784,0,dwt,0,0x0000098a,0x0800b8aa,, +16274783,0,dwt,0,0x0000098b,0x0800b8aa,, +16349784,0,dwt,0,0x0000098c,0x0800b8aa,, +16424781,0,dwt,0,0x0000098d,0x0800b8aa,, +16499780,0,dwt,0,0x0000098e,0x0800b8aa,, +16574779,0,dwt,0,0x0000098f,0x0800b8aa,, +16649778,0,dwt,0,0x00000990,0x0800b8aa,, +16724777,0,dwt,0,0x00000991,0x0800b8aa,, +16799776,0,dwt,0,0x00000992,0x0800b8aa,, +16874775,0,dwt,0,0x00000993,0x0800b8aa,, +16949774,0,dwt,0,0x00000994,0x0800b8aa,, +17024773,0,dwt,0,0x00000995,0x0800b8aa,, +17099772,0,dwt,0,0x00000996,0x0800b8aa,, +17174771,0,dwt,0,0x00000997,0x0800b8aa,, +17249770,0,dwt,0,0x00000998,0x0800b8aa,, +17324769,0,dwt,0,0x00000999,0x0800b8aa,, +17399768,0,dwt,0,0x0000099a,0x0800b8aa,, +17474767,0,dwt,0,0x0000099b,0x0800b8aa,, +17549766,0,dwt,0,0x0000099c,0x0800b8aa,, +17624765,0,dwt,0,0x0000099d,0x0800b8aa,, +17699764,0,dwt,0,0x0000099e,0x0800b8aa,, +17774763,0,dwt,0,0x0000099f,0x0800b8aa,, +17849762,0,dwt,0,0x000009a0,0x0800b8aa,, +17924763,0,dwt,0,0x000009a1,0x0800b8aa,, +17999760,0,dwt,0,0x000009a2,0x0800b8aa,, +18074759,0,dwt,0,0x000009a3,0x0800b8aa,, +18149758,0,dwt,0,0x000009a4,0x0800b8aa,, +18224757,0,dwt,0,0x000009a5,0x0800b8aa,, +18299756,0,dwt,0,0x000009a6,0x0800b8aa,, +18374755,0,dwt,0,0x000009a7,0x0800b8aa,, +18449754,0,dwt,0,0x000009a8,0x0800b8aa,, +18524753,0,dwt,0,0x000009a9,0x0800b8aa,, +18599752,0,dwt,0,0x000009aa,0x0800b8aa,, +18674751,0,dwt,0,0x000009ab,0x0800b8aa,, +18749750,0,dwt,0,0x000009ac,0x0800b8aa,, +18824749,0,dwt,0,0x000009ad,0x0800b8aa,, +18899748,0,dwt,0,0x000009ae,0x0800b8aa,, +18974747,0,dwt,0,0x000009af,0x0800b8aa,, +19049746,0,dwt,0,0x000009b0,0x0800b8aa,, +19124745,0,dwt,0,0x000009b1,0x0800b8aa,, +19199744,0,dwt,0,0x000009b2,0x0800b8aa,, +19274743,0,dwt,0,0x000009b3,0x0800b8aa,, +19349742,0,dwt,0,0x000009b4,0x0800b8aa,, +19424741,0,dwt,0,0x000009b5,0x0800b8aa,, +19499742,0,dwt,0,0x000009b6,0x0800b8aa,, +19574739,0,dwt,0,0x000009b7,0x0800b8aa,, +19649738,0,dwt,0,0x000009b8,0x0800b8aa,, +19724737,0,dwt,0,0x000009b9,0x0800b8aa,, +19799736,0,dwt,0,0x000009ba,0x0800b8aa,, +19874735,0,dwt,0,0x000009bb,0x0800b8aa,, +19949734,0,dwt,0,0x000009bc,0x0800b8aa,, +20024733,0,dwt,0,0x000009bd,0x0800b8aa,, +20099732,0,dwt,0,0x000009be,0x0800b8aa,, +20174731,0,dwt,0,0x000009bf,0x0800b8aa,, +20249730,0,dwt,0,0x000009c0,0x0800b8aa,, +20324729,0,dwt,0,0x000009c1,0x0800b8aa,, +20399728,0,dwt,0,0x000009c2,0x0800b8aa,, +20474727,0,dwt,0,0x000009c3,0x0800b8aa,, +20549726,0,dwt,0,0x000009c4,0x0800b8aa,, +20624725,0,dwt,0,0x000009c5,0x0800b8aa,, +20699729,0,dwt,0,0x000009c6,0x0800b8aa,, +20774723,0,dwt,0,0x000009c7,0x0800b8aa,, +20849722,0,dwt,0,0x000009c8,0x0800b8aa,, +20924721,0,dwt,0,0x000009c9,0x0800b8aa,, +20999720,0,dwt,0,0x000009ca,0x0800b8aa,, +21074719,0,dwt,0,0x000009cb,0x0800b8aa,, +21149718,0,dwt,0,0x000009cc,0x0800b8aa,, +21224717,0,dwt,0,0x000009cd,0x0800b8aa,, +21299716,0,dwt,0,0x000009ce,0x0800b8aa,, +21374715,0,dwt,0,0x000009cf,0x0800b8aa,, +21449714,0,dwt,0,0x000009d0,0x0800b8aa,, +21524713,0,dwt,0,0x000009d1,0x0800b8aa,, +21599712,0,dwt,0,0x000009d2,0x0800b8aa,, +21674711,0,dwt,0,0x000009d3,0x0800b8aa,, +21749710,0,dwt,0,0x000009d4,0x0800b8aa,, +21824709,0,dwt,0,0x000009d5,0x0800b8aa,, +21899708,0,dwt,0,0x000009d6,0x0800b8aa,, +21974707,0,dwt,0,0x000009d7,0x0800b8aa,, +22049706,0,dwt,0,0x000009d8,0x0800b8aa,, +22124705,0,dwt,0,0x000009d9,0x0800b8aa,, +22199704,0,dwt,0,0x000009da,0x0800b8aa,, +22274705,0,dwt,0,0x000009db,0x0800b8aa,, +22349702,0,dwt,0,0x000009dc,0x0800b8aa,, +22424701,0,dwt,0,0x000009dd,0x0800b8aa,, +22499700,0,dwt,0,0x000009de,0x0800b8aa,, +22574699,0,dwt,0,0x000009df,0x0800b8aa,, +22649698,0,dwt,0,0x000009e0,0x0800b8aa,, +22724697,0,dwt,0,0x000009e1,0x0800b8aa,, +22799696,0,dwt,0,0x000009e2,0x0800b8aa,, +22874695,0,dwt,0,0x000009e3,0x0800b8aa,, +22949694,0,dwt,0,0x000009e4,0x0800b8aa,, +23024693,0,dwt,0,0x000009e5,0x0800b8aa,, +23099692,0,dwt,0,0x000009e6,0x0800b8aa,, +23174691,0,dwt,0,0x000009e7,0x0800b8aa,, +23249690,0,dwt,0,0x000009e8,0x0800b8aa,, +23324689,0,dwt,0,0x000009e9,0x0800b8aa,, +23399688,0,dwt,0,0x000009ea,0x0800b8aa,, +23474687,0,dwt,0,0x000009eb,0x0800b8aa,, +23549686,0,dwt,0,0x000009ec,0x0800b8aa,, +23624685,0,dwt,0,0x000009ed,0x0800b8aa,, +23699684,0,dwt,0,0x000009ee,0x0800b8aa,, +23774683,0,dwt,0,0x000009ef,0x0800b8aa,, +23849684,0,dwt,0,0x000009f0,0x0800b8aa,, +23924681,0,dwt,0,0x000009f1,0x0800b8aa,, +23999680,0,dwt,0,0x000009f2,0x0800b8aa,, +24074679,0,dwt,0,0x000009f3,0x0800b8aa,, +24149678,0,dwt,0,0x000009f4,0x0800b8aa,, +24224677,0,dwt,0,0x000009f5,0x0800b8aa,, +24299676,0,dwt,0,0x000009f6,0x0800b8aa,, +24374675,0,dwt,0,0x000009f7,0x0800b8aa,, +24449674,0,dwt,0,0x000009f8,0x0800b8aa,, +24524673,0,dwt,0,0x000009f9,0x0800b8aa,, +24599672,0,dwt,0,0x000009fa,0x0800b8aa,, +24674671,0,dwt,0,0x000009fb,0x0800b8aa,, +24749670,0,dwt,0,0x000009fc,0x0800b8aa,, +24824669,0,dwt,0,0x000009fd,0x0800b8aa,, +24899668,0,dwt,0,0x000009fe,0x0800b8aa,, +24974667,0,dwt,0,0x000009ff,0x0800b8aa,, +25049666,0,dwt,0,0x00000a00,0x0800b8aa,, +25124665,0,dwt,0,0x00000a01,0x0800b8aa,, +25199664,0,dwt,0,0x00000a02,0x0800b8aa,, +25274663,0,dwt,0,0x00000a03,0x0800b8aa,, +25349662,0,dwt,0,0x00000a04,0x0800b8aa,, +25424663,0,dwt,0,0x00000a05,0x0800b8aa,, +25499660,0,dwt,0,0x00000a06,0x0800b8aa,, +25574659,0,dwt,0,0x00000a07,0x0800b8aa,, +25649658,0,dwt,0,0x00000a08,0x0800b8aa,, +25724657,0,dwt,0,0x00000a09,0x0800b8aa,, +25799656,0,dwt,0,0x00000a0a,0x0800b8aa,, +25874655,0,dwt,0,0x00000a0b,0x0800b8aa,, +25949654,0,dwt,0,0x00000a0c,0x0800b8aa,, +26024653,0,dwt,0,0x00000a0d,0x0800b8aa,, +26099652,0,dwt,0,0x00000a0e,0x0800b8aa,, +26174651,0,dwt,0,0x00000a0f,0x0800b8aa,, +26249650,0,dwt,0,0x00000a10,0x0800b8aa,, +26324649,0,dwt,0,0x00000a11,0x0800b8aa,, +26399648,0,dwt,0,0x00000a12,0x0800b8aa,, +26474647,0,dwt,0,0x00000a13,0x0800b8aa,, +26549646,0,dwt,0,0x00000a14,0x0800b8aa,, +26624645,0,dwt,0,0x00000a15,0x0800b8aa,, +26699644,0,dwt,0,0x00000a16,0x0800b8aa,, +26774643,0,dwt,0,0x00000a17,0x0800b8aa,, +26849642,0,dwt,0,0x00000a18,0x0800b8aa,, +26924641,0,dwt,0,0x00000a19,0x0800b8aa,, +26999642,0,dwt,0,0x00000a1a,0x0800b8aa,, +27074639,0,dwt,0,0x00000a1b,0x0800b8aa,, +27149638,0,dwt,0,0x00000a1c,0x0800b8aa,, +27224637,0,dwt,0,0x00000a1d,0x0800b8aa,, +27299636,0,dwt,0,0x00000a1e,0x0800b8aa,, +27374635,0,dwt,0,0x00000a1f,0x0800b8aa,, +27449634,0,dwt,0,0x00000a20,0x0800b8aa,, +27524633,0,dwt,0,0x00000a21,0x0800b8aa,, +27599632,0,dwt,0,0x00000a22,0x0800b8aa,, +27674631,0,dwt,0,0x00000a23,0x0800b8aa,, +27749630,0,dwt,0,0x00000a24,0x0800b8aa,, +27824629,0,dwt,0,0x00000a25,0x0800b8aa,, +27899628,0,dwt,0,0x00000a26,0x0800b8aa,, +27974627,0,dwt,0,0x00000a27,0x0800b8aa,, +28049626,0,dwt,0,0x00000a28,0x0800b8aa,, +28124625,0,dwt,0,0x00000a29,0x0800b8aa,, +28199629,0,dwt,0,0x00000a2a,0x0800b8aa,, +28274623,0,dwt,0,0x00000a2b,0x0800b8aa,, +28349622,0,dwt,0,0x00000a2c,0x0800b8aa,, +28424621,0,dwt,0,0x00000a2d,0x0800b8aa,, +28499620,0,dwt,0,0x00000a2e,0x0800b8aa,, +28574619,0,dwt,0,0x00000a2f,0x0800b8aa,, +28649618,0,dwt,0,0x00000a30,0x0800b8aa,, +28724617,0,dwt,0,0x00000a31,0x0800b8aa,, +28799616,0,dwt,0,0x00000a32,0x0800b8aa,, +28874615,0,dwt,0,0x00000a33,0x0800b8aa,, +28949614,0,dwt,0,0x00000a34,0x0800b8aa,, +29024613,0,dwt,0,0x00000a35,0x0800b8aa,, +29099612,0,dwt,0,0x00000a36,0x0800b8aa,, +29174611,0,dwt,0,0x00000a37,0x0800b8aa,, +29249610,0,dwt,0,0x00000a38,0x0800b8aa,, +29249610,0,dwt,0,,0xb8aa47aa,, +29249610,0,error,,,,,OpenCSD detected an invalid ITM packet sequence at raw offset 8195. +29249610,0,error,,,,,OpenCSD consumed 2179 raw bytes while waiting for usable ITM trace packets; data loss until a later sync/recovery point; timestamp 29249610 .. 29324609. +29324609,0,dwt,0,0x00000ebd,0x0800b8aa,, +29399608,0,dwt,0,0x00000ebe,0x0800b8aa,, +29474607,0,dwt,0,0x00000ebf,0x0800b8aa,, +29549606,0,dwt,0,0x00000ec0,0x0800b8aa,, +29624605,0,dwt,0,0x00000ec1,0x0800b8aa,, +29699604,0,dwt,0,0x00000ec2,0x0800b8aa,, +29774603,0,dwt,0,0x00000ec3,0x0800b8aa,, +29849602,0,dwt,0,0x00000ec4,0x0800b8aa,, +29924601,0,dwt,0,0x00000ec5,0x0800b8aa,, +29999600,0,dwt,0,0x00000ec6,0x0800b8aa,, +30074599,0,dwt,0,0x00000ec7,0x0800b8aa,, +30149598,0,dwt,0,0x00000ec8,0x0800b8aa,, +30224597,0,dwt,0,0x00000ec9,0x0800b8aa,, +30299598,0,dwt,0,0x00000eca,0x0800b8aa,, +30374595,0,dwt,0,0x00000ecb,0x0800b8aa,, +30449594,0,dwt,0,0x00000ecc,0x0800b8aa,, +30524593,0,dwt,0,0x00000ecd,0x0800b8aa,, +30599592,0,dwt,0,0x00000ece,0x0800b8aa,, +30674591,0,dwt,0,0x00000ecf,0x0800b8aa,, +30749590,0,dwt,0,0x00000ed0,0x0800b8aa,, +30824589,0,dwt,0,0x00000ed1,0x0800b8aa,, +30899588,0,dwt,0,0x00000ed2,0x0800b8aa,, +30974587,0,dwt,0,0x00000ed3,0x0800b8aa,, +31049586,0,dwt,0,0x00000ed4,0x0800b8aa,, +31124585,0,dwt,0,0x00000ed5,0x0800b8aa,, +31199584,0,dwt,0,0x00000ed6,0x0800b8aa,, +31274583,0,dwt,0,0x00000ed7,0x0800b8aa,, +31349582,0,dwt,0,0x00000ed8,0x0800b8aa,, +31424581,0,dwt,0,0x00000ed9,0x0800b8aa,, +31499585,0,dwt,0,0x00000eda,0x0800b8aa,, +31574579,0,dwt,0,0x00000edb,0x0800b8aa,, +31649578,0,dwt,0,0x00000edc,0x0800b8aa,, +31724577,0,dwt,0,0x00000edd,0x0800b8aa,, +31799576,0,dwt,0,0x00000ede,0x0800b8aa,, +31874575,0,dwt,0,0x00000edf,0x0800b8aa,, +31949574,0,dwt,0,0x00000ee0,0x0800b8aa,, +32024573,0,dwt,0,0x00000ee1,0x0800b8aa,, +32099572,0,dwt,0,0x00000ee2,0x0800b8aa,, +32174571,0,dwt,0,0x00000ee3,0x0800b8aa,, +32249570,0,dwt,0,0x00000ee4,0x0800b8aa,, +32324569,0,dwt,0,0x00000ee5,0x0800b8aa,, +32399568,0,dwt,0,0x00000ee6,0x0800b8aa,, +32474567,0,dwt,0,0x00000ee7,0x0800b8aa,, +32549566,0,dwt,0,0x00000ee8,0x0800b8aa,, +32624565,0,dwt,0,0x00000ee9,0x0800b8aa,, +32699564,0,dwt,0,0x00000eea,0x0800b8aa,, +32774563,0,dwt,0,0x00000eeb,0x0800b8aa,, +32849562,0,dwt,0,0x00000eec,0x0800b8aa,, +32924561,0,dwt,0,0x00000eed,0x0800b8aa,, +32999560,0,dwt,0,0x00000eee,0x0800b8aa,, +33074561,0,dwt,0,0x00000eef,0x0800b8aa,, +33149558,0,dwt,0,0x00000ef0,0x0800b8aa,, +33224557,0,dwt,0,0x00000ef1,0x0800b8aa,, +33299556,0,dwt,0,0x00000ef2,0x0800b8aa,, +33374555,0,dwt,0,0x00000ef3,0x0800b8aa,, +33449554,0,dwt,0,0x00000ef4,0x0800b8aa,, +33524553,0,dwt,0,0x00000ef5,0x0800b8aa,, +33599552,0,dwt,0,0x00000ef6,0x0800b8aa,, +33674551,0,dwt,0,0x00000ef7,0x0800b8aa,, +33749550,0,dwt,0,0x00000ef8,0x0800b8aa,, +33824549,0,dwt,0,0x00000ef9,0x0800b8aa,, +33899548,0,dwt,0,0x00000efa,0x0800b8aa,, +33974547,0,dwt,0,0x00000efb,0x0800b8aa,, +34049546,0,dwt,0,0x00000efc,0x0800b8aa,, +34124545,0,dwt,0,0x00000efd,0x0800b8aa,, +34199544,0,dwt,0,0x00000efe,0x0800b8aa,, +34274543,0,dwt,0,0x00000eff,0x0800b8aa,, +34349542,0,dwt,0,0x00000f00,0x0800b8aa,, +34424541,0,dwt,0,0x00000f01,0x0800b8aa,, +34499540,0,dwt,0,0x00000f02,0x0800b8aa,, +34574539,0,dwt,0,0x00000f03,0x0800b8aa,, +34649540,0,dwt,0,0x00000f04,0x0800b8aa,, +34724537,0,dwt,0,0x00000f05,0x0800b8aa,, +34799536,0,dwt,0,0x00000f06,0x0800b8aa,, +34874535,0,dwt,0,0x00000f07,0x0800b8aa,, +34949534,0,dwt,0,0x00000f08,0x0800b8aa,, +35024533,0,dwt,0,0x00000f09,0x0800b8aa,, +35099532,0,dwt,0,0x00000f0a,0x0800b8aa,, +35174531,0,dwt,0,0x00000f0b,0x0800b8aa,, +35249530,0,dwt,0,0x00000f0c,0x0800b8aa,, +35324529,0,dwt,0,0x00000f0d,0x0800b8aa,, +35399528,0,dwt,0,0x00000f0e,0x0800b8aa,, +35474527,0,dwt,0,0x00000f0f,0x0800b8aa,, +35549526,0,dwt,0,0x00000f10,0x0800b8aa,, +35624525,0,dwt,0,0x00000f11,0x0800b8aa,, +35699524,0,dwt,0,0x00000f12,0x0800b8aa,, +35774523,0,dwt,0,0x00000f13,0x0800b8aa,, +35849522,0,dwt,0,0x00000f14,0x0800b8aa,, +35924521,0,dwt,0,0x00000f15,0x0800b8aa,, +35999520,0,dwt,0,0x00000f16,0x0800b8aa,, +36074519,0,dwt,0,0x00000f17,0x0800b8aa,, +36149518,0,dwt,0,0x00000f18,0x0800b8aa,, +36224519,0,dwt,0,0x00000f19,0x0800b8aa,, +36299516,0,dwt,0,0x00000f1a,0x0800b8aa,, +36374515,0,dwt,0,0x00000f1b,0x0800b8aa,, +36449514,0,dwt,0,0x00000f1c,0x0800b8aa,, +36524513,0,dwt,0,0x00000f1d,0x0800b8aa,, +36599512,0,dwt,0,0x00000f1e,0x0800b8aa,, +36674511,0,dwt,0,0x00000f1f,0x0800b8aa,, +36749510,0,dwt,0,0x00000f20,0x0800b8aa,, +36824509,0,dwt,0,0x00000f21,0x0800b8aa,, +36899508,0,dwt,0,0x00000f22,0x0800b8aa,, +36974507,0,dwt,0,0x00000f23,0x0800b8aa,, +37049506,0,dwt,0,0x00000f24,0x0800b8aa,, +37124505,0,dwt,0,0x00000f25,0x0800b8aa,, +37199504,0,dwt,0,0x00000f26,0x0800b8aa,, +37274503,0,dwt,0,0x00000f27,0x0800b8aa,, +37349502,0,dwt,0,0x00000f28,0x0800b8aa,, +37424501,0,dwt,0,0x00000f29,0x0800b8aa,, +37499500,0,dwt,0,0x00000f2a,0x0800b8aa,, +37574499,0,dwt,0,0x00000f2b,0x0800b8aa,, +37649498,0,dwt,0,0x00000f2c,0x0800b8aa,, +37724497,0,dwt,0,0x00000f2d,0x0800b8aa,, +37799498,0,dwt,0,0x00000f2e,0x0800b8aa,, +37874495,0,dwt,0,0x00000f2f,0x0800b8aa,, +37949494,0,dwt,0,0x00000f30,0x0800b8aa,, +38024493,0,dwt,0,0x00000f31,0x0800b8aa,, +38099492,0,dwt,0,0x00000f32,0x0800b8aa,, +38174491,0,dwt,0,0x00000f33,0x0800b8aa,, +38249490,0,dwt,0,0x00000f34,0x0800b8aa,, +38324489,0,dwt,0,0x00000f35,0x0800b8aa,, +38399488,0,dwt,0,0x00000f36,0x0800b8aa,, +38474487,0,dwt,0,0x00000f37,0x0800b8aa,, +38549486,0,dwt,0,0x00000f38,0x0800b8aa,, +38624485,0,dwt,0,0x00000f39,0x0800b8aa,, +38699484,0,dwt,0,0x00000f3a,0x0800b8aa,, +38774483,0,dwt,0,0x00000f3b,0x0800b8aa,, +38849482,0,dwt,0,0x00000f3c,0x0800b8aa,, +38924481,0,dwt,0,0x00000f3d,0x0800b8aa,, +38999485,0,dwt,0,0x00000f3e,0x0800b8aa,, +39074479,0,dwt,0,0x00000f3f,0x0800b8aa,, +39149478,0,dwt,0,0x00000f40,0x0800b8aa,, +39224477,0,dwt,0,0x00000f41,0x0800b8aa,, +39299476,0,dwt,0,0x00000f42,0x0800b8aa,, +39374475,0,dwt,0,0x00000f43,0x0800b8aa,, +39449474,0,dwt,0,0x00000f44,0x0800b8aa,, +39449474,0,error,,,,,incomplete ITM packet at end of input diff --git a/tools/ctrace/test/data/Blinky+STM32H747I-EVAL/Blinky+STM32H747I-EVAL.SWO.raw b/tools/ctrace/test/data/Blinky+STM32H747I-EVAL/Blinky+STM32H747I-EVAL.SWO.raw new file mode 100644 index 0000000000000000000000000000000000000000..182805e4794f9be3bc56469f657c4e8f340a7119 GIT binary patch literal 12288 zcmY+|ceKy-1IO`e@4e@>_uhL)Bt?phXe+BBDdZXrBPuf$%BYBx?2%QdC^W1>w1)hk}Jn#FypO3_(#H7m!Z5FOdNIxk>LPEmEwTA~?QOcmxJ5pa! zs-RQXlsf3_HKjpmQ96_!Wk4BGCX^XvL0M5YlpWVCs=~4=s)nkg8mK0! zg=(WZs4l99>Z1mzA!>vgqb8^+YKEEz#Y!!LVx^WWTcPVvYt#l^kJ_Sk=myjt-H1A% zo6yauBkF{1L7ma9=(eC(sY_6-)RpD!s2l2zdZ3=@4%7?vMt7pS(B0@BbT7Iu=+d=2 za>q2uTW^dtHS{fvG=zoOsJ@8}QoCpv-tLMPEF^fx*k6f2zxik1Fh zc@~{R=g|dp5nV$6qW{okl#n9%Ul!lyQlOM56-tfLptL9*N{=!G#Y!22Vx>$hGovgh zE6RqlqZ}wF%7t>HJSZ>9hw`HWs30nY3Zo*ZXi%(_7!)fNV_6)PKqXPBWL>#e^JGZ= zdGQ3~Mfp&EQ~(u3g-~Hs1QkVzs2D1aN}!Uc6e^9%pt7i3Q0!bjD0Z&EvLdR4Dx)f> zDyoL6qZ+6ts)cH!I;bwHhw7sSs3B^E8VAKnO@d;jrYxJG=BNc~iCUrSP;1l%U60zL zcIXDw9^HsKpqtRms3YnW6f4~l6f1RRc`LdNbwORx?Wh~-j(VV;=nm8i^+tE1yMp4g z(%nJvS?L~@_oDmI{iqLm06mEMqJF498h{3(htR|55%ee;gdRhK(c?k!6buQ9r{D>e zPokk{7#fbAx~k+q>hZaG1dAjz5{*Km(HQhJdImj?Y^U!>>0KJLcLT{sm zXc2k`Ek;YwyJ#tT5513;p%2hs?Sm|U?taOUy-{>?t zgZ@Eh(K&P;T|gJnCG;=)4_!tH8IwOze7{M7QleBSHA)i{E2RyJmC~_Hk20W)C=<$z zvY@Oe8_JGypqwZd%8l}%yeJ>aj|!lIL9tSypjfFe%Oa>KN<_s_aa018M5Rz^R0fqr zIv6@%ivTZv_5R0UN<)lhX*1Jy*eP;FEP)kXE9uDt&j&zk%x;_pWZR1%d! zrBNAFHYlEhazXJNlxJB1RYa9gWmE-KMb%JsR0GvSwNPzT2h~OOP<_+@HAIbqVx`7G zu~HM3O;Iz{9JN3#Q7d#EYK_{U>rq?O4&8v-qZ?5NbQ8K6bqtD?It9f_x3KJtZbi4D zE~qQI9d$$9Q4iD;-GO?c-snzr7rGnWgYHH51;tAD2gOQ#SU!LrM14^|)E^B%1JOh1 zVe|-k6b(X;p}|4%=6yUU-n>ItK7pP@L(woa96g0bpd>UBjY6Z*81yuH20e?OLu1i6 zG(ISvf(b$K6g>0KJLcLT{smXc2k`Ek;YwyJ#tT5513;p%2jVpjc@|P^`3)UX0!!uMIWJ$(KfUl?LeOd#a5pN#a5rO+=+If&(RlXH`;^t zqA$@`=xg*1+K2X|13~fjJs1>k-$N|FMc<*r=m-h5kmT(HZm)I*ZPs^XLM)h%TXj(SPVNO30S{`Qm#+3X~G1 zLaBpdr8GgYQd*YjPTMNuLuhKi#Ss3a3uK z>p#6Y=X~dVzUO(~=l#xLMHu)QlZx8=Jr$Vyw>3n4iO>(szOSX)pY>{St&mMMCGnJ* z>WGfvTA=7xbCRC=4t`-}fGfH)G`)M1x-)I|c-q4|@!VtC)DSA*9a@Lx4t=don_1fP zGqS4xx{Y-G)ylj{dY-A(b8!DZ(`j|2D?+oa%+raq`4zwKN?fTh=gR5IFU#ebszhjZ z^Oc@*tvqvY!1a8C7ZCo1D(LEn)&Rco&A=TNf4n18FZfk}|AUDz=a`cs?s17{dVP)nN6(wAJwBoT!)f z&wgv<_4AzH2nSsG1-ui#VwP?S&GzdZB0RHAw1GQO_2@!QFFjma z-)HTJr<%a_8ePBmU~b14(-c)707{UI# zB5GZD-}3f=b@#uCKS=U<-21lU3!H2fUrv4r_esOM2bMW(?C*x2`rPr@O@HuyMxa$H z*0)O5k9|cQ`35mHAMbZ=BB!4t8DsZ%s1{rkgz}UxIr4tB#Wd5)u=?C#xOXQoJu_W; zk5c3nsdxS!;%iSb_woq=2Mx!_Rh}|2Hgftg(0EpjJY-eectX~z0&ozt>wV+gL<}{p zWcE1Q-dPV!akklYb>gg_tg$RL!0J}P_vQjqX!0%j5=_%PBFy#Ncab-tX?I^0=aX|tU@okMe?s%dSAwNe zSWmfWHO!X!i=dVGi}Q2-Xu>+(QyFk{7b^dEOOc~l>`G{0>&@=kOiurYpF718a(+(2 z=_>Qy3~0E9```Jiklk;weelr#=`R|6N1Gg`%rtj05dA*vS#GhxRo#oV-W8g^d@J{R z!lG*7qMr-@)D7H!@%jrlu!6Nnr^hnl9k4XCf+wdrU~#r{^}>CcZwc?QQS1CdX@qGXp7O=Q@gPpAzSf57+_7QI6?hHhtk z!d(Hg`lW2j?hrf;wCjfAX@@(r%^bu!^3S_X#MFIu`q3ieM|vjiywRF}&Gnxu@j9&Y z)UevN*kYSURk4b*iOwd*?}&OBZ|pD=;#%qXYfl~1L@Y5!^;B0ldh4^`s76j-E`j=& zG!oWV7du2(6}=wNPI3Hm-czQlHR`31@y*k>NmaEB{XfLmBoQ!=sjg^n zlNKLaj?T0{XLX|!0?wU58~ia`PMD5w&QB!0MWy`*)KAj>Rd-}nFw6S_Lc-ZZ(7ulK zf+QduLpL)v>n|{In7eNGDu-wVO)u<$0Mn$>bbdLq)Z#l;7vtlU`rVDaxj%|@mZ!CL z`nmN9T0eB7+TX}38g<$98%X@KG@Vdokar!=$n1}(|8|B9kLK4jnttTu(MOo6T@;M& zj0-MXsouyYdg6tdxzowJ_!RBB&nWL;xjS95V(ZOe;ziuQIn`c6>q{JVL+SKG)_;y% zN0_#+V@#n(0E+ptk18MiW~?I4FESyBk*5)x*8_>(C|W+~V|`SO-Lv<`?tw zuNM)O+vz9zWlV+j=kO-cugo_mT4RR|B&IlDiw^6u`dLgL?z)d?l37D6(L6b%LeX&G zySfp+!S|2T(3G*Se!4+0Wi<8QWC_RjnBS=ANKNcd_LodQJ~mzXzx)N3YRv~4GHW!7H1Ol@ x3ZiblGx;#*AD-AM=OUNFly5-wW2CzHY%X$@&(7!Tn4b-%;CGa5rPBO2{S#DP8Djtd literal 0 HcmV?d00001 diff --git a/tools/ctrace/test/data/Blinky+STM32H747I-EVAL/Blinky+STM32H747I-EVAL.ctrace-run.yml b/tools/ctrace/test/data/Blinky+STM32H747I-EVAL/Blinky+STM32H747I-EVAL.ctrace-run.yml new file mode 100644 index 000000000..e54084523 --- /dev/null +++ b/tools/ctrace/test/data/Blinky+STM32H747I-EVAL/Blinky+STM32H747I-EVAL.ctrace-run.yml @@ -0,0 +1,59 @@ +ctrace-run: + generated-by: pyTS v0.1.0 + created-by: CMSIS-Debugger v1.4.0 + ctrace-setup: + - pname: CM7 + data: + - location: Blinky_cm7|osRtxInfo.kernel.tick + access: W + output: PC+value + symbol-file: Blinky_cm7.axf + symbol: osRtxInfo.kernel.tick + address: 0x24000024 + size: 4 + type: unsigned int + synchronization: + DWT: 16M + timestamps: + itm-prescaler: 1 + - pname: CM4 + disable: + timestamps: + itm-prescaler: 1 + data: [] + exceptions: + pcsampling: + period: 16384 + ctrace-refs: + - ctrace-ref: CM7/timestamps + type: dwt + pname: CM7 + regs: + - name: ITM_TCR + value: 0x00000003 + mask: 0x00000303 + - ctrace-ref: CM7/data#0 + type: dwt + pname: CM7 + symbol-file: Blinky_cm7.axf + symbol-address: 0x24000024 + regs: + - name: DWT_COMP0 + value: 0x24000024 + - name: DWT_MASK0 + value: 0x00000002 + - name: DWT_FUNCTION0 + value: 0x0000000f + - name: ITM_TCR + value: 0x00000009 + mask: 0x00000009 + - ctrace-ref: CM7/synchronization#0 + type: dwt + pname: CM7 + regs: + - name: DWT_CTRL + value: 0x00000400 + mask: 0x00000c00 + - name: ITM_TCR + value: 0x00000005 + mask: 0x00000005 diff --git a/tools/ctrace/test/data/README.md b/tools/ctrace/test/data/README.md new file mode 100644 index 000000000..79e9561cc --- /dev/null +++ b/tools/ctrace/test/data/README.md @@ -0,0 +1,20 @@ +# ctrace Test Data + +This directory contains the versioned YAML inputs and raw trace captures used +by the reader and executable-level tests. Generated CSV/CTF outputs stay in the +build tree and are not versioned, except for reference output used by an exact +comparison test. + +The Blinky fixture contains SWO and TB input. The integration test compares the +generated SWO CSV byte-for-byte with its reference and verifies that TB is +reported as a trace channel that is not implemented yet. + +The Blinky YAML, SWO capture, and TB capture are approved ctrace test assets and +may be redistributed as part of Open-CMSIS-Pack/devtools. The reference CSV is +derived from the SWO capture and is covered by the same approval. + +`trace-run` contains only the small current-schema inputs needed by executable +tests. Reader unit tests cover the consumed fields and verify that unrelated +YAML content is ignored. The successful-output integration test combines the +Board YAML with an eight-byte synthetic ITM stream generated below the build +tree; no generated binary fixture is versioned. diff --git a/tools/ctrace/test/data/trace-run/Board.ctrace-run.yml b/tools/ctrace/test/data/trace-run/Board.ctrace-run.yml new file mode 100644 index 000000000..8e5634bc6 --- /dev/null +++ b/tools/ctrace/test/data/trace-run/Board.ctrace-run.yml @@ -0,0 +1,36 @@ +format-version: ignored +producer-extension: + malformed: [but, irrelevant] +ctrace-run: + generated-by: ctrace integration test + created-by: ignored + vendor-extension: { arbitrary: [content] } + ctrace-setup: + - timestamps: + clock: 400000000 + itm-prescaler: 1 + timestamp-extension: [ignored] + itm: + enable: 0xFFFFFFFE + privileged: [ignored] + exceptions: [ignored] + pcsampling: { ignored: true } + ctrace-refs: + - ctrace-ref: ignored/etm + type: etm + source: [invalid] + regs: [ignored] + - ctrace-ref: opaque/printf-route + type: itm + info: configured ITM channel + stream: 1 + source: 0 + regs: [ignored] + - ctrace-ref: data#0 + type: dwt + info: configured DWT data trace + warning: aligned DWT range + label: sample counter + stream: 1 + source: [0, 1] + reference-extension: [ignored] diff --git a/tools/ctrace/test/data/trace-run/Minimal.ctrace-run.yml b/tools/ctrace/test/data/trace-run/Minimal.ctrace-run.yml new file mode 100644 index 000000000..32a229b45 --- /dev/null +++ b/tools/ctrace/test/data/trace-run/Minimal.ctrace-run.yml @@ -0,0 +1,5 @@ +ctrace-run: + ctrace-setup: + - timestamps: + clock: 400000000 + ctrace-refs: [] diff --git a/tools/ctrace/test/integration/CMakeLists.txt b/tools/ctrace/test/integration/CMakeLists.txt new file mode 100644 index 000000000..a4da361b7 --- /dev/null +++ b/tools/ctrace/test/integration/CMakeLists.txt @@ -0,0 +1,121 @@ +# Copyright (c) 2026 Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# Generated with AI + +find_package(Python3 REQUIRED COMPONENTS Interpreter) + +set(CTRACE_EMULATOR_ARGUMENT + "-DCTRACE_EMULATOR=$,|>") + +add_test( + NAME ctrace-version + COMMAND ctrace --version +) + +add_test( + NAME ctrace-version-rejects-invalid-type + COMMAND ${CMAKE_COMMAND} + -DCTRACE_EXECUTABLE=$ + "${CTRACE_EMULATOR_ARGUMENT}" + "-DCTRACE_ARGS=--version;--type;DWT" + "-DEXPECTED_STDERR=\\[fatal\\] cli/invalid-arguments:" + -P ${CMAKE_CURRENT_SOURCE_DIR}/src/RunExpectedFailure.cmake +) + +add_test( + NAME ctrace-missing-trace-directory + COMMAND ${CMAKE_COMMAND} + -DCTRACE_EXECUTABLE=$ + "${CTRACE_EMULATOR_ARGUMENT}" + -DCTRACE_ARGS=${CMAKE_CURRENT_BINARY_DIR}/__missing_trace_directory_4b66b86f__ + "-DEXPECTED_STDERR=\\[fatal\\] input/trace-directory-failed:" + -P ${CMAKE_CURRENT_SOURCE_DIR}/src/RunExpectedFailure.cmake +) + +string(REPLACE "+" "\\+" CTRACE_VERSION_REGEX "${CTRACE_VERSION_FULL}") + +add_test( + NAME ctrace-no-args-shows-help + COMMAND ctrace +) +set_tests_properties(ctrace-no-args-shows-help PROPERTIES + PASS_REGULAR_EXPRESSION "ctrace \\[options\\]" +) + +set_tests_properties(ctrace-version PROPERTIES + PASS_REGULAR_EXPRESSION "ctrace ${CTRACE_VERSION_REGEX}" +) + +add_test( + NAME ctrace-trace-run-yaml + COMMAND ${CMAKE_COMMAND} + -DCTRACE_EXECUTABLE=$ + "${CTRACE_EMULATOR_ARGUMENT}" + -DYAML_FILE=${CTRACE_SOURCE_DIR}/test/data/trace-run/Board.ctrace-run.yml + -DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/trace-run-yaml + -DTARGET=Board + -DEXPECTED_RESULT=0 + "-DEXPECTED_STDERR=aligned DWT range" + "-DEXPECTED_STDERR_2=configured DWT data trace" + "-DEXPECTED_STDERR_3=input/ctrace-run-meta: applied ctrace-run meta" + "-DUNEXPECTED_STDERR=configured ITM channel|decode/ctrace-run-meta" + -P ${CMAKE_CURRENT_SOURCE_DIR}/src/RunTraceRunYaml.cmake +) + +add_test( + NAME ctrace-decoder-error-all-is-generated + COMMAND ${CMAKE_COMMAND} + -DCTRACE_EXECUTABLE=$ + "${CTRACE_EMULATOR_ARGUMENT}" + -DYAML_FILE=${CTRACE_SOURCE_DIR}/test/data/trace-run/Minimal.ctrace-run.yml + -DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/decoder-error-all + -DTARGET=Minimal + -DOUTPUT_OPTION=--all + -DRAW_DECODER_ERROR=TRUE + -DEXPECTED_RESULT=1 + -DEXPECT_CSV=TRUE + -DEXPECT_CTF=TRUE + -DEXPECT_CSV_ERROR_ROW=TRUE + "-DEXPECTED_STDERR=summary" + -P ${CMAKE_CURRENT_SOURCE_DIR}/src/RunTraceRunYaml.cmake +) + +add_test( + NAME ctrace-valid-swo-generates-all-outputs + COMMAND ${CMAKE_COMMAND} + -DCTRACE_EXECUTABLE=$ + "${CTRACE_EMULATOR_ARGUMENT}" + -DPYTHON_EXECUTABLE=${Python3_EXECUTABLE} + -DYAML_FILE=${CTRACE_SOURCE_DIR}/test/data/trace-run/Board.ctrace-run.yml + -DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/valid-swo-all + -DTARGET=Valid + -P ${CMAKE_CURRENT_SOURCE_DIR}/src/RunValidTraceFixture.cmake +) +set_tests_properties( + ctrace-valid-swo-generates-all-outputs + PROPERTIES + LABELS "integration;fixture" + TIMEOUT 30 +) + +set(CTRACE_BLINKY_FIXTURE_DIR + "${CTRACE_SOURCE_DIR}/test/data/Blinky+STM32H747I-EVAL") + +add_test( + NAME ctrace-fixture-blinky-swo-and-unimplemented-tb + COMMAND ${CMAKE_COMMAND} + -DCTRACE_EXECUTABLE=$ + "${CTRACE_EMULATOR_ARGUMENT}" + -DFIXTURE_DIR=${CTRACE_BLINKY_FIXTURE_DIR} + -DTARGET=Blinky+STM32H747I-EVAL + -DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/fixture-blinky + -DEXPECTED_RESULT=1 + -P ${CMAKE_CURRENT_SOURCE_DIR}/src/RunTraceFixture.cmake +) +set_tests_properties( + ctrace-fixture-blinky-swo-and-unimplemented-tb + PROPERTIES + LABELS "integration;fixture" + TIMEOUT 30 +) diff --git a/tools/ctrace/test/integration/README.md b/tools/ctrace/test/integration/README.md new file mode 100644 index 000000000..e9a299698 --- /dev/null +++ b/tools/ctrace/test/integration/README.md @@ -0,0 +1,10 @@ +# ctrace Integration Tests + +Integration tests exercise the `ctrace` executable and file-oriented workflows. +They should use fixtures from `test/data` and write generated output only under +the CMake build directory. + +`ctrace-valid-swo-generates-all-outputs` materializes a reviewable eight-byte +ITM stream in the build tree and verifies a successful CSV, CTF, and Trace +Compass conversion. The versioned Blinky capture separately covers damaged +input recovery, exact CSV output, and discovery of unsupported Trace Bus input. diff --git a/tools/ctrace/test/integration/src/CtraceTestCommand.cmake b/tools/ctrace/test/integration/src/CtraceTestCommand.cmake new file mode 100644 index 000000000..6b4a1fb5a --- /dev/null +++ b/tools/ctrace/test/integration/src/CtraceTestCommand.cmake @@ -0,0 +1,10 @@ +# Copyright (c) 2026 Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +set(ctrace_command) +if(DEFINED CTRACE_EMULATOR AND NOT CTRACE_EMULATOR STREQUAL "") + string(REPLACE "|" ";" ctrace_emulator "${CTRACE_EMULATOR}") + list(APPEND ctrace_command ${ctrace_emulator}) +endif() +list(APPEND ctrace_command "${CTRACE_EXECUTABLE}") diff --git a/tools/ctrace/test/integration/src/RunExpectedFailure.cmake b/tools/ctrace/test/integration/src/RunExpectedFailure.cmake new file mode 100644 index 000000000..8575165d4 --- /dev/null +++ b/tools/ctrace/test/integration/src/RunExpectedFailure.cmake @@ -0,0 +1,29 @@ +# Copyright (c) 2026 Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# Generated with AI + +foreach(required CTRACE_EXECUTABLE CTRACE_ARGS EXPECTED_STDERR) + if(NOT DEFINED ${required}) + message(FATAL_ERROR "Missing -D${required}=...") + endif() +endforeach() + +include("${CMAKE_CURRENT_LIST_DIR}/CtraceTestCommand.cmake") + +execute_process( + COMMAND ${ctrace_command} ${CTRACE_ARGS} + RESULT_VARIABLE result + ERROR_VARIABLE stderr +) + +if(result EQUAL 0) + message(FATAL_ERROR + "ctrace unexpectedly succeeded\n" + "stderr:\n${stderr}") +endif() +if(NOT stderr MATCHES "${EXPECTED_STDERR}") + message(FATAL_ERROR + "ctrace stderr does not contain '${EXPECTED_STDERR}'\n" + "stderr:\n${stderr}") +endif() diff --git a/tools/ctrace/test/integration/src/RunTraceFixture.cmake b/tools/ctrace/test/integration/src/RunTraceFixture.cmake new file mode 100644 index 000000000..ed70fe28d --- /dev/null +++ b/tools/ctrace/test/integration/src/RunTraceFixture.cmake @@ -0,0 +1,70 @@ +# Copyright (c) 2026 Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +foreach(required + CTRACE_EXECUTABLE + FIXTURE_DIR + TARGET + WORK_DIR + EXPECTED_RESULT) + if(NOT DEFINED ${required}) + message(FATAL_ERROR "Missing -D${required}=...") + endif() +endforeach() + +include("${CMAKE_CURRENT_LIST_DIR}/CtraceTestCommand.cmake") + +set(swo_raw "${FIXTURE_DIR}/${TARGET}.SWO.raw") +set(tb_raw "${FIXTURE_DIR}/${TARGET}.TB.raw") +set(trace_run "${FIXTURE_DIR}/${TARGET}.ctrace-run.yml") +set(expected_csv "${FIXTURE_DIR}/${TARGET}.SWO.csv") + +foreach(fixture_file IN ITEMS "${swo_raw}" "${tb_raw}" "${trace_run}" "${expected_csv}") + if(NOT EXISTS "${fixture_file}") + message(FATAL_ERROR "Missing fixture file: ${fixture_file}") + endif() +endforeach() + +file(REMOVE_RECURSE "${WORK_DIR}") +file(MAKE_DIRECTORY "${WORK_DIR}") +file(COPY "${swo_raw}" "${tb_raw}" "${trace_run}" DESTINATION "${WORK_DIR}") + +execute_process( + COMMAND + ${ctrace_command} + "${WORK_DIR}" + --target "${TARGET}" + --csv + RESULT_VARIABLE result + ERROR_VARIABLE stderr +) +if(NOT result EQUAL EXPECTED_RESULT) + message(FATAL_ERROR + "ctrace conversion returned ${result}, expected ${EXPECTED_RESULT}:\n${stderr}") +endif() + +if(NOT stderr MATCHES "unsupported-trace-channel:.*channel=TB") + message(FATAL_ERROR + "ctrace did not report TB as an unimplemented trace channel:\n${stderr}") +endif() + +set(actual_csv "${WORK_DIR}/${TARGET}.SWO.csv") +execute_process( + COMMAND "${CMAKE_COMMAND}" -E compare_files "${actual_csv}" "${expected_csv}" + RESULT_VARIABLE compare_result +) +if(NOT compare_result EQUAL 0) + message(FATAL_ERROR + "Generated SWO CSV differs from ${expected_csv}") +endif() + +foreach(unexpected_output IN ITEMS + "${WORK_DIR}/${TARGET}.TB.csv" + "${WORK_DIR}/${TARGET}.TB.traceanalysis.xml" + "${WORK_DIR}/${TARGET}.TB.ctf") + if(EXISTS "${unexpected_output}") + message(FATAL_ERROR + "TB output must not be generated before TB support is implemented: ${unexpected_output}") + endif() +endforeach() diff --git a/tools/ctrace/test/integration/src/RunTraceRunYaml.cmake b/tools/ctrace/test/integration/src/RunTraceRunYaml.cmake new file mode 100644 index 000000000..65b3b6fad --- /dev/null +++ b/tools/ctrace/test/integration/src/RunTraceRunYaml.cmake @@ -0,0 +1,101 @@ +# Copyright (c) 2026 Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# Generated with AI + +foreach(required CTRACE_EXECUTABLE YAML_FILE WORK_DIR TARGET EXPECTED_RESULT EXPECTED_STDERR) + if(NOT DEFINED ${required}) + message(FATAL_ERROR "Missing -D${required}=...") + endif() +endforeach() + +include("${CMAKE_CURRENT_LIST_DIR}/CtraceTestCommand.cmake") + +file(REMOVE_RECURSE "${WORK_DIR}") +file(MAKE_DIRECTORY "${WORK_DIR}") +configure_file("${YAML_FILE}" "${WORK_DIR}/${TARGET}.ctrace-run.yml" COPYONLY) +if(RAW_DECODER_ERROR) + string(ASCII 1 decoder_error_prefix) + file(WRITE "${WORK_DIR}/${TARGET}.SWO.raw" "${decoder_error_prefix}A") +else() + file(WRITE "${WORK_DIR}/${TARGET}.SWO.raw" "") +endif() + +set(command ${ctrace_command} "${WORK_DIR}" --target "${TARGET}") +if(DEFINED OUTPUT_OPTION) + list(APPEND command "${OUTPUT_OPTION}") +endif() +execute_process( + COMMAND ${command} + RESULT_VARIABLE result + ERROR_VARIABLE stderr +) + +if(NOT result EQUAL EXPECTED_RESULT) + message(FATAL_ERROR + "ctrace returned ${result}, expected ${EXPECTED_RESULT}\n" + "stderr:\n${stderr}") +endif() +if(NOT stderr MATCHES "${EXPECTED_STDERR}") + message(FATAL_ERROR + "ctrace stderr does not contain '${EXPECTED_STDERR}'\n" + "stderr:\n${stderr}") +endif() +foreach(index RANGE 2 6) + if(DEFINED EXPECTED_STDERR_${index} + AND NOT stderr MATCHES "${EXPECTED_STDERR_${index}}") + message(FATAL_ERROR + "ctrace stderr does not contain '${EXPECTED_STDERR_${index}}'\n" + "stderr:\n${stderr}") + endif() +endforeach() +if(DEFINED UNEXPECTED_STDERR AND stderr MATCHES "${UNEXPECTED_STDERR}") + message(FATAL_ERROR + "ctrace stderr unexpectedly contains '${UNEXPECTED_STDERR}'\n" + "stderr:\n${stderr}") +endif() +if(EXPECT_NO_OUTPUT) + file(GLOB generated_outputs + "${WORK_DIR}/*.csv" + "${WORK_DIR}/*.ctf" + "${WORK_DIR}/*.traceanalysis.xml") + if(generated_outputs) + message(FATAL_ERROR + "ctrace generated output despite a fatal configuration error: ${generated_outputs}") + endif() +endif() + +set(csv_path "${WORK_DIR}/${TARGET}.SWO.csv") +set(ctf_path "${WORK_DIR}/${TARGET}.ctf") +set(ctf_stream_path "${ctf_path}/stream_0") +set(xml_path "${WORK_DIR}/${TARGET}.SWO.traceanalysis.xml") +if(DEFINED EXPECT_CSV) + if(EXPECT_CSV AND NOT EXISTS "${csv_path}") + message(FATAL_ERROR "ctrace did not generate expected CSV output: ${csv_path}") + elseif(NOT EXPECT_CSV AND EXISTS "${csv_path}") + message(FATAL_ERROR "ctrace unexpectedly generated CSV output: ${csv_path}") + endif() +endif() +if(DEFINED EXPECT_CTF) + if(EXPECT_CTF) + if(NOT EXISTS "${ctf_path}/metadata" + OR NOT EXISTS "${ctf_stream_path}" + OR NOT EXISTS "${xml_path}") + message(FATAL_ERROR + "ctrace did not generate the complete expected CTF bundle: ${ctf_path}") + endif() + elseif(EXISTS "${ctf_path}" OR EXISTS "${xml_path}") + message(FATAL_ERROR + "ctrace unexpectedly generated a CTF bundle: ${ctf_path}") + endif() +endif() +if(EXPECT_CSV_ERROR_ROW) + if(NOT EXISTS "${csv_path}") + message(FATAL_ERROR "cannot inspect missing CSV output: ${csv_path}") + endif() + file(READ "${csv_path}" csv_contents) + if(NOT csv_contents MATCHES ",error,") + message(FATAL_ERROR + "CSV output does not contain the expected decoder error row:\n${csv_contents}") + endif() +endif() diff --git a/tools/ctrace/test/integration/src/RunValidTraceFixture.cmake b/tools/ctrace/test/integration/src/RunValidTraceFixture.cmake new file mode 100644 index 000000000..0799a355b --- /dev/null +++ b/tools/ctrace/test/integration/src/RunValidTraceFixture.cmake @@ -0,0 +1,81 @@ +# Copyright (c) 2026 Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +foreach(required CTRACE_EXECUTABLE PYTHON_EXECUTABLE YAML_FILE WORK_DIR TARGET) + if(NOT DEFINED ${required}) + message(FATAL_ERROR "Missing -D${required}=...") + endif() +endforeach() + +include("${CMAKE_CURRENT_LIST_DIR}/CtraceTestCommand.cmake") + +file(REMOVE_RECURSE "${WORK_DIR}") +file(MAKE_DIRECTORY "${WORK_DIR}") +configure_file("${YAML_FILE}" "${WORK_DIR}/${TARGET}.ctrace-run.yml" COPYONLY) + +# Hardware ITM synchronization followed by one channel-1 software packet with +# the value 0x41. Keep the fixture textual here and materialize it only below +# the build tree so the byte sequence stays reviewable. +set(raw_path "${WORK_DIR}/${TARGET}.SWO.raw") +execute_process( + COMMAND + "${PYTHON_EXECUTABLE}" + -c + "__import__('pathlib').Path(__import__('sys').argv[1]).write_bytes(bytes.fromhex(__import__('sys').argv[2]))" + "${raw_path}" + "0000000000800941" + RESULT_VARIABLE write_result + ERROR_VARIABLE write_stderr +) +if(NOT write_result EQUAL 0) + message(FATAL_ERROR "Failed to create valid SWO fixture: ${write_stderr}") +endif() + +execute_process( + COMMAND ${ctrace_command} "${WORK_DIR}" --target "${TARGET}" --all + RESULT_VARIABLE result + OUTPUT_VARIABLE stdout + ERROR_VARIABLE stderr +) +if(NOT result EQUAL 0) + message(FATAL_ERROR + "ctrace returned ${result}, expected 0\n" + "stdout:\n${stdout}\n" + "stderr:\n${stderr}") +endif() +if(stderr MATCHES "\\[fatal\\]") + message(FATAL_ERROR "Valid SWO input produced a fatal diagnostic:\n${stderr}") +endif() +if(NOT stderr MATCHES "decode/summary: decoded 2 packets from 8 bytes") + message(FATAL_ERROR "Valid SWO input produced an unexpected summary:\n${stderr}") +endif() + +set(csv_path "${WORK_DIR}/${TARGET}.SWO.csv") +set(ctf_path "${WORK_DIR}/${TARGET}.ctf") +set(xml_path "${WORK_DIR}/${TARGET}.SWO.traceanalysis.xml") +foreach(expected_file IN ITEMS + "${csv_path}" + "${ctf_path}/metadata" + "${ctf_path}/stream_0" + "${xml_path}") + if(NOT EXISTS "${expected_file}") + message(FATAL_ERROR "ctrace did not generate expected output: ${expected_file}") + endif() +endforeach() + +file(READ "${csv_path}" csv_contents) +string(CONCAT expected_csv + "cycles,stream,type,source,value,pc,offset,note\n" + "0,0,itm,1,0x41,,,\n") +if(NOT csv_contents STREQUAL expected_csv) + message(FATAL_ERROR + "Valid SWO CSV differs from the expected output:\n${csv_contents}") +endif() + +foreach(non_empty_file IN ITEMS "${ctf_path}/metadata" "${ctf_path}/stream_0" "${xml_path}") + file(SIZE "${non_empty_file}" output_size) + if(output_size EQUAL 0) + message(FATAL_ERROR "ctrace generated an empty output: ${non_empty_file}") + endif() +endforeach() diff --git a/tools/ctrace/test/unit/CMakeLists.txt b/tools/ctrace/test/unit/CMakeLists.txt index 7bd28ef9b..55d9cd447 100644 --- a/tools/ctrace/test/unit/CMakeLists.txt +++ b/tools/ctrace/test/unit/CMakeLists.txt @@ -2,15 +2,50 @@ # # SPDX-License-Identifier: Apache-2.0 -add_executable(CtraceUnitTests src/CtraceMainTest.cpp) +add_executable(CtraceUnitTests + src/cli/CliParserTests.cpp + src/control/DecodeConsumersTests.cpp + src/control/TraceDirectoryJobTests.cpp + src/decode/CortexMPostDecoderTests.cpp + src/decode/DecodePipelineTests.cpp + src/decode/DwtPacketDecoderTests.cpp + src/decode/OpenCsdErrorControllerTests.cpp + src/decode/OpenCsdPacketCollectorTests.cpp + src/diagnostics/DiagnosticsTests.cpp + src/model/TraceSelectionTests.cpp + src/output/OutputRequirementsTests.cpp + src/output/TraceOutputLifecycleTests.cpp + src/output/csv/CsvFileOutputCriteriaTests.cpp + src/output/csv/CsvFileOutputTests.cpp + src/output/csv/CsvRowMapperTests.cpp + src/output/ctf/CtfBundleOutputTests.cpp + src/output/ctf/CtfEncoderTests.cpp + src/output/ctf/CtfSchemaTests.cpp + src/tracerun/CtraceRunMetaTests.cpp + src/tracerun/TraceRunConfigReaderTests.cpp + src/tracerun/TraceRunDiscoveryTests.cpp +) -set_property(TARGET CtraceUnitTests PROPERTY - MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") -set_property(TARGET CtraceUnitTests PROPERTY - VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +target_link_libraries(CtraceUnitTests PRIVATE + ctrace::control + ctrace::decode + ctrace::opencsd + gtest_main +) -target_link_libraries(CtraceUnitTests PRIVATE ctracelib gtest_main) +target_include_directories(CtraceUnitTests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/support" +) -add_test(NAME CtraceUnitTests - COMMAND CtraceUnitTests --gtest_output=xml:test_reports/ctraceunittests-report-${SYSTEM}-${CPU_ARCH}.xml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +ctrace_configure_target(CtraceUnitTests) + +file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/test_reports") +include(GoogleTest) +gtest_discover_tests(CtraceUnitTests + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" + PROPERTIES + TIMEOUT 30 + LABELS "ctrace-unit" + XML_OUTPUT_DIR "${CMAKE_BINARY_DIR}/test_reports" + DISCOVERY_MODE PRE_TEST +) diff --git a/tools/ctrace/test/unit/src/CtraceMainTest.cpp b/tools/ctrace/test/unit/src/CtraceMainTest.cpp deleted file mode 100644 index a06c9fda7..000000000 --- a/tools/ctrace/test/unit/src/CtraceMainTest.cpp +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) 2026 Arm Limited. All rights reserved. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include "gtest/gtest.h" - -#include "CtraceMain.h" - -TEST(CtraceMainTest, PrintsExecutableName) { - testing::internal::CaptureStdout(); - - EXPECT_EQ(0, CtraceMain()); - - EXPECT_EQ("ctrace Executable\n", testing::internal::GetCapturedStdout()); -} diff --git a/tools/ctrace/test/unit/src/cli/CliParserTests.cpp b/tools/ctrace/test/unit/src/cli/CliParserTests.cpp new file mode 100644 index 000000000..a1ea2fa72 --- /dev/null +++ b/tools/ctrace/test/unit/src/cli/CliParserTests.cpp @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.hpp" +#include +#include "CliOptions.hpp" +#include "CliParser.hpp" +#include +#include +#include +#include +#include + +TEST(CtraceUnitTests, testCliParser) +{ + const char* allArgv[] = { + "ctrace", ".trace", "--all", "--target", "Board.Debug", "--type", "dwt", "itm", "--stream", "1", "2", + }; + const auto all = CliParser::parse(11, allArgv); + CliParser::validate(all); + require(all.traceDir == std::optional(".trace"), "CliParser trace directory mismatch"); + require(all.targetName == std::optional("Board.Debug"), "CliParser target mismatch"); + require(all.outputFormat == OutputFormat::All, "CliParser all output mismatch"); + require(all.selection.types == std::vector({"dwt", "itm"}), "CliParser type filter mismatch"); + require(all.selection.streams == std::vector({1U, 2U}), "CliParser stream filter mismatch"); + + const char* checkArgv[] = {"ctrace", ".trace"}; + const auto check = CliParser::parse(2, checkArgv); + CliParser::validate(check); + require(check.outputFormat == OutputFormat::None, "CliParser check-only output mismatch"); + + const char* csvArgv[] = {"ctrace", ".trace", "--csv"}; + const auto csv = CliParser::parse(3, csvArgv); + CliParser::validate(csv); + require(csv.outputFormat == OutputFormat::Csv, "CliParser CSV output mismatch"); + + const char* ctfArgv[] = {"ctrace", ".trace", "--ctf"}; + const auto ctf = CliParser::parse(3, ctfArgv); + CliParser::validate(ctf); + require(ctf.outputFormat == OutputFormat::Ctf, "CliParser CTF output mismatch"); + + const char* shortAllArgv[] = {"ctrace", ".trace", "-a"}; + const auto shortAll = CliParser::parse(3, shortAllArgv); + CliParser::validate(shortAll); + require(shortAll.outputFormat == OutputFormat::All, "CliParser short all output mismatch"); + + const char* combinedArgv[] = {"ctrace", ".trace", "--csv", "--ctf"}; + const auto combined = CliParser::parse(4, combinedArgv); + CliParser::validate(combined); + require(combined.outputFormat == OutputFormat::All, "CliParser combined output mismatch"); + + const char* versionArgv[] = {"ctrace", "-V"}; + const auto version = CliParser::parse(2, versionArgv); + CliParser::validate(version); + require(version.version, "CliParser short version alias mismatch"); + + const char* invalidVersionTypeArgv[] = {"ctrace", "-V", "--type", "DWT"}; + bool rejectedInvalidVersionType = false; + try { + const auto invalid = CliParser::parse(4, invalidVersionTypeArgv); + CliParser::validate(invalid); + } catch (const std::runtime_error& error) { + rejectedInvalidVersionType = std::string(error.what()).find("Invalid --type value: DWT") != std::string::npos; + } + require(rejectedInvalidVersionType, "--version must not bypass packet type validation"); + + const char* missingArgv[] = {"ctrace"}; + bool rejectedMissingTraceDir = false; + try { + const auto missing = CliParser::parse(1, missingArgv); + CliParser::validate(missing); + } catch (const std::runtime_error& error) { + rejectedMissingTraceDir = std::string(error.what()).find("Specify ") != std::string::npos; + } + require(rejectedMissingTraceDir, "CliParser should require a trace directory"); + + const char* invalidTypeArgv[] = {"ctrace", ".trace", "--type", "invalid"}; + bool rejectedInvalidType = false; + try { + const auto invalid = CliParser::parse(4, invalidTypeArgv); + CliParser::validate(invalid); + } catch (const std::runtime_error& error) { + rejectedInvalidType = std::string(error.what()).find("Invalid --type value") != std::string::npos; + } + require(rejectedInvalidType, "CliParser should reject invalid packet types"); + + for (const auto* type : {"event", "pmu", "pcsample"}) { + const char* unsupportedTypeArgv[] = {"ctrace", ".trace", "--type", type}; + bool rejectedUnsupportedType = false; + try { + const auto unsupported = CliParser::parse(4, unsupportedTypeArgv); + CliParser::validate(unsupported); + } catch (const std::runtime_error& error) { + rejectedUnsupportedType = std::string(error.what()).find("Invalid --type value") != std::string::npos; + } + require(rejectedUnsupportedType, std::string("CliParser should reject unimplemented type ") + type); + } + + const char* wrongCaseTypeArgv[] = {"ctrace", ".trace", "--type", "DWT"}; + bool rejectedWrongCaseType = false; + try { + const auto invalid = CliParser::parse(4, wrongCaseTypeArgv); + CliParser::validate(invalid); + } catch (const std::runtime_error& error) { + rejectedWrongCaseType = std::string(error.what()).find("Invalid --type value: DWT") != std::string::npos; + } + require(rejectedWrongCaseType, "CliParser should enforce case-sensitive packet type names"); + + const char* commaSeparatedArgv[] = {"ctrace", ".trace", "--type=dwt,event"}; + bool rejectedCommaSeparatedTypes = false; + try { + const auto invalid = CliParser::parse(3, commaSeparatedArgv); + CliParser::validate(invalid); + } catch (const std::runtime_error&) { + rejectedCommaSeparatedTypes = true; + } + require(rejectedCommaSeparatedTypes, "CliParser should accept only the specified space-separated selectors"); + + const char* traceBusZeroArgv[] = {"ctrace", ".trace", "--stream", "0"}; + const auto traceBusZero = CliParser::parse(4, traceBusZeroArgv); + require(traceBusZero.selection.streams == std::vector({0U}), + "CliParser must accept Trace Bus ID 0 for unformatted input"); + + for (const auto* stream : {"112", "255"}) { + const char* invalidStreamArgv[] = {"ctrace", ".trace", "--stream", stream}; + bool rejectedInvalidStream = false; + try { + (void)CliParser::parse(4, invalidStreamArgv); + } catch (const std::runtime_error& error) { + rejectedInvalidStream = std::string(error.what()).find("CoreSight Trace Bus ID") != std::string::npos; + } + require(rejectedInvalidStream, std::string("CliParser accepted invalid Trace Bus ID ") + stream); + } + + const char* unknownArgv[] = {"ctrace", ".trace", "--unknown"}; + bool rejectedUnknown = false; + try { + (void)CliParser::parse(3, unknownArgv); + } catch (const std::runtime_error&) { + rejectedUnknown = true; + } + require(rejectedUnknown, "CliParser should reject unknown options"); +} diff --git a/tools/ctrace/test/unit/src/control/DecodeConsumersTests.cpp b/tools/ctrace/test/unit/src/control/DecodeConsumersTests.cpp new file mode 100644 index 000000000..d634d60ba --- /dev/null +++ b/tools/ctrace/test/unit/src/control/DecodeConsumersTests.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.hpp" +#include +#include "DecodeConsumers.hpp" +#include "TraceOutput.hpp" +#include +#include +#include +#include +#include +#include + +class LifecycleRecordingTraceOutput final : public TraceOutput { +public: + explicit LifecycleRecordingTraceOutput(std::vector& calls) : calls_(calls) {} + + void start() override + { + calls_.push_back("start"); + } + + void stop() override + { + calls_.push_back("stop"); + } + + void abort() override + { + calls_.push_back("abort"); + } + + void writeEvent(const TraceEvent&) override + { + calls_.push_back("write"); + } + +private: + std::vector& calls_; +}; + +class OrderingDiagnosticSink final : public DiagnosticSink { +public: + explicit OrderingDiagnosticSink(std::vector& calls) : calls_(calls) {} + +protected: + void write(const Event&) override + { + calls_.push_back("diagnostic"); + } + +private: + std::vector& calls_; +}; + +TEST(CtraceUnitTests, testDecodeConsumersForwardsWarningsAndFailsOnErrorsWithOutputs) +{ + std::vector calls; + std::vector> outputs; + outputs.push_back(std::make_unique(calls)); + OrderingDiagnosticSink diagnostics(calls); + DecodeConsumers consumers(std::move(outputs), diagnostics); + + TraceEvent warning = issuePacket("opencsd-warning", "decoder warning", TraceIssueSeverity::Warning); + consumers.append(warning); + require(calls == std::vector({"start", "write", "diagnostic"}), + "warning packets must reach outputs before issue reporting"); + require(consumers.eventCount() == 1U, "DecodeConsumers warning packet count mismatch"); + require(diagnostics.fatalCount() == 0U, "decoder warnings must not fail validation"); + + TraceEvent error = warning; + auto& errorIssue = std::get(error.payload); + errorIssue.severity = TraceIssueSeverity::Error; + errorIssue.code = "opencsd-error"; + errorIssue.message = "decoder error"; + consumers.append(error); + require(calls == std::vector({"start", "write", "diagnostic", "write", "diagnostic"}), + "decoder error packets must reach outputs before issue reporting"); + require(consumers.eventCount() == 2U, "DecodeConsumers error packet count mismatch"); + require(diagnostics.fatalCount() == 1U, "decoder errors must fail validation even when outputs are configured"); + + consumers.abortOutputs(); + require(calls == std::vector({"start", "write", "diagnostic", "write", "diagnostic", "abort"}), + "fatal decode cleanup must abort and remove partial outputs"); +} + +TEST(CtraceUnitTests, testDecodeConsumersWarnsForDisabledItmChannelsOnce) +{ + CollectingDiagnosticSink unknownDiagnostics; + DecodeConsumers unknownConsumers({}, unknownDiagnostics); + unknownConsumers.append(softwarePacket(3U)); + require(unknownDiagnostics.events().empty(), "an absent ctrace-setup.itm node must not invent an ITM enable warning"); + + CollectingDiagnosticSink diagnostics; + DecodeConsumers consumers({}, diagnostics, 0x00000002U, {{2U, 0x00000004U}}); + + auto enabled = softwarePacket(1U); + consumers.append(enabled); + + auto disabled = softwarePacket(2U); + consumers.append(disabled); + consumers.append(disabled); + + auto printfChannel = softwarePacket(0U); + consumers.append(printfChannel); + + auto streamSpecificDisabled = softwarePacket(1U); + streamSpecificDisabled.traceBusId = 2U; + consumers.append(streamSpecificDisabled); + + auto streamSpecificEnabled = softwarePacket(2U); + streamSpecificEnabled.traceBusId = 2U; + consumers.append(streamSpecificEnabled); + + require(diagnostics.events().size() == 2U, "disabled ITM channels must produce one warning per Trace Bus ID/channel"); + for (const auto& event : diagnostics.events()) { + require(event.severity == DiagnosticSink::Severity::Warning && event.category == DiagnosticSink::Category::Input && + event.code == "itm-channel-not-enabled" && event.impact == DiagnosticSink::Impact::NonFatal, + "ITM enable mismatch diagnostic classification mismatch"); + } +} diff --git a/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp b/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp new file mode 100644 index 000000000..6ce6e5a89 --- /dev/null +++ b/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestPath.hpp" +#include "TestSupport.hpp" +#include +#include "CliOptions.hpp" +#include "TraceDirectoryJob.hpp" +#include "TraceRunConfig.hpp" +#include "TraceRunConfigReader.hpp" +#include +#include +#include + +class RecordingTraceRunConfigReader final : public TraceRunConfigReader { +public: + TraceRunConfig read(const std::string& path) const override + { + paths.push_back(path); + TraceRunConfig config; + config.path = path; + TraceRunSetup setup; + setup.timestamps = TraceRunTimestampSetup{100000000U, 1U}; + config.setups.push_back(setup); + return config; + } + + mutable std::vector paths; +}; + +TEST(CtraceUnitTests, testTraceDirectoryTargetAndOutputNames) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-directory-job-test"); + const auto& root = temporaryPath.path(); + const auto traceDir = root / ".trace"; + writeTestFile(traceDir / "Alpha.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Alpha.SWO.raw"); + writeTestFile(traceDir / "Beta.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Beta.SWO.raw"); + + CliOptions options; + options.traceDir = traceDir.string(); + options.targetName = "Alpha"; + options.outputFormat = OutputFormat::All; + + CollectingDiagnosticSink diagnostics; + RecordingTraceRunConfigReader reader; + TraceDirectoryJob job(options, diagnostics, reader); + const auto checkpoint = diagnostics.fatalCount(); + job.run(); + + require(!diagnostics.hasFatalSince(checkpoint), "TraceDirectoryJob target run failed"); + require(reader.paths.size() == 1U, "TraceDirectoryJob should read one selected YAML file"); + require(std::filesystem::path(reader.paths[0]).filename() == "Alpha.ctrace-run.yml", + "TraceDirectoryJob reader path mismatch"); + require(std::filesystem::is_regular_file(traceDir / "Alpha.SWO.csv"), "TraceDirectoryJob CSV output name mismatch"); + require(std::filesystem::is_regular_file(traceDir / "Alpha.ctf" / "metadata"), + "TraceDirectoryJob CTF output name mismatch"); + require(std::filesystem::is_regular_file(traceDir / "Alpha.SWO.traceanalysis.xml"), + "TraceDirectoryJob XML output name mismatch"); + require(!std::filesystem::exists(traceDir / "Beta.SWO.csv"), + "TraceDirectoryJob should not process unselected target"); +} + +TEST(CtraceUnitTests, testTraceDirectoryBatchCheckAndExplicitConfig) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-directory-check-test"); + const auto& root = temporaryPath.path(); + const auto traceDir = root / ".trace"; + writeTestFile(traceDir / "Alpha.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Alpha.SWO.raw"); + writeTestFile(traceDir / "Beta.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Beta.SWO.raw"); + + CliOptions batchOptions; + batchOptions.traceDir = traceDir.string(); + + CollectingDiagnosticSink diagnostics; + RecordingTraceRunConfigReader batchReader; + TraceDirectoryJob batchJob(batchOptions, diagnostics, batchReader); + const auto batchCheckpoint = diagnostics.fatalCount(); + batchJob.run(); + + require(!diagnostics.hasFatalSince(batchCheckpoint), "TraceDirectoryJob batch check failed"); + require(batchReader.paths.size() == 2U, "TraceDirectoryJob batch should read every trace-run file"); + require(!std::filesystem::exists(traceDir / "Alpha.SWO.csv"), "check-only batch should not create CSV output"); + require(!std::filesystem::exists(traceDir / "Alpha.ctf"), "check-only batch should not create CTF output"); + + writeTestFile(traceDir / "Broken.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Broken.SWO.raw", std::string{static_cast(0x01), 'A'}); + + CliOptions brokenOptions; + brokenOptions.traceDir = traceDir.string(); + brokenOptions.targetName = "Broken"; + + RecordingTraceRunConfigReader brokenReader; + TraceDirectoryJob brokenJob(brokenOptions, diagnostics, brokenReader); + const auto brokenCheckpoint = diagnostics.fatalCount(); + brokenJob.run(); + require(diagnostics.hasFatalSince(brokenCheckpoint), + "check-only trace directory should fail on decoder error packets"); +} diff --git a/tools/ctrace/test/unit/src/decode/CortexMPostDecoderTests.cpp b/tools/ctrace/test/unit/src/decode/CortexMPostDecoderTests.cpp new file mode 100644 index 000000000..a41c8122b --- /dev/null +++ b/tools/ctrace/test/unit/src/decode/CortexMPostDecoderTests.cpp @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.hpp" + +#include + +#include "CortexMPostDecoder.hpp" +#include "OpenCsdTraceElement.hpp" + +#include +#include +#include + +namespace { + +OpenCsdTraceElement localTimestampElement(std::uint64_t tcyc) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::LocalTimestamp; + element.tcyc = tcyc; + return element; +} + +} // namespace + +TEST(CtraceUnitTests, testCortexMPostDecoderUsesLocalTimestampRelations) +{ + const std::array, 4> cases{{ + {LocalTimestampRelation::Synchronous, true}, + {LocalTimestampRelation::TimestampDelayed, true}, + {LocalTimestampRelation::PayloadDelayed, false}, + {LocalTimestampRelation::TimestampAndPayloadDelayed, false}, + }}; + + for (const auto& [relation, expectedReliable] : cases) { + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + OpenCsdTraceElement software; + software.kind = OpenCsdTraceElement::Kind::Software; + software.channel = 1U; + software.size = 1U; + decoder.append(software); + + auto timestamp = localTimestampElement(100U); + timestamp.timestampRelation = relation; + decoder.append(timestamp); + decoder.finish(); + + require(sink.events.size() == 2U, "local timestamp relation event count mismatch"); + require(sink.events.front().quality.has_value() && + sink.events.front().quality->timestampReliable == expectedReliable, + "local timestamp relation reliability mismatch"); + } +} + +TEST(CtraceUnitTests, testCortexMPostDecoderOffsetsTimestampsAfterOverflow) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + decoder.append(localTimestampElement(100)); + + OpenCsdTraceElement overflow; + overflow.kind = OpenCsdTraceElement::Kind::Overflow; + decoder.append(overflow); + + decoder.append(localTimestampElement(10)); + decoder.append(localTimestampElement(15)); + + require(sink.events.size() == 4, "timestamp segment overflow event count mismatch"); + require(sink.events[0].tcyc == 100U, "timestamp segment first tcyc mismatch"); + require(sink.events[2].tcyc == 110U, "timestamp segment post-overflow tcyc mismatch"); + require(sink.events[3].tcyc == 115U, "timestamp segment post-overflow increment mismatch"); +} + +TEST(CtraceUnitTests, testCortexMPostDecoderUsesTimestampOverflowFlag) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + decoder.append(localTimestampElement(100)); + + auto timestamp = localTimestampElement(9); + timestamp.overflow = true; + decoder.append(timestamp); + + require(sink.events.size() == 2, "timestamp overflow flag event count mismatch"); + require(sink.events[1].tcyc == 109U, "post-decoder should use packet overflow flag as segment boundary"); +} + +TEST(CtraceUnitTests, testCortexMPostDecoderMapsDiscontinuityTimestamp) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + decoder.append(localTimestampElement(200)); + OpenCsdTraceElement discontinuity; + discontinuity.kind = OpenCsdTraceElement::Kind::Discontinuity; + decoder.append(discontinuity); + decoder.append(localTimestampElement(7)); + + require(sink.events.size() == 3, "timestamp discontinuity event count mismatch"); + require(sink.events[2].tcyc == 207U, "post-decoder explicit discontinuity timestamp mismatch"); +} diff --git a/tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp b/tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp new file mode 100644 index 000000000..2c238ab3e --- /dev/null +++ b/tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp @@ -0,0 +1,688 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +// Cortex-M post-decoder and end-to-end decode pipeline tests. +#include "TestSupport.hpp" + +#include + +#include "CortexMPostDecoder.hpp" +#include "CortexMStreamDecoder.hpp" +#include "DecodePipeline.hpp" +#include "OpenCsdTraceElement.hpp" +#include "TraceEvent.hpp" + +#include +#include +#include +#include +#include + +namespace { + +const SoftwareTraceEvent* softwareEvent(const TraceEvent& event) +{ + return traceEventPayload(event); +} + +const TraceIssueEvent* issueEvent(const TraceEvent& event) +{ + return traceEventPayload(event); +} + +bool hasSoftwareValue(const std::vector& events, std::uint8_t value, std::uint32_t channel = 0U) +{ + for (const auto& event : events) { + const auto* software = softwareEvent(event); + if (software != nullptr && software->channel == channel && software->size == 1U && software->value == value) { + return true; + } + } + return false; +} + +const TraceIssueEvent* findIssue(const std::vector& events, const std::string& code, + std::optional index = std::nullopt) +{ + for (const auto& event : events) { + const auto* issue = issueEvent(event); + if (issue != nullptr && issue->code == code && (!index.has_value() || event.index == index.value())) { + return issue; + } + } + return nullptr; +} + +std::size_t countIssues(const std::vector& events, const std::string& code) +{ + std::size_t count = 0U; + for (const auto& event : events) { + const auto* issue = issueEvent(event); + if (issue != nullptr && issue->code == code) { + ++count; + } + } + return count; +} + +} // namespace + +TEST(CtraceUnitTests, testCortexMPostDecoderSoftwareTimestampBoundary) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + OpenCsdTraceElement software; + software.kind = OpenCsdTraceElement::Kind::Software; + software.sourceIndex = 4; + software.traceBusId = 1; + software.channel = 0; + software.size = 1; + software.value = 'A'; + decoder.append(software); + + OpenCsdTraceElement timestamp; + timestamp.kind = OpenCsdTraceElement::Kind::LocalTimestamp; + timestamp.sourceIndex = 5; + timestamp.traceBusId = 1; + timestamp.timestampRelation = LocalTimestampRelation::Synchronous; + timestamp.tcyc = 120; + decoder.append(timestamp); + + decoder.finish(); + const auto& packets = sink.events; + require(packets.size() == 2, "CortexMPostDecoder packet count mismatch"); + const auto* decodedSoftware = softwareEvent(packets[0]); + require(decodedSoftware != nullptr, "CortexMPostDecoder first packet type mismatch"); + require(decodedSoftware->channel == 0, "CortexMPostDecoder software channel mismatch"); + require(decodedSoftware->size == 1U && decodedSoftware->value == 'A', "CortexMPostDecoder payload mismatch"); + require(packets[0].tcyc.has_value() && packets[0].tcyc.value() == 120, "CortexMPostDecoder software tcyc mismatch"); + require(packets[0].quality.has_value() && packets[0].quality->timestampReliable, + "CortexMPostDecoder software timestamp reliability mismatch"); + require(isTraceEvent(packets[1]), "CortexMPostDecoder timestamp packet type mismatch"); + require(packets[1].tcyc.has_value() && packets[1].tcyc.value() == 120, "CortexMPostDecoder timestamp tcyc mismatch"); +} + +TEST(CtraceUnitTests, testCortexMStreamDecoderAppliesPerStreamPrescalers) +{ + CollectingEventSink sink; + CortexMStreamDecoder decoder(ItmTimestampPrescalers{1U, {{1U, 4U}, {2U, 16U}}}, sink); + + OpenCsdTraceElement stream1Software; + stream1Software.kind = OpenCsdTraceElement::Kind::Software; + stream1Software.traceBusId = 1U; + stream1Software.channel = 1U; + stream1Software.size = 1U; + stream1Software.value = 0x11U; + decoder.append(stream1Software); + + auto stream2Software = stream1Software; + stream2Software.traceBusId = 2U; + stream2Software.value = 0x22U; + decoder.append(stream2Software); + + OpenCsdTraceElement stream1Timestamp; + stream1Timestamp.kind = OpenCsdTraceElement::Kind::LocalTimestamp; + stream1Timestamp.traceBusId = 1U; + stream1Timestamp.tcyc = 10U; + decoder.append(stream1Timestamp); + + auto stream2Timestamp = stream1Timestamp; + stream2Timestamp.traceBusId = 2U; + decoder.append(stream2Timestamp); + decoder.finish(); + + require(sink.events.size() == 4U, "per-stream timestamp event count mismatch"); + require(sink.events[0].traceBusId == 1U && sink.events[0].tcyc == std::optional(40U), + "stream 1 timestamp prescaler mismatch"); + require(sink.events[2].traceBusId == 2U && sink.events[2].tcyc == std::optional(160U), + "stream 2 timestamp prescaler mismatch"); +} + +TEST(CtraceUnitTests, testCortexMPostDecoderReportsDiscontinuityInterval) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + OpenCsdTraceElement discontinuity; + discontinuity.kind = OpenCsdTraceElement::Kind::Discontinuity; + discontinuity.sourceIndex = 0U; + discontinuity.issueCode = "data-loss"; + discontinuity.errorMessage = "OpenCSD consumed 2 raw bytes"; + discontinuity.rawBytesConsumed = 2U; + decoder.append(discontinuity); + + OpenCsdTraceElement software; + software.kind = OpenCsdTraceElement::Kind::Software; + software.sourceIndex = 8U; + software.channel = 0U; + software.size = 1U; + software.value = static_cast('B'); + decoder.append(software); + + OpenCsdTraceElement timestamp; + timestamp.kind = OpenCsdTraceElement::Kind::LocalTimestamp; + timestamp.sourceIndex = 10U; + timestamp.timestampRelation = LocalTimestampRelation::Synchronous; + timestamp.tcyc = 42U; + decoder.append(timestamp); + + decoder.finish(); + const auto& packets = sink.events; + require(packets.size() == 3U, "discontinuity interval packet count mismatch"); + const auto* discontinuityIssue = issueEvent(packets[0]); + require(discontinuityIssue != nullptr, "discontinuity interval should start with its error"); + require(packets[0].tcyc == std::optional(0U), "discontinuity last valid timestamp mismatch"); + require(discontinuityIssue->lastValidTcyc == std::optional(0U), "discontinuity start field mismatch"); + require(discontinuityIssue->message.find("timestamp 0 .. 42.") != std::string::npos, + "discontinuity interval message mismatch"); + require(isTraceEvent(packets[1]), "discontinuity interval payload order mismatch"); + require(packets[1].tcyc == std::optional(42U), "resumed payload timestamp mismatch"); + require(isTraceEvent(packets[2]), "discontinuity interval timestamp order mismatch"); +} + +TEST(CtraceUnitTests, testCortexMPostDecoderSeparatesRecoveryCauseAndDataLoss) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + OpenCsdTraceElement cause; + cause.kind = OpenCsdTraceElement::Kind::Error; + cause.sourceIndex = 8U; + cause.discontinuity = true; + cause.issueCode = "opencsd-bad-packet-sequence"; + cause.errorMessage = "OpenCSD detected an invalid ITM packet sequence at raw offset 8."; + decoder.append(cause); + + OpenCsdTraceElement loss; + loss.kind = OpenCsdTraceElement::Kind::Error; + loss.sourceIndex = 10U; + loss.awaitingResumeTimestamp = true; + loss.issueCode = "data-loss"; + loss.errorMessage = "OpenCSD consumed 2 raw bytes"; + loss.rawBytesConsumed = 2U; + decoder.append(loss); + + OpenCsdTraceElement timestamp; + timestamp.kind = OpenCsdTraceElement::Kind::LocalTimestamp; + timestamp.sourceIndex = 12U; + timestamp.timestampRelation = LocalTimestampRelation::Synchronous; + timestamp.tcyc = 42U; + decoder.append(timestamp); + + decoder.finish(); + const auto& packets = sink.events; + require(packets.size() == 3U, "recovery cause/data-loss packet count mismatch"); + const auto* causeIssue = issueEvent(packets[0]); + const auto* lossIssue = issueEvent(packets[1]); + require(causeIssue != nullptr && causeIssue->code == "opencsd-bad-packet-sequence", + "recovery cause should be emitted first"); + require(causeIssue->message.find("timestamp") == std::string::npos, + "recovery cause should not contain the data-loss timestamp interval"); + require(lossIssue != nullptr && lossIssue->code == "data-loss", "recovery data loss should be a separate error"); + require(lossIssue->message.find("timestamp 0 .. 42.") != std::string::npos, "recovery data-loss interval mismatch"); + require(packets[0].quality.has_value() && packets[0].quality->overflowCount == 1U && packets[1].quality.has_value() && + packets[1].quality->overflowCount == 1U, + "separate data-loss error must not count a second discontinuity"); + require(isTraceEvent(packets[2]), "recovery timestamp packet missing"); +} + +TEST(CtraceUnitTests, testCortexMPostDecoderOverflowFlushesDwtSegments) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + OpenCsdTraceElement firstTimestamp; + firstTimestamp.kind = OpenCsdTraceElement::Kind::LocalTimestamp; + firstTimestamp.sourceIndex = 1; + firstTimestamp.traceBusId = 1; + firstTimestamp.timestampRelation = LocalTimestampRelation::Synchronous; + firstTimestamp.tcyc = 100; + decoder.append(firstTimestamp); + + OpenCsdTraceElement pc; + pc.kind = OpenCsdTraceElement::Kind::Hardware; + pc.sourceIndex = 2; + pc.traceBusId = 1; + pc.discriminator = 8; + pc.size = 4; + pc.value = 0x08001234U; + decoder.append(pc); + + OpenCsdTraceElement overflow; + overflow.kind = OpenCsdTraceElement::Kind::Overflow; + overflow.sourceIndex = 3; + decoder.append(overflow); + + OpenCsdTraceElement value; + value.kind = OpenCsdTraceElement::Kind::Hardware; + value.sourceIndex = 4; + value.traceBusId = 1; + value.discriminator = 16; + value.size = 4; + value.value = 0x55U; + decoder.append(value); + + OpenCsdTraceElement secondTimestamp; + secondTimestamp.kind = OpenCsdTraceElement::Kind::LocalTimestamp; + secondTimestamp.sourceIndex = 5; + secondTimestamp.traceBusId = 1; + secondTimestamp.timestampRelation = LocalTimestampRelation::Synchronous; + secondTimestamp.tcyc = 20; + decoder.append(secondTimestamp); + + decoder.finish(); + const auto& packets = sink.events; + require(packets.size() == 5, "CortexMPostDecoder overflow segment packet count mismatch"); + require(isTraceEvent(packets[0]), "overflow segment first packet should be timestamp"); + const auto* address = traceEventPayload(packets[1]); + require(address != nullptr, "overflow should flush pending DWT fragment as an address event"); + require(dwtAddressPc(*address) == std::optional(0x08001234U), "flushed DWT PC mismatch"); + require(packets[1].tcyc.has_value() && packets[1].tcyc.value() == 100, "flushed DWT PC timestamp mismatch"); + require(packets[1].quality.has_value() && packets[1].quality->overflow, + "flushed DWT PC should carry overflow status"); + require(!packets[1].quality->timestampReliable, "flushed DWT PC should be timestamp-unreliable"); + require(isTraceEvent(packets[2]), "overflow segment should emit overflow marker"); + const auto* data = traceEventPayload(packets[3]); + require(data != nullptr, "post-overflow DWT value missing"); + require(!data->pc.has_value(), "post-overflow DWT value must not inherit prior PC"); + require(data->value == 0x55U, "post-overflow DWT value mismatch"); + require(packets[3].tcyc.has_value() && packets[3].tcyc.value() == 120, "post-overflow DWT value timestamp mismatch"); + require(packets[3].quality.has_value() && packets[3].quality->overflow, + "post-overflow DWT value should carry overflow status"); + require(!packets[3].quality->timestampReliable, "post-overflow DWT value should be timestamp-unreliable"); + require(isTraceEvent(packets[4]), "overflow segment final packet should be timestamp"); +} + +TEST(CtraceUnitTests, testCortexMPostDecoderPreservesDecoderTimestamps) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + OpenCsdTraceElement firstTimestamp; + firstTimestamp.kind = OpenCsdTraceElement::Kind::LocalTimestamp; + firstTimestamp.sourceIndex = (std::uint64_t{1} << 32U) + 1U; + firstTimestamp.timestampRelation = LocalTimestampRelation::Synchronous; + firstTimestamp.tcyc = 100; + decoder.append(firstTimestamp); + + OpenCsdTraceElement secondTimestamp; + secondTimestamp.kind = OpenCsdTraceElement::Kind::LocalTimestamp; + secondTimestamp.sourceIndex = 2; + secondTimestamp.timestampRelation = LocalTimestampRelation::Synchronous; + secondTimestamp.tcyc = 10; + decoder.append(secondTimestamp); + + decoder.finish(); + const auto& packets = sink.events; + require(packets.size() == 2, "preserve timestamp packet count mismatch"); + require(packets[0].index == firstTimestamp.sourceIndex, "raw offsets must not truncate above 4 GiB"); + require(packets[0].tcyc.has_value() && packets[0].tcyc.value() == 100, "first preserved timestamp mismatch"); + require(packets[1].tcyc.has_value() && packets[1].tcyc.value() == 10, "second preserved timestamp mismatch"); +} + +TEST(CtraceUnitTests, testCortexMPostDecoderPreservesGlobalTimestampOrder) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + OpenCsdTraceElement software; + software.kind = OpenCsdTraceElement::Kind::Software; + software.sourceIndex = 1U; + software.channel = 1U; + software.size = 1U; + software.value = static_cast('A'); + decoder.append(software); + + OpenCsdTraceElement dwtPc; + dwtPc.kind = OpenCsdTraceElement::Kind::Hardware; + dwtPc.sourceIndex = 2U; + dwtPc.discriminator = 8U; + dwtPc.size = 4U; + dwtPc.value = 0x08001234U; + decoder.append(dwtPc); + + OpenCsdTraceElement globalTimestamp; + globalTimestamp.kind = OpenCsdTraceElement::Kind::GlobalTimestamp; + globalTimestamp.sourceIndex = 3U; + globalTimestamp.timestampValue = 0x123456789abcdef0ULL; + decoder.append(globalTimestamp); + + OpenCsdTraceElement warning; + warning.kind = OpenCsdTraceElement::Kind::Error; + warning.sourceIndex = 4U; + warning.issueCode = "opencsd-warning"; + warning.issueSeverity = TraceIssueSeverity::Warning; + warning.errorMessage = "decoder warning"; + decoder.append(warning); + + OpenCsdTraceElement localTimestamp; + localTimestamp.kind = OpenCsdTraceElement::Kind::LocalTimestamp; + localTimestamp.sourceIndex = 5U; + localTimestamp.timestampRelation = LocalTimestampRelation::Synchronous; + localTimestamp.tcyc = 42U; + decoder.append(localTimestamp); + decoder.finish(); + + require(sink.events.size() == 5U, "global timestamp order packet count mismatch"); + require(isTraceEvent(sink.events[0]), + "global timestamp must not overtake preceding software data"); + require(isTraceEvent(sink.events[1]), + "global timestamp must flush and follow preceding DWT data"); + const auto* timestamp = traceEventPayload(sink.events[2]); + require(timestamp != nullptr && timestamp->value == globalTimestamp.timestampValue, + "global timestamp payload/order mismatch"); + require(sink.events[0].tcyc == std::optional(42U) && + sink.events[1].tcyc == std::optional(42U), + "preceding payloads must retain the following local timestamp"); + require(!sink.events[2].tcyc.has_value(), "global timestamp must remain independent of the local timestamp domain"); + require(!sink.events[2].quality.has_value(), "global timestamp must not acquire local trace quality"); + require(isTraceEvent(sink.events[3]) && sink.events[3].tcyc == std::optional(42U), + "a warning must not overtake pending payload or global timestamp packets"); + require(isTraceEvent(sink.events[4]), + "local timestamp must remain after the global timestamp boundary"); +} + +TEST(CtraceUnitTests, testDecodePipelineRecoversAtRealSync) +{ + CollectingEventSink sink; + DecodePipeline pipeline(16, sink); + + const std::uint8_t trace[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), 0x00U, 0xfeU, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('B'), + }; + pipeline.push({trace, sizeof(trace)}); + const auto result = pipeline.finish(); + + require(result.bytesIn == sizeof(trace), "recovery byte count mismatch"); + require(hasSoftwareValue(sink.events, static_cast('A')), + "recovery should preserve packets before the damaged section"); + const auto* error = findIssue(sink.events, "opencsd-bad-packet-sequence", 8U); + require(error != nullptr && error->message == "OpenCSD detected an invalid ITM packet sequence at raw offset 8.", + "recovery should report the exact OpenCSD error offset"); + require(hasSoftwareValue(sink.events, static_cast('B')), + "recovery should resume after the next real ITM sync"); +} + +TEST(CtraceUnitTests, testDecodePipelineKeepsCallbacksAttachedAcrossRepeatedResets) +{ + CollectingEventSink sink; + DecodePipeline pipeline(16U, sink); + + const std::uint8_t trace[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), 0x00U, 0xfeU, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('B'), 0x00U, 0xfeU, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('C'), + }; + pipeline.push({trace, sizeof(trace)}); + const auto result = pipeline.finish(); + + require(result.bytesIn == sizeof(trace), "repeated recovery byte count mismatch"); + require(hasSoftwareValue(sink.events, static_cast('A')) && + hasSoftwareValue(sink.events, static_cast('B')) && + hasSoftwareValue(sink.events, static_cast('C')), + "decoder resets must retain all OpenCSD callbacks"); + require(countIssues(sink.events, "opencsd-bad-packet-sequence") == 2U, + "each damaged section must trigger one recoverable reset"); +} + +TEST(CtraceUnitTests, testDecodePipelineReconstructsGlobalTimestamp) +{ + CollectingEventSink sink; + DecodePipeline pipeline(1U, sink); + + constexpr std::uint64_t lower = 0x00f23456ULL; + constexpr std::uint64_t upper = 0x1020304c000000ULL; + std::vector trace{ + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x80U, + 0x94U, + static_cast(0x80U | ((lower >> 0U) & 0x7fU)), + static_cast(0x80U | ((lower >> 7U) & 0x7fU)), + static_cast(0x80U | ((lower >> 14U) & 0x7fU)), + static_cast((lower >> 21U) & 0x1fU), + 0xb4U, + static_cast(0x80U | ((upper >> 26U) & 0x7fU)), + static_cast(0x80U | ((upper >> 33U) & 0x7fU)), + static_cast(0x80U | ((upper >> 40U) & 0x7fU)), + static_cast(0x80U | ((upper >> 47U) & 0x7fU)), + static_cast(0x80U | ((upper >> 54U) & 0x7fU)), + static_cast((upper >> 61U) & 0x07U), + }; + pipeline.push({trace.data(), trace.size()}); + const auto result = pipeline.finish(); + + require(result.bytesIn == trace.size(), "global timestamp input byte count mismatch"); + std::size_t globalTimestampCount = 0U; + for (const auto& packet : sink.events) { + if (const auto* timestamp = traceEventPayload(packet)) { + ++globalTimestampCount; + require(timestamp->value == (upper | lower), "GTS1/GTS2 reconstruction lost timestamp bits"); + require(!timestamp->clockChange, "unexpected global timestamp clock-change flag"); + } + } + require(globalTimestampCount == 1U, "GTS1/GTS2 must produce exactly one reconstructed global timestamp"); +} + +TEST(CtraceUnitTests, testDecodePipelineAppliesPrescalerAfterOpenCsd) +{ + CollectingEventSink sink; + DecodePipeline pipeline(16U, sink); + const std::uint8_t trace[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x09U, 0x41U, 0x20U, + }; + + pipeline.push({trace, sizeof(trace)}); + pipeline.finish(); + + bool foundScaledSoftware = false; + for (const auto& event : sink.events) { + const auto* software = softwareEvent(event); + foundScaledSoftware = foundScaledSoftware || (software != nullptr && software->channel == 1U && + event.tcyc == std::optional(32U)); + } + require(foundScaledSoftware, "DecodePipeline must apply the ITM prescaler after OpenCSD decoding"); +} + +TEST(CtraceUnitTests, testDecodePipelineRecoversWhenErrorSpansChunks) +{ + CollectingEventSink sink; + DecodePipeline pipeline(16, sink); + + const std::uint8_t firstChunk[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), 0x00U, + }; + const std::uint8_t secondChunk[] = { + 0xfeU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('B'), + }; + pipeline.push({firstChunk, sizeof(firstChunk)}); + pipeline.push({secondChunk, sizeof(secondChunk)}); + const auto result = pipeline.finish(); + + require(result.bytesIn == sizeof(firstChunk) + sizeof(secondChunk), "split recovery byte count mismatch"); + require(findIssue(sink.events, "opencsd-bad-packet-sequence", 8U) != nullptr, + "split bad packet should retain its header offset"); + require(hasSoftwareValue(sink.events, static_cast('B')), + "recovery should resume after a split bad packet"); +} + +TEST(CtraceUnitTests, testDecodePipelineRecoversFromReservedHeader) +{ + CollectingEventSink sink; + DecodePipeline pipeline(16, sink); + + const std::uint8_t trace[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), 0x04U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('B'), + }; + pipeline.push({trace, sizeof(trace)}); + pipeline.finish(); + + const auto* error = findIssue(sink.events, "opencsd-invalid-packet-header", 8U); + require(error != nullptr && error->message == "OpenCSD detected an invalid ITM packet header at raw offset 8.", + "reserved header should report its exact OpenCSD error"); + require(hasSoftwareValue(sink.events, static_cast('B')), + "recovery should resume after a reserved header"); +} + +TEST(CtraceUnitTests, testDecodePipelineFinishesWithoutLaterSync) +{ + CollectingEventSink sink; + DecodePipeline pipeline(16, sink); + + const std::uint8_t trace[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), 0x00U, 0xfeU, 0xffU, 0xffU, + }; + pipeline.push({trace, sizeof(trace)}); + const auto result = pipeline.finish(); + + require(result.bytesIn == sizeof(trace), "bad-tail byte count mismatch"); + require(findIssue(sink.events, "opencsd-bad-packet-sequence", 8U) != nullptr, + "bad tail should report its exact OpenCSD error and finish"); +} + +TEST(CtraceUnitTests, testDecodePipelineReportsIncompletePacketAtEndOfInput) +{ + CollectingEventSink sink; + DecodePipeline pipeline(16, sink); + + const std::uint8_t trace[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), 0x03U, 0x12U, + }; + pipeline.push({trace, sizeof(trace)}); + pipeline.finish(); + + require(!sink.events.empty(), "incomplete trace should emit packets"); + const auto& last = sink.events.back(); + const auto* issue = issueEvent(last); + require(issue != nullptr, "incomplete trace should end with an error packet"); + require(issue->code == "opencsd-incomplete-tail", "incomplete trace issue code mismatch"); + require(last.index == 8U, "incomplete trace should report the partial packet header offset"); +} + +TEST(CtraceUnitTests, testDecodePipelineRecoversFromOverlongContinuationPackets) +{ + const std::vector> damagedPackets{ + {0x80U, 0x80U, 0x80U, 0x80U, 0x80U}, + {0x94U, 0x80U, 0x80U, 0x80U, 0x80U}, + {0xb4U, 0x80U, 0x80U, 0x80U, 0x80U, 0x80U, 0x80U}, + {0x88U, 0x80U, 0x80U, 0x80U, 0x80U}, + }; + + for (const auto& damaged : damagedPackets) { + CollectingEventSink sink; + DecodePipeline pipeline(16, sink); + std::vector trace{ + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), + }; + trace.insert(trace.end(), damaged.begin(), damaged.end()); + trace.insert(trace.end(), {0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U}); + trace.insert(trace.end(), {0x01U, static_cast('B')}); + + pipeline.push({trace.data(), trace.size()}); + pipeline.finish(); + + require(findIssue(sink.events, "opencsd-bad-packet-sequence") != nullptr, + "overlong continuation packet should emit the OpenCSD error"); + require(hasSoftwareValue(sink.events, static_cast('B')), + "decode should resume after an overlong continuation packet"); + } +} + +TEST(CtraceUnitTests, testDecodePipelinePreservesDwtEventAndPmuPackets) +{ + CollectingEventSink sink; + DecodePipeline pipeline(16, sink); + + const std::uint8_t trace[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x05U, 0x21U, 0x1dU, 0x81U, + }; + pipeline.push({trace, sizeof(trace)}); + pipeline.finish(); + + bool foundEvent = false; + bool foundPmu = false; + for (const auto& packet : sink.events) { + const auto* event = traceEventPayload(packet); + const auto* pmu = traceEventPayload(packet); + foundEvent = + foundEvent || (event != nullptr && event->discriminator == 0U && event->size == 1U && event->value == 0x21U); + foundPmu = foundPmu || (pmu != nullptr && pmu->discriminator == 3U && pmu->size == 1U && pmu->value == 0x81U); + } + require(foundEvent, "OpenCSD DWT event-counter packet must survive post-decoding"); + require(foundPmu, "OpenCSD PMU-overflow packet must survive post-decoding"); +} + +TEST(CtraceUnitTests, testDecodePipelineDoesNotInjectSync) +{ + CollectingEventSink sink; + DecodePipeline pipeline(16, sink); + + const std::uint8_t validWithoutAsync[] = {0x01U, static_cast('A')}; + pipeline.push({validWithoutAsync, sizeof(validWithoutAsync)}); + const auto result = pipeline.finish(); + + require(result.bytesIn == sizeof(validWithoutAsync), "decode byte count mismatch"); + bool foundPayload = false; + bool foundDataLoss = false; + for (const auto& packet : sink.events) { + if (softwareEvent(packet) != nullptr) { + foundPayload = true; + } + const auto* issue = issueEvent(packet); + if (issue != nullptr && issue->code == "data-loss") { + foundDataLoss = true; + require(issue->rawBytesConsumed == std::optional(sizeof(validWithoutAsync)), + "decode should count all raw bytes consumed before synchronization"); + require(issue->message.find("OpenCSD consumed 2 raw bytes") != std::string::npos, + "decode data-loss message should include the consumed raw-byte count"); + require(issue->message.find("timestamp 0 .. unknown.") != std::string::npos, + "decode should mark a missing post-recovery timestamp as unknown"); + } + } + require(!foundPayload, "decode should not decode sync-less data by injecting sync"); + require(foundDataLoss, "decode should mark sync-less data loss"); +} + +TEST(CtraceUnitTests, testDecodePipelineCountsBytesUntilRealSync) +{ + CollectingEventSink sink; + DecodePipeline pipeline(16, sink); + + const std::uint8_t leading[] = {0x01U, static_cast('A')}; + pipeline.push({leading, sizeof(leading)}); + const std::uint8_t synchronized[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('B'), + }; + pipeline.push({synchronized, sizeof(synchronized)}); + pipeline.finish(); + + bool foundDataLoss = false; + bool foundPayloadAfterDataLoss = false; + for (const auto& packet : sink.events) { + const auto* issue = issueEvent(packet); + if (issue != nullptr && issue->code == "data-loss") { + foundDataLoss = true; + require(issue->rawBytesConsumed == std::optional(sizeof(leading)), + "decode should count bytes only up to the real sync offset"); + continue; + } + const auto* software = softwareEvent(packet); + if (foundDataLoss && software != nullptr && software->channel == 0U && software->size == 1U && + software->value == static_cast('B')) { + foundPayloadAfterDataLoss = true; + } + } + + require(foundDataLoss, "decode should emit data loss before the recovered stream"); + require(foundPayloadAfterDataLoss, "decode should emit synchronized payload after the counted data loss"); +} diff --git a/tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp b/tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp new file mode 100644 index 000000000..760368c9b --- /dev/null +++ b/tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +// DWT packet decoding and explicitly deferred DWT feature tests. +#include "TestSupport.hpp" + +#include + +#include "csv/CsvRowMapper.hpp" +#include "DwtPacketDecoder.hpp" +#include "TraceEvent.hpp" +#include "TraceSelection.hpp" + +#include +#include + +TEST(CtraceUnitTests, testDwtPcSampleIsSuppressedUntilDedicatedEventExists) +{ + DwtPacketDecoder decoder; + DwtPayloadPacket payload; + payload.index = 19U; + payload.traceBusId = 3U; + payload.discriminator = 2U; + payload.size = 4U; + payload.value = 0x08001234U; + payload.tcyc = 949339000U; + payload.status.timestampReliable = true; + + const auto packets = decoder.decode(payload); + require(packets.empty(), "DWT PC samples must remain suppressed until their output event is defined"); +} + +TEST(CtraceUnitTests, testDwtCounterPacketsArePreservedUntilOutputSemanticsExist) +{ + const auto verify = [](std::uint8_t discriminator, const char* selector) { + DwtPacketDecoder decoder; + DwtPayloadPacket payload; + payload.index = 23U; + payload.traceBusId = 4U; + payload.discriminator = discriminator; + payload.size = 1U; + payload.value = 0x21U; + payload.tcyc = 949339100U; + payload.status.overflow = true; + payload.status.timestampReliable = false; + payload.status.overflowCount = 7U; + + const auto packets = decoder.decode(payload); + require(packets.size() == 1U, "DWT counter packet must be preserved internally"); + const auto& packet = packets.front(); + const auto* event = traceEventPayload(packet); + const auto* pmu = traceEventPayload(packet); + require((discriminator == 0U && event != nullptr && pmu == nullptr) || + (discriminator == 3U && event == nullptr && pmu != nullptr), + "DWT counter semantic event type mismatch"); + const auto actualDiscriminator = event != nullptr ? event->discriminator : pmu->discriminator; + const auto actualSize = event != nullptr ? event->size : pmu->size; + const auto actualValue = event != nullptr ? event->value : pmu->value; + require(actualDiscriminator == discriminator, "DWT counter discriminator mismatch"); + require(actualSize == 1U && actualValue == 0x21U, "DWT counter payload mismatch"); + require(packet.index == 23U && packet.traceBusId == 4U, "DWT counter identity mismatch"); + require(packet.tcyc == 949339100U, "DWT counter timestamp mismatch"); + require(packet.quality.has_value() && packet.quality->overflow, "DWT counter overflow status mismatch"); + require(packet.quality->overflowCount == 7U, "DWT counter overflow count mismatch"); + require(!traceEventType(packet).has_value(), "unimplemented DWT counter must not expose an output type"); + require(!traceEventSelectedForOutput(packet, TraceSelection{}), + "DWT counter packet must remain disabled by default"); + require(!traceEventSelectedForOutput(packet, TraceSelection{{selector}, {}}), + "DWT counter selector must remain disabled until output semantics exist"); + }; + + verify(0U, "event"); + verify(3U, "pmu"); +} + +TEST(CtraceUnitTests, testDwtPacketDecoderRejectsReservedExceptionAction) +{ + DwtPacketDecoder decoder; + DwtPayloadPacket payload; + payload.index = 17; + payload.traceBusId = 3; + payload.discriminator = 1; + payload.size = 2; + payload.value = 11; + payload.tcyc = 1234; + payload.status.timestampReliable = true; + + const auto packets = decoder.decode(payload); + require(packets.size() == 1U, "DwtPacketDecoder reserved exception action packet count mismatch"); + const auto* issue = traceEventPayload(packets[0]); + require(issue != nullptr, "DwtPacketDecoder reserved exception action should emit only an error"); + require(issue->code == "invalid-exception-action", "DwtPacketDecoder reserved exception action code mismatch"); + require(issue->message == "invalid exception action 0x0 for exception 11", + "DwtPacketDecoder reserved exception action message mismatch"); + require(packets[0].index == 17U && packets[0].traceBusId == 3U, + "DwtPacketDecoder reserved exception action identity mismatch"); + require(packets[0].tcyc.has_value() && *packets[0].tcyc == 1234U, + "DwtPacketDecoder reserved exception action timestamp mismatch"); + require(CsvRowMapper::row(packets[0]) == "1234,3,error,,,,,invalid exception action 0x0 for exception 11", + "DwtPacketDecoder reserved exception error CSV mismatch"); +} + +TEST(CtraceUnitTests, testDwtPacketDecoderFlushesPendingEventsInRawOrder) +{ + DwtPacketDecoder decoder; + + DwtPayloadPacket comparatorThree; + comparatorThree.index = 10U; + comparatorThree.traceBusId = 3U; + comparatorThree.discriminator = 14U; + comparatorThree.size = 4U; + comparatorThree.value = 0x08003000U; + require(decoder.decode(comparatorThree).empty(), "an incomplete DWT comparator-three event must remain pending"); + + DwtPayloadPacket comparatorZero; + comparatorZero.index = 20U; + comparatorZero.traceBusId = 4U; + comparatorZero.discriminator = 8U; + comparatorZero.size = 4U; + comparatorZero.value = 0x08000000U; + require(decoder.decode(comparatorZero).empty(), "an incomplete DWT comparator-zero event must remain pending"); + + const auto packets = decoder.flush({}, 123U); + require(packets.size() == 2U, "DWT flush must emit both pending comparator events"); + require(packets[0].index == 10U && packets[1].index == 20U, + "DWT flush must preserve raw-stream order across comparators"); + require(packets[0].traceBusId == 3U && packets[1].traceBusId == 4U, + "DWT flush must preserve the identity of each pending event"); + const auto* first = traceEventPayload(packets[0]); + const auto* second = traceEventPayload(packets[1]); + require(first != nullptr && first->comparator == 3U && second != nullptr && second->comparator == 0U, + "DWT raw-order regression must not rewrite comparator identities"); +} + +TEST(CtraceUnitTests, testDwtPacketDecoderPreservesRepeatedAddressFragments) +{ + const auto verify = [](std::uint8_t discriminator, std::uint32_t firstValue, std::uint32_t secondValue) { + DwtPacketDecoder decoder; + DwtPayloadPacket first; + first.index = 10U; + first.traceBusId = 3U; + first.discriminator = discriminator; + first.size = (discriminator & 1U) == 0U ? 4U : 2U; + first.value = firstValue; + require(decoder.decode(first).empty(), "first DWT address fragment must remain pending"); + + auto packets = decoder.decode(DwtPayloadPacket{ + 20U, + 4U, + discriminator, + static_cast((discriminator & 1U) == 0U ? 4U : 2U), + secondValue, + 200U, + {}, + }); + require(packets.size() == 1U, "a repeated DWT address fragment must flush its predecessor"); + const auto* firstAddress = traceEventPayload(packets.front()); + require(firstAddress != nullptr && packets.front().index == 10U && packets.front().traceBusId == 3U, + "the first repeated DWT address fragment lost its identity"); + const auto firstPc = dwtAddressPc(*firstAddress); + const auto firstOffset = dwtAddressOffset(*firstAddress); + + packets = decoder.flush({}, 300U); + require(packets.size() == 1U, "the second DWT address fragment must remain available"); + const auto* secondAddress = traceEventPayload(packets.front()); + require(secondAddress != nullptr && packets.front().index == 20U && packets.front().traceBusId == 4U, + "the second repeated DWT address fragment lost its identity"); + + if (discriminator == 8U) { + require(firstPc == std::optional(firstValue) && + dwtAddressPc(*secondAddress) == std::optional(secondValue), + "repeated DWT PC fragments were overwritten"); + } else { + require(firstOffset == std::optional(firstValue) && + dwtAddressOffset(*secondAddress) == std::optional(secondValue), + "repeated DWT offset fragments were overwritten"); + } + }; + + verify(8U, 0x08001000U, 0x08002000U); + verify(9U, 0x1000U, 0x2000U); +} + +TEST(CtraceUnitTests, testDwtPacketDecoderRejectsUnsupportedAddressWidths) +{ + const auto verify = [](std::uint8_t discriminator, std::uint8_t size) { + DwtPacketDecoder decoder; + DwtPayloadPacket payload; + payload.index = 17U; + payload.traceBusId = 3U; + payload.discriminator = discriminator; + payload.size = size; + payload.value = 0x12345678U; + payload.tcyc = 99U; + payload.status.timestampReliable = true; + + const auto packets = decoder.decode(payload); + require(packets.size() == 1U, "unsupported DWT address width must emit one error"); + const auto* issue = traceEventPayload(packets.front()); + require(issue != nullptr && issue->code == "unsupported-dwt-address-payload" && + issue->severity == TraceIssueSeverity::Error, + "unsupported DWT address width diagnostic mismatch"); + require(packets.front().index == 17U && packets.front().traceBusId == 3U && + packets.front().tcyc == std::optional(99U), + "unsupported DWT address width diagnostic lost packet identity"); + }; + + verify(8U, 1U); + verify(9U, 4U); +} diff --git a/tools/ctrace/test/unit/src/decode/OpenCsdErrorControllerTests.cpp b/tools/ctrace/test/unit/src/decode/OpenCsdErrorControllerTests.cpp new file mode 100644 index 000000000..1645adfce --- /dev/null +++ b/tools/ctrace/test/unit/src/decode/OpenCsdErrorControllerTests.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.hpp" + +#include + +#include "OpenCsdErrorController.hpp" +#include "common/ocsd_error.h" +#include "opencsd/ocsd_if_types.h" + +#include + +TEST(CtraceUnitTests, testOpenCsdErrorControllerClassifiesResponses) +{ + OpenCsdErrorController controller; + const auto source = controller.RegisterErrorSource("ITM packet processor"); + require(controller.RegisterErrorSource("ITM packet processor") == source, + "recreated OpenCSD components should reuse their logger source handle"); + + controller.beginDataPathCall(); + const ocsdError recoverable(OCSD_ERR_SEV_ERROR, OCSD_ERR_BAD_PACKET_SEQ, 123U, 1U, "invalid async sequence"); + controller.LogError(source, &recoverable); + const auto recovery = controller.decide(OCSD_RESP_FATAL_INVALID_DATA); + require(recovery.action == OpenCsdErrorController::Action::RecoverStream, + "bad packet sequence should be recoverable"); + require(recovery.error.has_value() && recovery.error->hasIndex && recovery.error->index == 123U, + "OpenCSD error offset should be preserved"); + require(recovery.errors.size() == 1U && recovery.errors[0].code == OCSD_ERR_BAD_PACKET_SEQ, + "OpenCSD decision should retain every error reported for the datapath call"); + const auto description = OpenCsdErrorController::describe(recovery); + require(description.find("OCSD_ERR_BAD_PACKET_SEQ") != std::string::npos && + description.find("raw offset 123") != std::string::npos, + "OpenCSD recovery description should contain code and offset"); + require(OpenCsdErrorController::describeSummary(recovery) == + "OpenCSD detected an invalid ITM packet sequence at raw offset 123.", + "OpenCSD error module should provide a concise user-facing summary"); + require(OpenCsdErrorController::issueCode(recovery) == "opencsd-bad-packet-sequence", + "OpenCSD error module should own issue-code selection"); + require(OpenCsdErrorController::describeApiError(OCSD_ERR_MEM, "decoder setup failed") == + "decoder setup failed (OCSD_ERR_MEM)", + "OpenCSD error module should own API error formatting"); + + controller.beginDataPathCall(); + require(controller.decide(OCSD_RESP_FATAL_INVALID_DATA).action == OpenCsdErrorController::Action::Abort, + "fatal response without a classified stream error should abort"); + + controller.beginDataPathCall(); + const ocsdError invalidHeader(OCSD_ERR_SEV_ERROR, OCSD_ERR_INVALID_PCKT_HDR, 456U, 1U, "reserved packet header"); + controller.LogError(source, &invalidHeader); + require(controller.decide(OCSD_RESP_CONT).action == OpenCsdErrorController::Action::RecoverStream, + "invalid packet header should be recoverable even when forwarding is suppressed"); + require(controller.decide(OCSD_RESP_FATAL_SYS_ERR).action == OpenCsdErrorController::Action::Abort, + "system-fatal response must not be reclassified as a stream recovery"); + + controller.beginDataPathCall(); + const ocsdError internalFailure(OCSD_ERR_SEV_ERROR, OCSD_ERR_MEM, "allocation failed"); + controller.LogError(source, &internalFailure); + require(controller.decide(OCSD_RESP_FATAL_INVALID_DATA).action == OpenCsdErrorController::Action::Abort, + "non-stream OpenCSD error should abort on a fatal response"); + require(controller.decide(OCSD_RESP_ERR_CONT).action == OpenCsdErrorController::Action::Continue, + "a non-stream OpenCSD error should continue when the datapath explicitly permits it"); + + controller.beginDataPathCall(); + controller.LogError(source, &recoverable); + controller.LogError(source, &internalFailure); + const auto mixedFailure = controller.decide(OCSD_RESP_FATAL_INVALID_DATA); + require(mixedFailure.action == OpenCsdErrorController::Action::Abort, + "a recoverable stream error must not hide a non-recoverable error from the same call"); + require(mixedFailure.error.has_value() && mixedFailure.error->code == OCSD_ERR_MEM, + "fatal mixed-error calls should identify the non-recoverable cause"); + require(mixedFailure.errors.size() == 2U, "mixed OpenCSD errors should all be retained"); + + controller.beginDataPathCall(); + const ocsdError warning(OCSD_ERR_SEV_WARN, OCSD_ERR_BAD_PACKET_SEQ, 789U, 1U, "warning-only packet observation"); + controller.LogError(source, &warning); + require(controller.decide(OCSD_RESP_WARN_CONT).action == OpenCsdErrorController::Action::Continue, + "warning-only packet observations should not trigger stream recovery"); + require(controller.decide(OCSD_RESP_FATAL_INVALID_DATA).action == OpenCsdErrorController::Action::Abort, + "a warning must not justify recovery from a fatal invalid-data response"); + + controller.beginDataPathCall(); + require(controller.decide(OCSD_RESP_WAIT).action == OpenCsdErrorController::Action::Wait, + "WAIT response should request a flush"); + require(controller.decide(OCSD_RESP_CONT).action == OpenCsdErrorController::Action::Continue, + "CONT response should continue"); +} diff --git a/tools/ctrace/test/unit/src/decode/OpenCsdPacketCollectorTests.cpp b/tools/ctrace/test/unit/src/decode/OpenCsdPacketCollectorTests.cpp new file mode 100644 index 000000000..0815ac44e --- /dev/null +++ b/tools/ctrace/test/unit/src/decode/OpenCsdPacketCollectorTests.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.hpp" + +#include + +#include "csv/CsvRowMapper.hpp" +#include "OpenCsdPacketCollector.hpp" +#include "OpenCsdTraceElement.hpp" +#include "TraceEvent.hpp" +#include "common/trc_gen_elem.h" +#include "opencsd/itm/trc_pkt_elem_itm.h" +#include "opencsd/itm/trc_pkt_types_itm.h" +#include "opencsd/ocsd_if_types.h" +#include "opencsd/trc_gen_elem_types.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +class CollectingTraceElementSink final : public OpenCsdTraceElementSink { +public: + void append(OpenCsdTraceElement element) override + { + elements.push_back(std::move(element)); + } + + std::vector elements; +}; + +} // namespace + +TEST(CtraceUnitTests, testOpenCsdPacketCollectorUsesReconstructedGlobalTimestamp) +{ + CollectingTraceElementSink sink; + OpenCsdPacketCollector collector(sink); + + OcsdTraceElement globalTimestamp; + globalTimestamp.setType(OCSD_GEN_TRC_ELEM_ITMTRACE); + swt_itm_info info{}; + info.pkt_type = TS_GLOBAL; + globalTimestamp.setSWT_ITMInfo(info); + globalTimestamp.setTS(0xfedcba9876543210ULL, true); + + require(collector.TraceElemIn(42U, 7U, globalTimestamp) == OCSD_RESP_CONT, + "OpenCSD global timestamp collection failed"); + require(sink.elements.size() == 1U, "reconstructed global timestamp element missing"); + const auto element = sink.elements.front(); + require(element.kind == OpenCsdTraceElement::Kind::GlobalTimestamp, "reconstructed global timestamp kind mismatch"); + require(element.sourceIndex == 42U, "reconstructed global timestamp source index mismatch"); + require(element.traceBusId == 7U, "OpenCSD Trace Bus ID was not preserved"); + require(element.timestampValue == 0xfedcba9876543210ULL, "reconstructed global timestamp value mismatch"); + require(element.clockChange, "reconstructed global timestamp clock-change flag missing"); + + require(collector.TraceElemIn(43U, 0xffU, globalTimestamp) == OCSD_RESP_CONT, + "OpenCSD global timestamp collection with missing source ID failed"); + require(sink.elements.back().traceBusId == 0U, "an unavailable OpenCSD Trace Bus ID must fall back to zero"); + + ItmTrcPacket rawGts1; + rawGts1.setPktType(ITM_PKT_TS_GLOBAL_1); + rawGts1.setValue(0x123456U, 4U); + collector.RawPacketDataMon(OCSD_OP_DATA, 43U, &rawGts1, 0U, nullptr); + + ItmTrcPacket rawGts2; + rawGts2.setPktType(ITM_PKT_TS_GLOBAL_2); + rawGts2.setExtValue(0x123456789ULL); + collector.RawPacketDataMon(OCSD_OP_DATA, 44U, &rawGts2, 0U, nullptr); + require(sink.elements.size() == 2U, "raw GTS fragments must not be published as independent global timestamps"); + + TraceEvent packet{GlobalTimestampTraceEvent{element.timestampValue, false}}; + require(CsvRowMapper::row(packet) == std::to_string(element.timestampValue) + ",0,global_ts,,,,,", + "CSV must preserve all 64 global timestamp bits in the cycles column"); +} + +TEST(CtraceUnitTests, testOpenCsdPacketCollectorMapsLocalTimestampRelations) +{ + const std::array, 4> cases{{ + {TS_SYNC, LocalTimestampRelation::Synchronous}, + {TS_DELAY, LocalTimestampRelation::TimestampDelayed}, + {TS_PKT_DELAY, LocalTimestampRelation::PayloadDelayed}, + {TS_PKT_TS_DELAY, LocalTimestampRelation::TimestampAndPayloadDelayed}, + }}; + + CollectingTraceElementSink sink; + OpenCsdPacketCollector collector(sink); + for (std::size_t index = 0; index < cases.size(); ++index) { + OcsdTraceElement timestamp; + timestamp.setType(OCSD_GEN_TRC_ELEM_ITMTRACE); + swt_itm_info info{}; + info.pkt_type = cases[index].first; + timestamp.setSWT_ITMInfo(info); + timestamp.setTS(100U + index, false); + + require(collector.TraceElemIn(index, 0U, timestamp) == OCSD_RESP_CONT, "OpenCSD local timestamp collection failed"); + } + + require(sink.elements.size() == cases.size(), "local timestamp relation count mismatch"); + for (std::size_t index = 0; index < cases.size(); ++index) { + require(sink.elements[index].kind == OpenCsdTraceElement::Kind::LocalTimestamp, "local timestamp kind mismatch"); + require(sink.elements[index].timestampRelation == cases[index].second, "local timestamp relation mismatch"); + } +} diff --git a/tools/ctrace/test/unit/src/diagnostics/DiagnosticsTests.cpp b/tools/ctrace/test/unit/src/diagnostics/DiagnosticsTests.cpp new file mode 100644 index 000000000..cd4c1424a --- /dev/null +++ b/tools/ctrace/test/unit/src/diagnostics/DiagnosticsTests.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.hpp" +#include +#include "TraceIssueReporter.hpp" +#include +#include + +TEST(CtraceUnitTests, testDiagnosticCollection) +{ + CollectingDiagnosticSink sink; + sink.report({ + DiagnosticSink::Severity::Warning, + DiagnosticSink::Category::Decode, + "timestamp", + "timestamp discontinuity", + {{"offset", "42"}}, + }); + + require(sink.events().size() == 1, "diagnostic event count mismatch"); + require(sink.events()[0].severity == DiagnosticSink::Severity::Warning, "diagnostic severity mismatch"); + require(sink.events()[0].category == DiagnosticSink::Category::Decode, "diagnostic category mismatch"); + require(toString(sink.events()[0].severity) == "warning", "diagnostic severity text mismatch"); + require(toString(sink.events()[0].category) == "decode", "diagnostic category text mismatch"); + require(sink.fatalCount() == 0U, "warnings must not be fatal"); + + sink.report({ + DiagnosticSink::Severity::Error, + DiagnosticSink::Category::Input, + "generator-error", + "generator could not create an irrelevant register", + {}, + std::nullopt, + DiagnosticSink::Impact::NonFatal, + }); + require(sink.events().back().impact == DiagnosticSink::Impact::NonFatal && sink.fatalCount() == 0U, + "non-fatal errors must retain error severity without failing the job"); + + sink.report({ + DiagnosticSink::Severity::Error, + DiagnosticSink::Category::Input, + "required-input-missing", + "required input is missing", + }); + require(sink.events().back().impact == DiagnosticSink::Impact::Fatal && sink.fatalCount() == 1U, + "fatal errors must fail the job"); + + sink.clear(); + require(sink.events().empty(), "diagnostic clear failed"); +} + +TEST(CtraceUnitTests, testTraceIssueReporterReportsEveryIssue) +{ + CollectingDiagnosticSink payloadIndependentDiagnostics; + TraceIssueReporter payloadIndependentReporter(payloadIndependentDiagnostics); + TraceEvent payloadIndependentError = issuePacket("opencsd-incomplete-tail"); + payloadIndependentReporter.append(payloadIndependentError); + require(payloadIndependentDiagnostics.events().size() == 1U, + "decoder errors must remain visible independently of payload filtering"); + require(payloadIndependentDiagnostics.fatalCount() == 1U, + "decoder errors must fail validation independently of payload filtering"); + + CollectingDiagnosticSink diagnostics; + TraceIssueReporter reporter(diagnostics); + + reporter.append(overflowPacket(10)); + reporter.append(overflowPacket(20)); + reporter.finish(); + reporter.finish(); + + TraceEvent dataLoss = issuePacket("data-loss", "trace data was lost"); + dataLoss.index = 12U; + std::get(dataLoss.payload).rawBytesConsumed = 3U; + reporter.append(dataLoss); + reporter.append(dataLoss); + + TraceEvent warning = issuePacket("opencsd-warning", "decoder warning", TraceIssueSeverity::Warning); + reporter.append(warning); + + TraceEvent initializationError = issuePacket("opencsd-initialization-error", "decoder setup failed"); + reporter.append(initializationError); + + require(diagnostics.events().size() == 5U, "TraceIssueReporter should report every error and warning occurrence"); + require(diagnostics.events()[0].code == "overflow", "TraceIssueReporter overflow code mismatch"); + require(diagnostics.events()[0].severity == DiagnosticSink::Severity::Warning, + "TraceIssueReporter overflow severity mismatch"); + require(diagnostics.events()[1].code == "data-loss", "TraceIssueReporter data-loss code mismatch"); + require(diagnostics.events()[1].severity == DiagnosticSink::Severity::Error, + "TraceIssueReporter data-loss severity mismatch"); + require(diagnostics.events()[1].context.empty(), "TraceIssueReporter context mismatch"); + require(diagnostics.events()[1].compactMessage.has_value() && + diagnostics.events()[1].compactMessage->find("3 raw bytes") != std::string::npos, + "TraceIssueReporter should provide a compact stderr message"); + require(diagnostics.events()[2].code == "data-loss", "TraceIssueReporter repeated data-loss missing"); + require(diagnostics.events()[3].severity == DiagnosticSink::Severity::Warning, + "TraceIssueReporter should preserve warning severity"); + require(diagnostics.events()[4].context.empty(), "TraceIssueReporter context mismatch"); + require(diagnostics.fatalCount() == 3U, "TraceIssueReporter should classify decoder errors as fatal"); +} diff --git a/tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp b/tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp new file mode 100644 index 000000000..dc024e025 --- /dev/null +++ b/tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.hpp" +#include +#include "TraceEvent.hpp" +#include "TraceSelection.hpp" +#include "csv/CsvRowMapper.hpp" + +TEST(CtraceUnitTests, testTraceSelection) +{ + TraceEvent itm = softwarePacket(1U); + itm.traceBusId = 1; + require(traceEventSelectedForOutput(itm, TraceSelection{{"itm"}, {}}), "TraceSelection ITM type mismatch"); + require(!traceEventSelectedForOutput(itm, TraceSelection{{"dwt"}, {}}), + "TraceSelection should reject unrelated DWT type"); + require(traceEventSelectedForOutput(itm, TraceSelection{{}, {1U, 2U}}), "TraceSelection stream-set mismatch"); + require(!traceEventSelectedForOutput(itm, TraceSelection{{}, {2U, 3U}}), "TraceSelection should reject other stream"); + require(traceEventSelectedForOutput(itm, TraceSelection{{"itm"}, {1U}}), "TraceSelection combined criteria mismatch"); + require(!traceEventSelectedForOutput(itm, TraceSelection{{"itm"}, {2U}}), "TraceSelection combined stream mismatch"); + + itm.traceBusId = 0U; + require(traceEventSelectedForOutput(itm, TraceSelection{{"itm"}, {}}), + "TraceSelection type selector must retain Trace Bus ID 0 input"); + require(!traceEventSelectedForOutput(itm, TraceSelection{{}, {1U}}), + "a non-zero stream selector must not match Trace Bus ID 0 input"); + require(traceEventSelectedForOutput(itm, TraceSelection{{}, {0U}}), + "stream selector 0 must match unformatted single-source input"); + require(CsvRowMapper::row(itm) == ",0,itm,1,0x00,,,", "CSV must write Trace Bus ID 0 for unformatted input"); + + std::get(itm.payload).channel = 0; + require(!traceEventSelectedForOutput(itm, TraceSelection{}), + "TraceSelection must exclude software channel zero without selectors"); + require(!traceEventSelectedForOutput(itm, TraceSelection{{"itm"}, {1U}}), + "TraceSelection selectors must not enable software channel zero"); + + TraceEvent softwareError = issuePacket("decode-error"); + require(traceEventSelectedForOutput(softwareError, TraceSelection{}), "TraceSelection must retain decoder errors"); + + TraceEvent dwt{DwtDataTraceEvent{2U}}; + require(traceEventSelectedForOutput(dwt, TraceSelection{{"dwt"}, {}}), "TraceSelection DWT type mismatch"); + require(traceEventSelectedForOutput(dwt, TraceSelection{{"itm", "dwt"}, {}}), "TraceSelection type OR mismatch"); + + TraceEvent exception{ExceptionTraceEvent{11U, ExceptionAction::Entered}}; + require(traceEventSelectedForOutput(exception, TraceSelection{{"exception"}, {}}), + "TraceSelection exception type mismatch"); +} diff --git a/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp b/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp new file mode 100644 index 000000000..f1bcc8117 --- /dev/null +++ b/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.hpp" +#include +#include "CliOptions.hpp" +#include "CtraceRunMeta.hpp" +#include "OutputRequirements.hpp" +#include "TraceRunConfig.hpp" +#include +#include +#include +#include +#include +#include +#include + +static TraceOutputRequest traceOutputRequest(const CliOptions& options) +{ + return { + options.outputFormat == OutputFormat::Csv || options.outputFormat == OutputFormat::All, + options.outputFormat == OutputFormat::Ctf || options.outputFormat == OutputFormat::All, + options.selection, + }; +} + +TEST(CtraceUnitTests, testTimestampPrescalerAndBackendRequirements) +{ + TraceRunConfig traceRun; + traceRun.path = "Board.ctrace-run.yml"; + TraceRunSetup setup; + setup.timestamps = TraceRunTimestampSetup{280000000U, 16U}; + traceRun.setups.push_back(setup); + const auto ctraceRunMeta = CtraceRunMeta::fromConfig(traceRun); + + require(ctraceRunMeta.timestampPrescaler() == std::optional(16U), "trace-run prescaler mismatch"); + + TraceRunConfig multicore; + multicore.path = "Multicore.ctrace-run.yml"; + TraceRunSetup core0; + core0.processorName = "core0"; + core0.timestamps = TraceRunTimestampSetup{400000000U, 1U}; + TraceRunSetup core1; + core1.processorName = "core1"; + core1.timestamps = TraceRunTimestampSetup{400000000U, 4U}; + multicore.setups = {core0, core1}; + + TraceRunReference core0Itm; + core0Itm.ctraceRef = "opaque/core0-route"; + core0Itm.type = "itm"; + core0Itm.processorName = "core0"; + core0Itm.stream = 1U; + core0Itm.sources = {1U}; + TraceRunReference core1Itm; + core1Itm.ctraceRef = "opaque/core1-route"; + core1Itm.type = "itm"; + core1Itm.processorName = "core1"; + core1Itm.stream = 2U; + core1Itm.sources = {1U}; + multicore.references = {core0Itm, core1Itm}; + + const auto distinctPrescalerMeta = CtraceRunMeta::fromConfig(multicore); + require(!distinctPrescalerMeta.timestampPrescaler().has_value() && + distinctPrescalerMeta.hasDistinctProcessorPrescalers(), + "different processor prescalers must not be collapsed into an unformatted value"); + require(distinctPrescalerMeta.timestampPrescalersByTraceBusId() == + std::map({{1U, 1U}, {2U, 4U}}), + "processor prescalers must remain associated with their ATB streams"); + + core1.timestamps = TraceRunTimestampSetup{400000000U, std::nullopt}; + multicore.setups = {core0, core1}; + const auto missingPrescalerMeta = CtraceRunMeta::fromConfig(multicore); + require(missingPrescalerMeta.timestampPrescaler() == + std::optional(TraceRunSchema::kDefaultTimestampPrescaler), + "an omitted processor prescaler must resolve to the specified default"); + require(missingPrescalerMeta.timestampPrescalersByTraceBusId() == + std::map({{1U, 1U}, {2U, 1U}}), + "the default prescaler must be retained per ATB stream"); + + core0.timestamps = TraceRunTimestampSetup{400000000U, 4U}; + multicore.setups = {core0, core1}; + const auto mixedMissingPrescalerMeta = CtraceRunMeta::fromConfig(multicore); + require(mixedMissingPrescalerMeta.hasDistinctProcessorPrescalers(), + "an explicit prescaler must be compared with another processor's default"); + require(mixedMissingPrescalerMeta.timestampPrescalersByTraceBusId() == + std::map({{1U, 4U}, {2U, 1U}}), + "different explicit and default prescalers must remain stream-specific"); + + core1.timestamps = TraceRunTimestampSetup{200000000U, 1U}; + multicore.setups = {core0, core1}; + const auto distinctClockMeta = CtraceRunMeta::fromConfig(multicore); + require(distinctClockMeta.timestampsByTraceBusId().at(1U).clockHz == std::optional(400000000U) && + distinctClockMeta.timestampsByTraceBusId().at(2U).clockHz == std::optional(200000000U), + "processor clocks must remain associated with their Trace Bus IDs"); + + CliOptions ctfOptions; + ctfOptions.outputFormat = OutputFormat::Ctf; + CollectingDiagnosticSink allClockDiagnostics; + const auto allClockPlan = + planTraceOutputs(traceOutputRequest(ctfOptions), std::filesystem::path("captures/Multicore.SWO.raw"), + distinctClockMeta, allClockDiagnostics); + require(!allClockPlan.ctf.has_value() && !allClockDiagnostics.events().empty() && + allClockDiagnostics.events()[0].code == "ctf-timestamp-clock-ambiguous", + "CTF must reject selected Trace Bus IDs with different clocks"); + + ctfOptions.selection.streams = {2U}; + CollectingDiagnosticSink selectedClockDiagnostics; + const auto selectedClockPlan = + planTraceOutputs(traceOutputRequest(ctfOptions), std::filesystem::path("captures/Multicore.SWO.raw"), + distinctClockMeta, selectedClockDiagnostics); + require(selectedClockPlan.ctf.has_value() && selectedClockPlan.ctf->coreClockHz == 200000000U && + selectedClockDiagnostics.events().empty(), + "a selected Trace Bus ID must use its processor's clock"); +} + +TEST(CtraceUnitTests, testDwtDataMetadataDefaultsAndValidation) +{ + TraceRunConfig config; + config.path = "DwtSize.ctrace-run.yml"; + TraceRunSetup setup; + setup.timestamps = TraceRunTimestampSetup{100000000U, 1U}; + setup.data = { + TraceRunDataSetup{}, + TraceRunDataSetup{"unsigned int", 1U}, + TraceRunDataSetup{"signed int", 2U}, + }; + config.setups.push_back(std::move(setup)); + for (std::uint32_t comparator = 0U; comparator < 3U; ++comparator) { + TraceRunReference reference; + reference.ctraceRef = "opaque/dwt-" + std::to_string(comparator); + reference.type = "dwt"; + reference.sources = {comparator}; + reference.dataSetupIndex = comparator; + config.references.push_back(std::move(reference)); + } + const auto meta = CtraceRunMeta::fromConfig(config); + + CliOptions allOptions; + allOptions.outputFormat = OutputFormat::All; + CollectingDiagnosticSink configurationDiagnostics; + const auto outputPlan = planTraceOutputs(traceOutputRequest(allOptions), std::filesystem::path("DwtSize.SWO.raw"), + meta, configurationDiagnostics); + require(outputPlan.csv.has_value() && outputPlan.ctf.has_value(), + "valid or missing DWT metadata must not disable CSV or CTF"); + require(configurationDiagnostics.events().empty(), "valid or missing DWT metadata must not produce diagnostics"); + require(outputPlan.ctf->sources.size() == 3U && outputPlan.ctf->sources[0].valueType == "unsigned int" && + outputPlan.ctf->sources[0].valueSize == 4U && outputPlan.ctf->sources[1].valueType == "unsigned int" && + outputPlan.ctf->sources[1].valueSize == 1U && outputPlan.ctf->sources[2].valueType == "signed int" && + outputPlan.ctf->sources[2].valueSize == 2U, + "CTF data.symbol-type/data.symbol-size defaults or explicit values mismatch"); + + config.setups[0].data[1].symbolSize = 0U; + CollectingDiagnosticSink invalidSizeDiagnostics; + const auto invalidSizePlan = + planTraceOutputs(traceOutputRequest(allOptions), std::filesystem::path("DwtSize.SWO.raw"), + CtraceRunMeta::fromConfig(config), invalidSizeDiagnostics); + require(invalidSizePlan.csv.has_value() && !invalidSizePlan.ctf.has_value(), + "invalid data.symbol-size must disable only CTF"); + require(invalidSizeDiagnostics.events().size() == 1U && + invalidSizeDiagnostics.events()[0].code == "ctf-dwt-symbol-size-invalid", + "invalid CTF data.symbol-size diagnostic mismatch"); +} + +TEST(CtraceUnitTests, testOutputRequirementsAreBackendSpecific) +{ + TraceRunConfig config; + config.path = "BackendRequirements.ctrace-run.yml"; + TraceRunSetup setup; + setup.timestamps = TraceRunTimestampSetup{400000000U, 1U}; + setup.data.push_back(TraceRunDataSetup{"double", 4U}); + config.setups.push_back(setup); + TraceRunReference reference; + reference.ctraceRef = "opaque/dwt"; + reference.type = "dwt"; + reference.sources = {0U}; + reference.dataSetupIndex = 0U; + config.references.push_back(reference); + + CliOptions csvOptions; + csvOptions.outputFormat = OutputFormat::Csv; + CollectingDiagnosticSink invalidTypeCsvDiagnostics; + const auto invalidTypeCsv = + planTraceOutputs(traceOutputRequest(csvOptions), std::filesystem::path("BackendRequirements.SWO.raw"), + CtraceRunMeta::fromConfig(config), invalidTypeCsvDiagnostics); + require(invalidTypeCsv.csv.has_value() && !invalidTypeCsv.ctf.has_value(), + "an invalid explicit data.symbol-type must not disable CSV"); + require(invalidTypeCsvDiagnostics.events().empty(), "CSV must not inspect CTF-only data.symbol-type metadata"); + + CliOptions allOptions; + allOptions.outputFormat = OutputFormat::All; + CollectingDiagnosticSink invalidTypeAllDiagnostics; + const auto invalidTypeAll = + planTraceOutputs(traceOutputRequest(allOptions), std::filesystem::path("BackendRequirements.SWO.raw"), + CtraceRunMeta::fromConfig(config), invalidTypeAllDiagnostics); + require(invalidTypeAll.csv.has_value() && !invalidTypeAll.ctf.has_value(), + "an invalid explicit data.symbol-type must disable only CTF for --all"); + require(invalidTypeAllDiagnostics.events().size() == 1U && + invalidTypeAllDiagnostics.events()[0].code == "ctf-dwt-symbol-type-invalid", + "invalid CTF data.symbol-type diagnostic mismatch"); + + config.setups[0].data[0].symbolType.reset(); + config.setups[0].data[0].symbolSize.reset(); + CollectingDiagnosticSink missingTypeDiagnostics; + const auto missingType = + planTraceOutputs(traceOutputRequest(allOptions), std::filesystem::path("BackendRequirements.SWO.raw"), + CtraceRunMeta::fromConfig(config), missingTypeDiagnostics); + require(missingType.csv.has_value() && missingType.ctf.has_value(), + "missing data.symbol-type/data.symbol-size must use CTF defaults and leave CSV enabled"); + require(missingType.csv->outputPath == std::filesystem::path("BackendRequirements.SWO.csv") && + missingType.ctf->outputDirectory == std::filesystem::path("BackendRequirements.ctf") && + missingType.ctf->traceCompassXmlPath == + std::filesystem::path("BackendRequirements.SWO.traceanalysis.xml") && + missingType.ctf->coreClockHz == 400000000U && missingType.ctf->sources.size() == 1U && + missingType.ctf->sources[0].valueType == "unsigned int" && missingType.ctf->sources[0].valueSize == 4U, + "output preflight must resolve artifact paths, clock, routes, and defaults"); + require(missingTypeDiagnostics.events().empty(), + "missing optional data.symbol-type/data.symbol-size must not produce diagnostics"); + + config.setups[0].data[0].symbolTypeError = "data.symbol-type must be scalar"; + CollectingDiagnosticSink malformedTypeDiagnostics; + const auto malformedType = + planTraceOutputs(traceOutputRequest(allOptions), std::filesystem::path("BackendRequirements.SWO.raw"), + CtraceRunMeta::fromConfig(config), malformedTypeDiagnostics); + require(malformedType.csv.has_value() && !malformedType.ctf.has_value(), + "malformed data.symbol-type must disable only CTF"); + + config.setups[0].data[0].symbolTypeError.reset(); + config.setups[0].data[0].symbolSizeError = "data.symbol-size must be unsigned"; + CollectingDiagnosticSink malformedSizeDiagnostics; + const auto malformedSize = + planTraceOutputs(traceOutputRequest(allOptions), std::filesystem::path("BackendRequirements.SWO.raw"), + CtraceRunMeta::fromConfig(config), malformedSizeDiagnostics); + require(malformedSize.csv.has_value() && !malformedSize.ctf.has_value() && + malformedSizeDiagnostics.events().size() == 1U && + malformedSizeDiagnostics.events()[0].code == "ctf-dwt-symbol-size-invalid", + "malformed data.symbol-size must disable only CTF"); + + config.setups[0].data[0].symbolSizeError.reset(); + config.setups[0].timestamps->clockHz.reset(); + CollectingDiagnosticSink missingClockDiagnostics; + const auto missingClock = + planTraceOutputs(traceOutputRequest(allOptions), std::filesystem::path("BackendRequirements.SWO.raw"), + CtraceRunMeta::fromConfig(config), missingClockDiagnostics); + require(missingClock.csv.has_value() && !missingClock.ctf.has_value(), + "missing timestamps.clock must disable only CTF"); + + config.setups[0].timestamps->clockError = "timestamps.clock must be unsigned"; + CollectingDiagnosticSink malformedClockDiagnostics; + const auto malformedClock = + planTraceOutputs(traceOutputRequest(allOptions), std::filesystem::path("BackendRequirements.SWO.raw"), + CtraceRunMeta::fromConfig(config), malformedClockDiagnostics); + require(malformedClock.csv.has_value() && !malformedClock.ctf.has_value(), + "malformed timestamps.clock must disable only CTF"); + + config.setups[0].timestamps->clockError.reset(); + config.setups[0].timestamps->clockHz = 0U; + CollectingDiagnosticSink zeroClockDiagnostics; + const auto zeroClock = + planTraceOutputs(traceOutputRequest(allOptions), std::filesystem::path("BackendRequirements.SWO.raw"), + CtraceRunMeta::fromConfig(config), zeroClockDiagnostics); + require(zeroClock.csv.has_value() && !zeroClock.ctf.has_value(), "zero timestamps.clock must disable only CTF"); + + config.setups[0].timestamps->clockHz = 400000000U; + allOptions.selection.types = {"itm"}; + CollectingDiagnosticSink filteredDiagnostics; + const auto filtered = + planTraceOutputs(traceOutputRequest(allOptions), std::filesystem::path("BackendRequirements.SWO.raw"), + CtraceRunMeta::fromConfig(config), filteredDiagnostics); + require(filtered.csv.has_value() && filtered.ctf.has_value(), + "filtered-out DWT type metadata must not affect either backend"); + require(filteredDiagnostics.events().empty(), "filtered-out invalid DWT metadata must not produce diagnostics"); + + CliOptions checkOnlyOptions; + CollectingDiagnosticSink checkOnlyDiagnostics; + const auto checkOnly = + planTraceOutputs(traceOutputRequest(checkOnlyOptions), std::filesystem::path("BackendRequirements.SWO.raw"), + CtraceRunMeta::fromConfig(config), checkOnlyDiagnostics); + require(!checkOnly.hasRequestedOutputs() && checkOnlyDiagnostics.events().empty(), + "check-only mode must not apply CSV or CTF metadata requirements"); +} + +TEST(CtraceUnitTests, testOutputPreflightRejectsAmbiguousRoutesForCtfOnly) +{ + TraceRunConfig config; + config.path = "AmbiguousRoutes.ctrace-run.yml"; + TraceRunSetup setup; + setup.timestamps = TraceRunTimestampSetup{400000000U, 1U}; + setup.data.push_back(TraceRunDataSetup{"unsigned int", 4U}); + config.setups.push_back(setup); + + TraceRunReference first; + first.ctraceRef = "opaque/dwt-a"; + first.type = "dwt"; + first.stream = 1U; + first.sources = {0U}; + first.dataSetupIndex = 0U; + first.label = "core-one"; + TraceRunReference second = first; + second.label = "core-two"; + config.references = {first, second}; + + CliOptions allOptions; + allOptions.outputFormat = OutputFormat::All; + CollectingDiagnosticSink diagnostics; + const auto plan = + planTraceOutputs(traceOutputRequest(allOptions), std::filesystem::path("captures/AmbiguousRoutes.SWO.raw"), + CtraceRunMeta::fromConfig(config), diagnostics); + require(plan.csv.has_value() && !plan.ctf.has_value(), + "conflicting CTF route labels must not disable independent CSV output"); + require(diagnostics.events().size() == 1U && diagnostics.events()[0].code == "ctf-trace-route-ambiguous", + "CTF route ambiguity must be diagnosed during preflight"); + + config.references[1].stream = 2U; + config.references[1].label = "core-one"; + CollectingDiagnosticSink routeDiagnostics; + const auto routePlan = + planTraceOutputs(traceOutputRequest(allOptions), std::filesystem::path("captures/AmbiguousRoutes.SWO.raw"), + CtraceRunMeta::fromConfig(config), routeDiagnostics); + require(routePlan.csv.has_value() && routePlan.ctf.has_value() && routePlan.ctf->sources.size() == 2U, + "CTF must retain equivalent routes with distinct Trace Bus IDs"); + require(routeDiagnostics.events().empty(), "equivalent CTF metadata on distinct Trace Bus IDs must not be ambiguous"); + + TraceRunConfig processorConfig; + processorConfig.path = "AmbiguousProcessors.ctrace-run.yml"; + auto core0Setup = setup; + core0Setup.processorName = "core0"; + auto core1Setup = setup; + core1Setup.processorName = "core1"; + processorConfig.setups = {core0Setup, core1Setup}; + auto core0Reference = first; + core0Reference.processorName = "core0"; + auto core1Reference = first; + core1Reference.processorName = "core1"; + processorConfig.references = {core0Reference, core1Reference}; + CliOptions processorCsvOptions; + processorCsvOptions.outputFormat = OutputFormat::Csv; + CollectingDiagnosticSink processorDiagnostics; + const auto processorPlan = planTraceOutputs(traceOutputRequest(processorCsvOptions), + std::filesystem::path("captures/AmbiguousProcessors.SWO.raw"), + CtraceRunMeta::fromConfig(processorConfig), processorDiagnostics); + require(processorPlan.csv.has_value() && processorDiagnostics.events().empty(), + "CSV must preserve raw stream/source values without consuming processor metadata"); + + allOptions.selection.streams = {1U}; + CollectingDiagnosticSink selectedDiagnostics; + const auto selectedPlan = + planTraceOutputs(traceOutputRequest(allOptions), std::filesystem::path("captures/AmbiguousRoutes.SWO.raw"), + CtraceRunMeta::fromConfig(config), selectedDiagnostics); + require(selectedPlan.csv.has_value() && selectedPlan.ctf.has_value() && selectedPlan.ctf->sources.size() == 1U && + selectedPlan.ctf->sources[0].label == std::optional("core-one"), + "an explicit stream selection must produce one resolved CTF route"); + require(selectedDiagnostics.events().empty(), "an unambiguous selected route must not produce preflight diagnostics"); + + config.setups[0].data.push_back(TraceRunDataSetup{"unsigned int", 2U}); + config.references[1].stream = 1U; + config.references[1].ctraceRef = "opaque/dwt-b"; + config.references[1].dataSetupIndex = 1U; + CliOptions csvOptions; + csvOptions.outputFormat = OutputFormat::Csv; + CollectingDiagnosticSink sizeDiagnostics; + const auto csvPlan = + planTraceOutputs(traceOutputRequest(csvOptions), std::filesystem::path("captures/AmbiguousRoutes.SWO.raw"), + CtraceRunMeta::fromConfig(config), sizeDiagnostics); + require(csvPlan.csv.has_value(), "CTF-only data.symbol-size metadata must not disable CSV"); + require(sizeDiagnostics.events().empty(), "CSV must not inspect CTF-only data.symbol-size metadata"); +} diff --git a/tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp b/tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp new file mode 100644 index 000000000..6eeeae13f --- /dev/null +++ b/tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestPath.hpp" +#include "TestSupport.hpp" +#include +#include "csv/CsvFileOutput.hpp" +#include "TraceOutput.hpp" +#include "TraceOutputLifecycle.hpp" +#include +#include +#include +#include +#include + +class FailingStopOutput final : public TraceOutput { +public: + void stop() override + { + throw std::runtime_error("intentional stop failure"); + } + + void abort() override + { + aborted = true; + } + + void writeEvent(const TraceEvent& packet) override + { + (void)packet; + } + + bool aborted = false; +}; + +class FailingStartOutput final : public TraceOutput { +public: + void start() override + { + throw std::runtime_error("intentional start failure"); + } + + void abort() override + { + aborted = true; + } + + void writeEvent(const TraceEvent& packet) override + { + (void)packet; + } + + bool aborted = false; +}; + +class FailingAbortOutput final : public TraceOutput { +public: + void abort() override + { + throw std::runtime_error("intentional abort cleanup failure"); + } + + void writeEvent(const TraceEvent&) override {} +}; + +TEST(CtraceUnitTests, testTraceOutputLifecycleCompletesIndependentOutputs) +{ + const TemporaryTestPath temporaryPath("ctrace-output-lifecycle-test.csv"); + const auto& path = temporaryPath.path(); + writeTestFile(path, "old-output\n"); + + std::vector> outputs; + outputs.push_back(std::make_unique()); + auto csv = std::make_unique(path); + outputs.push_back(std::move(csv)); + outputs.push_back(std::make_unique()); + + CollectingDiagnosticSink diagnostics; + TraceOutputLifecycle lifecycle(std::move(outputs), diagnostics); + TraceEvent packet = softwarePacket(1U, 1U, 'A'); + lifecycle.append(packet); + lifecycle.finish(); + require(diagnostics.fatalCount() == 2U, "output lifecycle should report start and finalization failures"); + + const auto contents = readTestTextFile(path); + require(contents.find("cycles,stream,type,source,value,pc,offset,note\n") == 0U && + contents.find(",0,itm,1,0x41,,,\n") != std::string::npos, + "output lifecycle should complete successful outputs despite another output failure"); +} + +TEST(CtraceUnitTests, testTraceOutputLifecycleReportsAbortFailures) +{ + std::vector> outputs; + outputs.push_back(std::make_unique()); + CollectingDiagnosticSink diagnostics; + TraceOutputLifecycle lifecycle(std::move(outputs), diagnostics); + lifecycle.abort(); + + bool reportsAbortPhase = false; + for (const auto& event : diagnostics.events()) { + for (const auto& [key, value] : event.context) { + if (key == "phase" && value == "abort") { + reportsAbortPhase = true; + } + } + } + require(diagnostics.fatalCount() == 1U && reportsAbortPhase, + "output lifecycle must report a failed direct-output cleanup"); +} diff --git a/tools/ctrace/test/unit/src/output/csv/CsvFileOutputCriteriaTests.cpp b/tools/ctrace/test/unit/src/output/csv/CsvFileOutputCriteriaTests.cpp new file mode 100644 index 000000000..021960be1 --- /dev/null +++ b/tools/ctrace/test/unit/src/output/csv/CsvFileOutputCriteriaTests.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestPath.hpp" +#include "TestSupport.hpp" +#include +#include "csv/CsvFileOutput.hpp" +#include "TraceSelection.hpp" +#include + +TEST(CtraceUnitTests, testCsvFileOutputCriteria) +{ + const TemporaryTestPath outputPath("ctrace-filtered-output.csv"); + const auto& path = outputPath.path(); + CsvFileOutput output(path, TraceSelection{{"itm"}, {1U, 2U}}); + output.start(); + + TraceEvent accepted = softwarePacket(1U, 1U, 0x41U); + accepted.traceBusId = 2; + output.writeEvent(accepted); + + TraceEvent wrongStream = accepted; + wrongStream.traceBusId = 3; + output.writeEvent(wrongStream); + + TraceEvent unformatted = accepted; + unformatted.traceBusId = 0U; + output.writeEvent(unformatted); + + TraceEvent excludedChannel = accepted; + std::get(excludedChannel.payload).channel = 0; + output.writeEvent(excludedChannel); + + TraceEvent error = issuePacket("decode-error"); + error.traceBusId = accepted.traceBusId; + output.writeEvent(error); + + output.stop(); + const auto contents = readTestTextFile(path); + require(contents == "cycles,stream,type,source,value,pc,offset,note\n,2,itm,1,0x41,,,\n", + "CsvFileOutput criteria mismatch"); + const TemporaryTestPath errorOutputPath("ctrace-filtered-errors.csv"); + const auto& errorPath = errorOutputPath.path(); + CsvFileOutput errorOutput(errorPath, TraceSelection{{"error"}, {}}); + errorOutput.start(); + TraceEvent warning = issuePacket("decode-warning", "decoder warning", TraceIssueSeverity::Warning); + errorOutput.writeEvent(warning); + errorOutput.stop(); + const auto errorContents = readTestTextFile(errorPath); + require(errorContents.find(",0,error,,,,,decoder warning\n") != std::string::npos, + "the error selector must include warning-severity decoder issue packets"); +} diff --git a/tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp b/tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp new file mode 100644 index 000000000..c72ceda7f --- /dev/null +++ b/tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestPath.hpp" +#include "TestSupport.hpp" +#include +#include "TraceEvent.hpp" +#include "csv/CsvFileOutput.hpp" +#include +#include +#include + +TEST(CtraceUnitTests, testCsvFileOutputMatchesSpecification) +{ + const TemporaryTestPath temporaryPath("ctrace-csv-test.csv"); + const auto& csvPath = temporaryPath.path(); + + CsvFileOutput output(csvPath); + output.start(); + + TraceEvent dwt{DwtDataTraceEvent{ + 2U, + 4U, + 0xfffffdf9U, + AccessType::Read, + 0xfdf9U, + 0x08001234U, + }}; + dwt.tcyc = 949338400U; + output.writeEvent(dwt); + + TraceEvent exception{ExceptionTraceEvent{11U, ExceptionAction::Entered}}; + exception.tcyc = 950364820U; + output.writeEvent(exception); + + output.stop(); + + const auto lines = readTestLines(csvPath); + + require(lines.size() == 3U, "CSV specification row count mismatch"); + require(lines[0] == "cycles,stream,type,source,value,pc,offset,note", "CSV specification header mismatch"); + require(lines[1] == "949338400,0,dwt,2,0xfffffdf9,0x08001234,0xfdf9,", "CSV DWT row schema mismatch"); + require(lines[2] == "950364820,0,exception,11,0x1,,,", "CSV exception state schema mismatch"); +} + +TEST(CtraceUnitTests, testCsvFileOutputWritesTraceIssues) +{ + const TemporaryTestPath temporaryPath("ctrace-csv-issue-test.csv"); + const auto& csvPath = temporaryPath.path(); + + CsvFileOutput output(csvPath); + output.start(); + output.writeEvent(overflowPacket(1234)); + + TraceEvent dataLoss = issuePacket("data-loss", "trace data lost before resynchronization"); + dataLoss.tcyc = 1235; + output.writeEvent(dataLoss); + output.stop(); + + const auto lines = readTestLines(csvPath); + + require(lines.size() == 3U, "CSV issue row count mismatch"); + require(lines[0] == "cycles,stream,type,source,value,pc,offset,note", "CSV issue header mismatch"); + require(lines[1] == "1234,0,overflow,,,,,overflow: new timestamp segment; time across boundary may be unreliable", + "CSV overflow issue row mismatch"); + require(lines[2] == "1235,0,error,,,,,trace data lost before resynchronization", "CSV data-loss issue row mismatch"); +} + +TEST(CtraceUnitTests, testCsvFileOutputWritesDirectly) +{ + const TemporaryTestPath temporaryPath("ctrace-csv-direct-test"); + const auto& testRoot = temporaryPath.path(); + const auto csvPath = testRoot / std::filesystem::u8path(u8"Gr\u00f6\u00dfe.csv"); + writeTestFile(csvPath, "old-output\n"); + + CsvFileOutput output(csvPath); + output.start(); + require(std::filesystem::is_regular_file(csvPath), + "CSV start must replace the existing target with the direct output file"); + TraceEvent packet = softwarePacket(1U, 1U, 'A'); + output.writeEvent(packet); + output.abort(); + require(!std::filesystem::exists(csvPath), "CSV abort must remove the incomplete direct output"); + + writeTestFile(csvPath, "stale-output\n"); + output.start(); + output.writeEvent(packet); + output.stop(); + const auto contents = readTestTextFile(csvPath); + require(contents.find("cycles,stream,type") == 0 && contents.find("stale-output") == std::string::npos, + "completed CSV output must directly replace the previous file"); + std::filesystem::remove(csvPath); + + std::filesystem::create_directory(csvPath); + bool rejectedDirectory = false; + try { + CsvFileOutput directoryOutput(csvPath); + directoryOutput.start(); + } catch (const std::runtime_error&) { + rejectedDirectory = true; + } + require(rejectedDirectory && std::filesystem::is_directory(csvPath), + "CSV output must not delete a directory occupying its target path"); + std::filesystem::remove_all(testRoot); + + std::filesystem::create_directories(testRoot / "working"); + std::filesystem::create_directories(testRoot / "captures"); + const auto parentRelativePath = testRoot / "working" / ".." / "captures" / "output.csv"; + CsvFileOutput parentRelativeOutput(parentRelativePath); + parentRelativeOutput.start(); + parentRelativeOutput.writeEvent(packet); + parentRelativeOutput.stop(); + require(std::filesystem::is_regular_file(testRoot / "captures" / "output.csv"), + "CSV output must accept a legitimate parent-relative trace path"); +} diff --git a/tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp b/tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp new file mode 100644 index 000000000..8b4ce6ce1 --- /dev/null +++ b/tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.hpp" +#include +#include "TraceEvent.hpp" +#include "TraceSelection.hpp" +#include "csv/CsvRowMapper.hpp" +#include +#include +#include +#include +#include +#include + +TEST(CtraceUnitTests, testCsvRowMapperAndTraceEventSchema) +{ + const std::array expectedTypes{{ + "itm", + "dwt", + "exception", + "global_ts", + "overflow", + "error", + }}; + require(kTraceEventTypeNames == expectedTypes, "trace event type names mismatch"); + for (const auto type : kTraceEventTypeNames) { + require(parseTraceEventType(type).has_value(), "declared trace event type should parse"); + } + for (const auto* type : {"event", "pmu", "pcsample"}) { + require(!parseTraceEventType(type).has_value(), + std::string(type) + " must not be selectable before a semantic output event exists"); + } + require(!parseTraceEventType("timestamp").has_value(), "undeclared trace event type should be rejected"); + const std::vector>> semanticTypes{ + {TraceEvent{SoftwareTraceEvent{}}, TraceEventType::Itm}, + {TraceEvent{DwtDataTraceEvent{}}, TraceEventType::Dwt}, + {TraceEvent{DwtAddressTraceEvent{0U, DwtPcTraceLocation{0U}}}, TraceEventType::Dwt}, + {TraceEvent{ExceptionTraceEvent{}}, TraceEventType::Exception}, + {TraceEvent{DwtEventTraceEvent{}}, std::nullopt}, + {TraceEvent{PmuTraceEvent{}}, std::nullopt}, + {TraceEvent{LocalTimestampTraceEvent{}}, std::nullopt}, + {TraceEvent{GlobalTimestampTraceEvent{}}, TraceEventType::GlobalTimestamp}, + {TraceEvent{OverflowTraceEvent{}}, TraceEventType::Overflow}, + {TraceEvent{SyncTraceEvent{}}, std::nullopt}, + {TraceEvent{TraceIssueEvent{}}, TraceEventType::Error}, + }; + for (const auto& [event, expectedType] : semanticTypes) { + require(traceEventType(event) == expectedType, "semantic TraceEvent type mapping mismatch"); + } + + require(CsvRowMapper::header() == "cycles,stream,type,source,value,pc,offset,note", + "CSV schema header integration mismatch"); + require(CsvRowMapper::row(TraceEvent{ExceptionTraceEvent{11U, ExceptionAction::Entered}}) == ",0,exception,11,0x1,,,", + "CSV exception value mismatch"); + require(CsvRowMapper::row(TraceEvent{DwtDataTraceEvent{0U, 1U, 0x0aU, AccessType::Write}}) == ",0,dwt,0,0x0a,,,", + "CSV must render the raw hexadecimal DWT value with the one-byte SWO width"); + require(CsvRowMapper::row(TraceEvent{DwtDataTraceEvent{0U, 2U, 0x0aU, AccessType::Write}}) == ",0,dwt,0,0x000a,,,", + "CSV must render the raw hexadecimal DWT value with the two-byte SWO width"); +} diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp new file mode 100644 index 000000000..53892b6d4 --- /dev/null +++ b/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp @@ -0,0 +1,691 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +// CTF bundle metadata, event handling, and direct-output lifecycle tests. +#include "CtfTestSupport.hpp" +#include "TestSupport.hpp" + +#include + +#include "ctf/CtfBundleOutput.hpp" +#include "CtraceRunMeta.hpp" +#include "TestPath.hpp" +#include "TraceEvent.hpp" +#include "TraceOutputConfig.hpp" +#include "TraceRunConfig.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace CtfTestSupport; + +constexpr std::size_t kCtfStatusOrContextPayloadSize = 5U; +constexpr std::size_t kCtfStatusOrContextEventSize = kCtfEventHeaderSize + kCtfStatusOrContextPayloadSize; +constexpr std::uint32_t kCtfDwtValueEventId = 4U; +constexpr std::uint32_t kCtfTraceStatusEventId = 5U; +constexpr std::uint32_t kCtfExceptionEventId = 7U; + +std::filesystem::path testTraceCompassXmlPath(const std::filesystem::path& outputDirectory) +{ + auto basePath = outputDirectory; + if (basePath.extension() == ".ctf") { + basePath.replace_extension(); + } + basePath += ".SWO.traceanalysis.xml"; + return basePath; +} + +CtfOutputConfig makeCtfBundleConfig(const std::filesystem::path& outputDirectory, std::uint64_t coreClockHz) +{ + return CtfOutputConfig(outputDirectory, testTraceCompassXmlPath(outputDirectory), coreClockHz, {}, {}); +} + +class TemporaryCtfOutput { +public: + explicit TemporaryCtfOutput(const std::string& name) : root_(name), outputDirectory_(root_.path() / "output.ctf") {} + + const std::filesystem::path& outputDirectory() const + { + return outputDirectory_; + } + +private: + TemporaryTestPath root_; + std::filesystem::path outputDirectory_; +}; + +std::vector resolvedSources(const CtraceRunMeta& meta) +{ + std::set> keys; + std::vector sources; + for (const auto& route : meta.sources()) { + if ((route.type != "itm" && route.type != "dwt") || (route.type == "itm" && route.source == 0U) || + !keys.emplace(route.type, route.source).second) { + continue; + } + const auto* source = meta.resolveSource(route.type, std::nullopt, route.source); + require(source != nullptr, "test trace route resolution failed"); + sources.push_back({ + source->type, + source->source, + source->traceBusId, + source->label, + source->symbolAddress, + source->valueType, + static_cast(source->valueSize), + }); + } + return sources; +} + +void requireSingleCtfItmEvent(const std::filesystem::path& streamPath, std::uint8_t expectedChannel, + const std::string& message) +{ + const auto bytes = readTestBinaryFile(streamPath); + + constexpr std::size_t itmPayloadSize = 8U; + constexpr auto contentSize = kCtfEventOffset + kCtfEventHeaderSize + itmPayloadSize; + require(bytes.size() >= contentSize, message); + require(readLe32(bytes, kCtfPacketHeaderSize + 4U) == contentSize * 8U, message); + require(readLe32(bytes, kCtfEventOffset) == 0U, message); + require(bytes[kCtfEventOffset + kCtfEventHeaderSize] == expectedChannel, message); +} + +std::vector readCtfExceptionRecords(const std::filesystem::path& streamPath) +{ + const auto bytes = readTestBinaryFile(streamPath); + + constexpr std::size_t exceptionPayloadSize = 5U; + + std::vector records; + for (std::size_t packetStart = 0; packetStart + kCtfEventOffset <= bytes.size(); packetStart += kCtfPacketSize) { + const auto contentBits = readLe32(bytes, packetStart + kCtfPacketHeaderSize + 4U); + const auto contentEnd = packetStart + static_cast(contentBits / 8U); + std::size_t offset = packetStart + kCtfEventOffset; + while (offset + kCtfEventHeaderSize <= contentEnd) { + const auto id = readLe32(bytes, offset); + if (id == kCtfTraceStatusEventId) { + offset += kCtfStatusOrContextEventSize; + continue; + } + if (id == kCtfExceptionEventId) { + const auto number = readLe16(bytes, offset + kCtfEventHeaderSize); + const auto action = bytes[offset + kCtfEventHeaderSize + 2U]; + records.push_back(std::to_string(number) + ":" + std::to_string(static_cast(action))); + offset += kCtfEventHeaderSize + exceptionPayloadSize; + continue; + } + break; + } + } + return records; +} + +std::vector readOnlyCtfTraceStatusReasons(const std::filesystem::path& streamPath) +{ + const auto bytes = readTestBinaryFile(streamPath); + + std::vector reasons; + for (std::size_t packetStart = 0; packetStart + kCtfEventOffset <= bytes.size(); packetStart += kCtfPacketSize) { + const auto contentBits = readLe32(bytes, packetStart + kCtfPacketHeaderSize + 4U); + const auto contentEnd = packetStart + static_cast(contentBits / 8U); + std::size_t offset = packetStart + kCtfEventOffset; + while (offset + kCtfStatusOrContextEventSize <= contentEnd) { + require(readLe32(bytes, offset) == kCtfTraceStatusEventId, "expected only CTF trace-status events"); + reasons.push_back(bytes[offset + kCtfEventHeaderSize]); + offset += kCtfStatusOrContextEventSize; + } + require(offset == contentEnd, "malformed CTF trace-status content size"); + } + return reasons; +} + +std::uint8_t readFirstCtfDwtValueTag(const std::filesystem::path& streamPath) +{ + const auto bytes = readTestBinaryFile(streamPath); + + const auto contentEnd = static_cast(readLe32(bytes, kCtfPacketHeaderSize + 4U) / 8U); + std::size_t offset = kCtfEventOffset; + while (offset + kCtfEventHeaderSize <= contentEnd) { + const auto eventId = readLe32(bytes, offset); + if (eventId == kCtfDwtValueEventId) { + return bytes[offset + kCtfEventHeaderSize + 2U]; + } + require(eventId == kCtfTraceStatusEventId || eventId == kCtfExceptionEventId, + "unexpected CTF event before DWT value"); + offset += kCtfStatusOrContextEventSize; + } + throw std::runtime_error("CTF DWT value event missing"); +} + +} // namespace + +TEST(CtraceUnitTests, testCtfBundleOutputExceptionContext) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-exception-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + + auto options = makeCtfBundleConfig(outputDir, 1000000U); + CtfBundleOutput output(std::move(options)); + output.start(); + output.writeEvent(exceptionPacket(15, ExceptionAction::Entered, 100)); + output.writeEvent(exceptionPacket(54, ExceptionAction::Entered, 200)); + output.writeEvent(exceptionPacket(54, ExceptionAction::Exited, 300)); + output.writeEvent(exceptionPacket(15, ExceptionAction::Returned, 400)); + output.writeEvent(exceptionPacket(15, ExceptionAction::Exited, 500)); + output.stop(); + + const auto records = readCtfExceptionRecords(outputDir / "stream_0"); + const auto metadata = readTestTextFile(outputDir / "metadata"); + + require(metadata.find("\"entered\" = 1") != std::string::npos, "CTF exception entered label mismatch"); + require(metadata.find("\"exited\" = 2") != std::string::npos, "CTF exception exited label mismatch"); + + require(records == std::vector({ + "0:1", + "0:2", + "15:1", + "15:2", + "54:1", + "54:2", + "15:1", + "15:2", + "0:1", + }), + "CtfBundleOutput exception active-context records mismatch"); +} + +TEST(CtraceUnitTests, testCtfBundleOutputUsesCtraceRunMeta) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-ctrace-run-meta-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + + TraceRunConfig traceRun; + traceRun.path = "Board.ctrace-run.yml"; + TraceRunReference signedByteReference; + signedByteReference.ctraceRef = "opaque/signed-byte"; + signedByteReference.type = "dwt"; + signedByteReference.sources = {0U}; + signedByteReference.dataSetupIndex = 0U; + signedByteReference.label = "Sine"; + signedByteReference.stream = 7U; + traceRun.references.push_back(signedByteReference); + + TraceRunReference reference; + reference.ctraceRef = "opaque/current"; + reference.type = "dwt"; + reference.sources = {2U}; + reference.dataSetupIndex = 2U; + reference.label = "Current\n\t\"\\\x01"; + reference.symbolAddress = 0x24000e88U; + traceRun.references.push_back(reference); + TraceRunSetup setup; + setup.data.resize(3U); + setup.data[0].symbolType = "signed int"; + setup.data[0].symbolSize = 1U; + setup.data[2].symbolType = "signed int"; + setup.data[2].symbolSize = 4U; + traceRun.setups.push_back(std::move(setup)); + + auto options = makeCtfBundleConfig(outputDir, 280000000U); + options.sources = resolvedSources(CtraceRunMeta::fromConfig(traceRun)); + require(!options.sources.empty(), "resolved CTF source missing"); + require(options.sources.front().traceBusId == 7U, "resolved CTF source must retain its Trace Bus ID"); + require(options.sources.front().valueType == "signed int", "resolved CTF source must retain its value type"); + CtfBundleOutput output(std::move(options)); + output.start(); + TraceEvent signedByte{DwtDataTraceEvent{ + 0U, + 1U, + 0xffU, + AccessType::Write, + }}; + signedByte.tcyc = 100U; + signedByte.traceBusId = 7U; + output.writeEvent(signedByte); + output.stop(); + + const auto metadata = readTestTextFile(outputDir / "metadata"); + const auto stream = readTestBinaryFile(outputDir / "stream_0"); + + require(metadata.find("freq = 280000000") != std::string::npos, "CTF trace-run clock mismatch"); + require(metadata.find("cmsis_dwt0_value_type = \"signed int\"") != std::string::npos, + "CTF signed-byte source type mismatch"); + require(metadata.find("cmsis_dwt2_value_type = \"signed int\"") != std::string::npos, "CTF trace-run type mismatch"); + require(metadata.find("cmsis_dwt2_address_start = \"0x24000E88\"") != std::string::npos, + "CTF trace-run start address mismatch"); + require(metadata.find("cmsis_dwt2_address_end = \"0x24000E8B\"") != std::string::npos, + "CTF trace-run end address mismatch"); + require(metadata.find("\"Current\\n\\t\\\"\\\\\\x01\" = 2") != std::string::npos, + "CTF trace-run label escaping mismatch"); + + auto dwtEventOffset = kCtfEventOffset; + require(readLe32(stream, kCtfEventOffset) == kCtfTraceStatusEventId, "CTF trace-start event missing"); + while (dwtEventOffset + kCtfEventHeaderSize <= stream.size() && + readLe32(stream, dwtEventOffset) != kCtfDwtValueEventId) { + const auto eventId = readLe32(stream, dwtEventOffset); + require(eventId == kCtfTraceStatusEventId || eventId == kCtfExceptionEventId, + "unexpected CTF event before DWT value"); + dwtEventOffset += kCtfStatusOrContextEventSize; + } + require(readLe32(stream, dwtEventOffset) == kCtfDwtValueEventId, "CTF DWT value event missing"); + require(stream[dwtEventOffset + 12U] == 7U, "CTF event context must preserve the CoreSight Trace Bus ID"); + require(stream[dwtEventOffset + kCtfEventHeaderSize + 2U] == 1U, + "CTF one-byte int payload must select the i8 variant"); + require(stream[dwtEventOffset + kCtfEventHeaderSize + 3U] == 0xffU, "CTF signed-byte payload mismatch"); +} + +TEST(CtraceUnitTests, testCtfBundleOutputDefaultsDwtValueType) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-default-dwt-type-test"); + const auto& root = temporaryPath.path(); + + TraceRunConfig defaultTraceRun; + TraceRunReference defaultReference; + defaultReference.ctraceRef = "opaque/default-dwt"; + defaultReference.type = "dwt"; + defaultReference.sources = {0U}; + defaultReference.dataSetupIndex = 0U; + defaultTraceRun.references.push_back(defaultReference); + const auto defaultMeta = CtraceRunMeta::fromConfig(defaultTraceRun); + const auto* defaultSource = defaultMeta.resolveSource("dwt", std::nullopt, 0U); + require(defaultSource != nullptr && defaultSource->valueType == "unsigned int" && defaultSource->valueSize == 4U, + "missing DWT data.symbol-type/data.symbol-size must default to unsigned int/4"); + + const auto defaultOutputDir = root / "default"; + auto defaultOptions = makeCtfBundleConfig(defaultOutputDir, 1000000U); + defaultOptions.sources = resolvedSources(defaultMeta); + CollectingDiagnosticSink diagnostics; + CtfBundleOutput defaultOutput(std::move(defaultOptions), &diagnostics); + defaultOutput.start(); + defaultOutput.writeEvent(TraceEvent{DwtDataTraceEvent{0U, 1U, 0xffU, AccessType::Write}}); + defaultOutput.writeEvent(TraceEvent{DwtDataTraceEvent{0U, 2U, 0xffffU, AccessType::Write}}); + defaultOutput.stop(); + require(readFirstCtfDwtValueTag(defaultOutputDir / "stream_0") == 6U, + "default DWT metadata must select the unsigned 32-bit CTF variant"); + require(diagnostics.events().size() == 1U && diagnostics.events()[0].severity == DiagnosticSink::Severity::Warning && + diagnostics.events()[0].code == "dwt-symbol-size-mismatch" && diagnostics.fatalCount() == 0U, + "DWT size mismatch must be reported once per channel"); + + TraceRunConfig signedTraceRun; + TraceRunSetup signedSetup; + signedSetup.data.push_back(TraceRunDataSetup{"signed int", 1U}); + signedTraceRun.setups.push_back(std::move(signedSetup)); + signedTraceRun.references.push_back(defaultReference); + const auto signedOutputDir = root / "signed"; + auto signedOptions = makeCtfBundleConfig(signedOutputDir, 1000000U); + signedOptions.sources = resolvedSources(CtraceRunMeta::fromConfig(signedTraceRun)); + CollectingDiagnosticSink signedDiagnostics; + CtfBundleOutput signedOutput(std::move(signedOptions), &signedDiagnostics); + signedOutput.start(); + signedOutput.writeEvent(TraceEvent{DwtDataTraceEvent{0U, 1U, 0xffU, AccessType::Write}}); + signedOutput.stop(); + require(readFirstCtfDwtValueTag(signedOutputDir / "stream_0") == 1U, + "explicit signed int/1 metadata must select the signed 8-bit CTF variant"); + require(signedDiagnostics.events().empty(), "matching DWT sizes must not produce a warning"); + + TraceRunConfig traceRun; + traceRun.path = "ambiguous-streams.ctrace-run.yml"; + TraceRunSetup setup; + setup.data.push_back(TraceRunDataSetup{"signed int", 4U}); + traceRun.setups.push_back(std::move(setup)); + TraceRunReference first; + first.ctraceRef = "opaque/dwt-route"; + first.type = "dwt"; + first.stream = 1U; + first.sources = {0U}; + first.dataSetupIndex = 0U; + first.label = "core-one"; + TraceRunReference second = first; + second.stream = 2U; + second.label = "core-two"; + traceRun.references = {first, second}; + const auto meta = CtraceRunMeta::fromConfig(traceRun); + const auto* selected = meta.resolveSource("dwt", std::optional(1U), 0U); + require(selected != nullptr && selected->label == std::optional("core-one"), + "selected stream must resolve its exact DWT metadata"); + + bool rejectedAmbiguous = false; + try { + (void)meta.resolveSource("dwt", std::nullopt, 0U); + } catch (const std::runtime_error& error) { + rejectedAmbiguous = std::string(error.what()).find("ambiguous metadata") != std::string::npos; + } + require(rejectedAmbiguous, "unformatted CTF must reject conflicting metadata from multiple ATB streams"); +} + +TEST(CtraceUnitTests, testCtfWarningsRemainVisibleWithoutResettingContext) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-warning-test"); + const auto& root = temporaryPath.path(); + const auto filteredDir = root / "filtered.ctf"; + const auto contextDir = root / "context.ctf"; + const auto dataLossDir = root / "data-loss.ctf"; + + auto filteredOptions = makeCtfBundleConfig(filteredDir, 1000000U); + filteredOptions.selection.types.push_back("error"); + CtfBundleOutput filtered(std::move(filteredOptions)); + filtered.start(); + TraceEvent warning = issuePacket("opencsd-warning", "decoder warning", TraceIssueSeverity::Warning); + filtered.writeEvent(warning); + filtered.stop(); + require(readOnlyCtfTraceStatusReasons(filteredDir / "stream_0") == std::vector({4U}), + "--type error must retain decoder warnings in CTF"); + + CtfBundleOutput context(makeCtfBundleConfig(contextDir, 1000000U)); + context.start(); + context.writeEvent(exceptionPacket(15U, ExceptionAction::Entered, 10U)); + context.writeEvent(warning); + context.writeEvent(exceptionPacket(54U, ExceptionAction::Entered, 20U)); + context.stop(); + require(readCtfExceptionRecords(contextDir / "stream_0") == + std::vector({"0:1", "0:2", "15:1", "15:2", "54:1"}), + "a decoder warning must not reset the active CTF exception context"); + + auto dataLossOptions = makeCtfBundleConfig(dataLossDir, 1000000U); + dataLossOptions.selection.types.push_back("exception"); + CtfBundleOutput dataLoss(std::move(dataLossOptions)); + dataLoss.start(); + dataLoss.writeEvent(exceptionPacket(15U, ExceptionAction::Entered, 10U)); + dataLoss.writeEvent(issuePacket("data-loss", "decoder data loss")); + dataLoss.writeEvent(exceptionPacket(15U, ExceptionAction::Returned, 20U)); + dataLoss.stop(); + require(readCtfExceptionRecords(dataLossDir / "stream_0") == + std::vector({"0:1", "0:2", "15:1", "15:2", "15:1"}), + "filtered data-loss must still reset the CTF exception context"); +} + +TEST(CtraceUnitTests, testCtfBundleOutputTypeFilterExcludesSyntheticEvents) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-filter-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + + auto options = makeCtfBundleConfig(outputDir, 1000000U); + options.selection.types.push_back("itm"); + CtfBundleOutput output(std::move(options)); + output.start(); + + TraceEvent software = softwarePacket(1U, 1U, 'A'); + software.tcyc = 100; + output.writeEvent(software); + output.writeEvent(exceptionPacket(15, ExceptionAction::Entered, 200)); + output.writeEvent(overflowPacket(300)); + TraceEvent error = issuePacket("opencsd-bad-packet-sequence"); + error.tcyc = 400; + error.quality = TraceQuality{true, false, 1U}; + output.writeEvent(error); + output.stop(); + + requireSingleCtfItmEvent(outputDir / "stream_0", 1U, + "CTF itm filter should exclude synthetic events and errors of other packet types"); +} + +TEST(CtraceUnitTests, testCtfGlobalTimestampDoesNotEstablishLocalTimeQuality) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-global-timestamp-quality-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + + auto options = makeCtfBundleConfig(outputDir, 1000000U); + options.selection.types.push_back("itm"); + CtfBundleOutput output(std::move(options)); + output.start(); + + TraceEvent globalTimestamp{GlobalTimestampTraceEvent{1234U, false}}; + globalTimestamp.tcyc = 42U; + output.writeEvent(globalTimestamp); + output.writeEvent(softwarePacket(1U, 1U, 'A')); + output.stop(); + + const auto stream = readTestBinaryFile(outputDir / "stream_0"); + + constexpr std::size_t qualityFlagsOffset = kCtfEventOffset + kCtfEventHeaderSize + 3U; + constexpr std::uint8_t timestampReliable = 1U << 1U; + constexpr std::uint8_t beforeFirstLocalTimestamp = 1U << 2U; + require(readLe32(stream, kCtfEventOffset) == 0U, "CTF ITM event missing after global timestamp"); + require(stream[qualityFlagsOffset] == (timestampReliable | beforeFirstLocalTimestamp), + "global timestamp must not mark following samples as locally timestamped"); +} + +TEST(CtraceUnitTests, testCtfHoldsRegressingEventTimestamps) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-regressing-timestamp-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + + auto options = makeCtfBundleConfig(outputDir, 1000000U); + options.selection.types.push_back("itm"); + CtfBundleOutput output(std::move(options)); + output.start(); + TraceEvent first = softwarePacket(1U, 1U, 'A'); + first.tcyc = 100U; + output.writeEvent(first); + TraceEvent second = softwarePacket(1U, 1U, 'B'); + second.tcyc = 10U; + output.writeEvent(second); + output.stop(); + + const auto stream = readTestBinaryFile(outputDir / "stream_0"); + + constexpr std::size_t itmEventSize = kCtfEventHeaderSize + 8U; + require(readLe64(stream, kCtfEventOffset + 4U) == 100U, "first CTF event timestamp mismatch"); + require(readLe64(stream, kCtfEventOffset + itmEventSize + 4U) == 100U, "CTF event timestamps must not regress"); +} + +TEST(CtraceUnitTests, testCtfGlobalTimestampEvent) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-global-timestamp-event-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + + auto options = makeCtfBundleConfig(outputDir, 1000000U); + options.selection.types.push_back("global_ts"); + CtfBundleOutput output(std::move(options)); + output.start(); + constexpr std::uint64_t timestampValue = 0x123456789abcdef0ULL; + output.writeEvent(TraceEvent{GlobalTimestampTraceEvent{timestampValue, true}}); + output.stop(); + + const auto stream = readTestBinaryFile(outputDir / "stream_0"); + const auto metadata = readTestTextFile(outputDir / "metadata"); + + constexpr std::size_t payloadSize = 9U; + constexpr std::size_t contentSize = kCtfEventOffset + kCtfEventHeaderSize + payloadSize; + require(stream.size() >= contentSize, "CTF global timestamp event missing"); + require(readLe32(stream, kCtfPacketHeaderSize + 4U) == contentSize * 8U, + "CTF global timestamp filter emitted unrelated events"); + require(readLe32(stream, kCtfEventOffset) == 8U, "CTF global timestamp event ID mismatch"); + require(readLe64(stream, kCtfEventOffset + kCtfEventHeaderSize) == timestampValue, + "CTF global timestamp value mismatch"); + require(stream[kCtfEventOffset + kCtfEventHeaderSize + 8U] == 1U, "CTF global timestamp clock-change flag mismatch"); + require(metadata.find("name = \"GLOBAL_TIMESTAMP\"") != std::string::npos, "CTF global timestamp metadata missing"); +} + +TEST(CtraceUnitTests, testCtfBundleOutputExcludesSoftwareChannelZero) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-channel-zero-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + auto options = makeCtfBundleConfig(outputDir, 1000000U); + TraceRunConfig traceRun; + TraceRunReference channelZero; + channelZero.ctraceRef = "opaque/channel-zero"; + channelZero.type = "itm"; + channelZero.label = "Console"; + channelZero.sources = {0U}; + traceRun.references.push_back(std::move(channelZero)); + options.sources = resolvedSources(CtraceRunMeta::fromConfig(traceRun)); + options.selection.types.push_back("itm"); + options.selection.types.push_back("error"); + CtfBundleOutput output(std::move(options)); + output.start(); + TraceEvent software = softwarePacket(0U, 1U, 'A'); + software.tcyc = 100U; + output.writeEvent(software); + TraceEvent softwareError = issuePacket("opencsd-incomplete-tail"); + softwareError.tcyc = 101U; + output.writeEvent(softwareError); + output.stop(); + require(readOnlyCtfTraceStatusReasons(outputDir / "stream_0") == std::vector({4U}), + "CTF must exclude software channel zero payload but retain its decoder errors"); + require(!std::filesystem::exists(outputDir / ("stream_" + std::to_string(1))), "CTF must not create a second stream"); + + const auto metadata = readTestTextFile(outputDir / "metadata"); + require(metadata.find("ITM" + std::to_string(0)) == std::string::npos, + "CTF metadata must not register software channel zero"); + require(metadata.find("cmsis_itm0_") == std::string::npos && metadata.find("Console") == std::string::npos, + "CTF analysis metadata must not expose configured software channel zero"); +} + +TEST(CtraceUnitTests, testCtfBundleOutputAbortRemovesPartialBundle) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-direct-abort-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + const auto xmlPath = testTraceCompassXmlPath(outputDir); + writeTestFile(outputDir / "old-marker", "old"); + writeTestFile(xmlPath, "old-xml"); + + auto options = makeCtfBundleConfig(outputDir, 1000000U); + CtfBundleOutput output(std::move(options)); + output.start(); + require(!std::filesystem::exists(outputDir / "old-marker") && + std::filesystem::is_regular_file(outputDir / "stream_0") && readTestTextFile(xmlPath) != "old-xml", + "CTF start must remove both previous targets and write directly to their final paths"); + + TraceEvent software = softwarePacket(1U, 1U, 'A'); + output.writeEvent(software); + output.abort(); + require(!std::filesystem::exists(outputDir) && !std::filesystem::exists(xmlPath), + "CTF abort must remove the incomplete direct output"); +} + +TEST(CtraceUnitTests, testCtfBundleOutputReplacesExistingBundleAtStart) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-direct-replace-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + const auto xmlPath = testTraceCompassXmlPath(outputDir); + writeTestFile(outputDir / "old-marker", "old"); + writeTestFile(xmlPath, "old-xml"); + + auto options = makeCtfBundleConfig(outputDir, 1000000U); + CtfBundleOutput output(std::move(options)); + output.start(); + const auto xml = readTestTextFile(xmlPath); + require(!std::filesystem::exists(outputDir / "old-marker") && + std::filesystem::is_regular_file(outputDir / "stream_0") && + !std::filesystem::exists(outputDir / "metadata") && xml != "old-xml" && !xml.empty(), + "CTF start must replace existing output before decoding begins"); + + TraceEvent software = softwarePacket(1U, 1U, 'A'); + software.tcyc = 10U; + output.writeEvent(software); + output.stop(); + require(std::filesystem::is_regular_file(outputDir / "metadata") && + std::filesystem::file_size(outputDir / "stream_0") > 0U && std::filesystem::is_regular_file(xmlPath), + "CTF stop must complete the directly written bundle"); +} + +TEST(CtraceUnitTests, testCtfBundleOutputRejectsOverlappingTargetsBeforeDeletion) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-overlapping-targets-test"); + const auto& root = temporaryPath.path(); + const auto ctfDirectory = root / "Bundle.ctf"; + const auto nestedXml = ctfDirectory / "Bundle.SWO.traceanalysis.xml"; + writeTestFile(ctfDirectory / "old-marker", "old-ctf"); + writeTestFile(nestedXml, "old-xml"); + + bool rejected = false; + try { + CtfBundleOutput output(CtfOutputConfig(ctfDirectory, nestedXml, 1000000U, {}, {})); + } catch (const std::invalid_argument&) { + rejected = true; + } + require(rejected && readTestTextFile(ctfDirectory / "old-marker") == "old-ctf" && + readTestTextFile(nestedXml) == "old-xml", + "overlapping CTF targets must be rejected before either existing target is deleted"); + + const auto wrongTypeCtf = root / "WrongType.ctf"; + const auto wrongTypeXml = root / "WrongType.SWO.traceanalysis.xml"; + writeTestFile(wrongTypeCtf, "not-a-directory"); + std::filesystem::create_directory(wrongTypeXml); + bool rejectedWrongTypes = false; + try { + CtfBundleOutput output(CtfOutputConfig(wrongTypeCtf, wrongTypeXml, 1000000U, {}, {})); + output.start(); + } catch (const std::runtime_error&) { + rejectedWrongTypes = true; + } + require(rejectedWrongTypes && readTestTextFile(wrongTypeCtf) == "not-a-directory" && + std::filesystem::is_directory(wrongTypeXml), + "CTF start must reject unexpected target types before deleting either target"); + +#ifdef _WIN32 + bool rejectedCaseInsensitiveOverlap = false; + try { + CtfBundleOutput output( + CtfOutputConfig(root / "Bundle.ctf", root / "BUNDLE.CTF" / "Bundle.SWO.traceanalysis.xml", 1000000U, {}, {})); + } catch (const std::invalid_argument&) { + rejectedCaseInsensitiveOverlap = true; + } + require(rejectedCaseInsensitiveOverlap, "Windows CTF target overlap checks must be case-insensitive"); +#endif +} + +TEST(CtraceUnitTests, testCtfBundleOutputOwnsDirectLifecycle) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-bundle-lifecycle-test"); + const auto& testRoot = temporaryPath.path(); + const auto root = testRoot / std::filesystem::u8path(u8"Gr\u00f6\u00dfe"); + const auto ctfDirectory = root / "Bundle.ctf"; + const auto xmlPath = root / "Bundle.SWO.traceanalysis.xml"; + writeTestFile(ctfDirectory / "old-marker", "old-ctf"); + writeTestFile(xmlPath, "old-xml"); + + auto config = makeCtfBundleConfig(ctfDirectory, 1000000U); + CtfBundleOutput output(std::move(config)); + output.start(); + TraceEvent software = softwarePacket(1U, 1U, 'A'); + software.tcyc = 10U; + output.writeEvent(software); + output.stop(); + + require(!std::filesystem::exists(ctfDirectory / "old-marker") && + std::filesystem::is_regular_file(ctfDirectory / "metadata") && + std::filesystem::is_regular_file(ctfDirectory / "stream_0") && std::filesystem::is_regular_file(xmlPath), + "CTF bundle must complete directly in its final output paths"); + const auto completedXml = readTestTextFile(xmlPath); + require(completedXml != "old-xml" && !completedXml.empty(), + "CTF bundle did not replace the previous Trace Compass XML"); + + writeTestFile(ctfDirectory / "stale-marker", "stale"); + writeTestFile(xmlPath, "stale-xml"); + output.start(); + require(!std::filesystem::exists(ctfDirectory / "stale-marker") && + !std::filesystem::exists(ctfDirectory / "metadata") && readTestTextFile(xmlPath) != "stale-xml", + "restarting CTF output must delete the previous bundle before writing"); + output.writeEvent(software); + output.abort(); + require(!std::filesystem::exists(ctfDirectory) && !std::filesystem::exists(xmlPath), + "aborted direct CTF output must not leave an incomplete bundle"); + + std::filesystem::create_directories(testRoot / "working"); + std::filesystem::create_directories(testRoot / "captures"); + const auto relativeRoot = testRoot / "working" / ".." / "captures"; + const auto relativeCtf = relativeRoot / "Relative.ctf"; + CtfBundleOutput relativeOutput(makeCtfBundleConfig(relativeCtf, 1000000U)); + relativeOutput.start(); + relativeOutput.writeEvent(software); + relativeOutput.stop(); + require(std::filesystem::is_regular_file(testRoot / "captures" / "Relative.ctf" / "metadata") && + std::filesystem::is_regular_file(testRoot / "captures" / "Relative.SWO.traceanalysis.xml"), + "CTF output must accept a legitimate parent-relative trace path"); +} diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp new file mode 100644 index 000000000..131837d6c --- /dev/null +++ b/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfTestSupport.hpp" +#include "TestSupport.hpp" + +#include + +#include "ctf/CtfEncoder.hpp" +#include "TestPath.hpp" +#include "TraceEvent.hpp" +#include "TraceSelection.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace CtfTestSupport; + +std::string formatCtfUuid(const std::vector& bytes, std::size_t offset) +{ + static constexpr char hexDigits[] = "0123456789abcdef"; + std::string result; + result.reserve(36U); + for (std::size_t index = 0; index < 16U; ++index) { + if (index == 4U || index == 6U || index == 8U || index == 10U) { + result.push_back('-'); + } + const auto byte = bytes[offset + index]; + result.push_back(hexDigits[(byte >> 4U) & 0x0fU]); + result.push_back(hexDigits[byte & 0x0fU]); + } + return result; +} + +void requireSingleCtfItmEvent(const std::filesystem::path& streamPath, std::uint8_t expectedChannel, + const std::string& message) +{ + const auto bytes = readTestBinaryFile(streamPath); + + constexpr std::size_t itmPayloadSize = 8U; + constexpr auto contentSize = kCtfEventOffset + kCtfEventHeaderSize + itmPayloadSize; + require(bytes.size() >= contentSize, message); + require(readLe32(bytes, kCtfPacketHeaderSize + 4U) == contentSize * 8U, message); + require(readLe32(bytes, kCtfEventOffset) == 0U, message); + require(bytes[kCtfEventOffset + kCtfEventHeaderSize] == expectedChannel, message); +} + +} // namespace + +TEST(CtraceUnitTests, testCtfEncoderWritesOnlyIntoProvidedDirectory) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-encoder-boundary-test"); + const auto& root = temporaryPath.path(); + const auto missingDirectory = root / "missing"; + const auto outputDirectory = root / "provided"; + + CtfEncoder encoder(CtfEncoderConfig{ + 1000000U, + TraceSelection{{"itm"}, {}}, + {}, + }); + bool rejectedMissingDirectory = false; + try { + encoder.start(missingDirectory); + } catch (const std::runtime_error&) { + rejectedMissingDirectory = true; + } + require(rejectedMissingDirectory && !std::filesystem::exists(missingDirectory), + "CtfEncoder must not create or own its output directory"); + + std::filesystem::create_directories(outputDirectory); + encoder.start(outputDirectory); + TraceEvent software = softwarePacket(1U, 1U, 'A'); + software.tcyc = 10U; + encoder.writeEvent(software); + encoder.stop(); + require(std::filesystem::is_regular_file(outputDirectory / "metadata") && + std::filesystem::is_regular_file(outputDirectory / "stream_0"), + "CtfEncoder must encode metadata and stream data into the provided directory"); + requireSingleCtfItmEvent(outputDirectory / "stream_0", 1U, "CtfEncoder encoded an unexpected ITM event"); + + encoder.abort(); + require(std::filesystem::is_regular_file(outputDirectory / "metadata"), + "CtfEncoder abort must not delete a directory owned by its caller"); +} + +TEST(CtraceUnitTests, testCtfEncoderPacketBoundaryAndUuid) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-encoder-packet-boundary-test"); + const auto& outputDirectory = temporaryPath.path(); + std::filesystem::create_directories(outputDirectory); + + CtfEncoder encoder(CtfEncoderConfig{ + 1000000U, + TraceSelection{{"itm"}, {}}, + {}, + }); + encoder.start(outputDirectory); + + // A one-byte ITM event occupies 21 bytes. Exactly 3118 events fit after + // the 56-byte header/context; event 3119 starts packet 2. + constexpr std::size_t eventsInFirstPacket = 3118U; + for (std::size_t index = 0; index <= eventsInFirstPacket; ++index) { + TraceEvent software = softwarePacket(1U, 1U, static_cast(index)); + software.tcyc = index + 1U; + encoder.writeEvent(software); + } + encoder.stop(); + + const auto stream = readTestBinaryFile(outputDirectory / "stream_0"); + const auto metadata = readTestTextFile(outputDirectory / "metadata"); + + constexpr std::size_t eventSize = 21U; + constexpr auto firstContentSize = kCtfPacketHeaderSize + kCtfPacketContextSize + eventsInFirstPacket * eventSize; + constexpr std::uint32_t ctfMagic = 0xc1fc1fc1U; + require(stream.size() == kCtfPacketSize * 2U, "CTF packet rollover must emit two complete 64-KiB packets"); + require(readLe32(stream, 0U) == ctfMagic && readLe32(stream, kCtfPacketSize) == ctfMagic, + "CTF packet rollover emitted an invalid packet header"); + require(readLe32(stream, kCtfPacketHeaderSize) == kCtfPacketSize * 8U && + readLe32(stream, kCtfPacketHeaderSize + 4U) == firstContentSize * 8U, + "first CTF packet content size mismatch at rollover"); + require(readLe32(stream, kCtfPacketSize + kCtfPacketHeaderSize + 4U) == (kCtfEventOffset + eventSize) * 8U, + "second CTF packet content size mismatch"); + require(readLe32(stream, kCtfPacketHeaderSize + 28U) == 0U && + readLe32(stream, kCtfPacketSize + kCtfPacketHeaderSize + 28U) == 1U, + "CTF packet sequence must advance across a 64-KiB boundary"); + require(std::equal(stream.begin() + 4U, stream.begin() + 20U, stream.begin() + kCtfPacketSize + 4U), + "CTF packet UUID must remain stable across packet rollover"); + + const auto uuid = formatCtfUuid(stream, 4U); + require(metadata.find("uuid = \"" + uuid + "\";") != std::string::npos, + "CTF metadata UUID must match the binary packet UUID"); + require((stream[4U + 6U] & 0xf0U) == 0x40U && (stream[4U + 8U] & 0xc0U) == 0x80U, + "CTF UUID must use RFC 4122 version-4 and variant bits"); + + encoder.abort(); +} + +TEST(CtraceUnitTests, testCtfEncoderDwtAddressEncoding) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-encoder-dwt-address-test"); + const auto& outputDirectory = temporaryPath.path(); + std::filesystem::create_directories(outputDirectory); + + CtfEncoder encoder(CtfEncoderConfig{ + 1000000U, + TraceSelection{{"dwt"}, {}}, + {}, + }); + encoder.start(outputDirectory); + TraceEvent address{DwtAddressTraceEvent{ + 3U, + DwtPcAndOffsetTraceLocation{0x12345678U, 0x0000abcdU}, + }}; + address.tcyc = 99U; + encoder.writeEvent(address); + encoder.stop(); + + const auto stream = readTestBinaryFile(outputDirectory / "stream_0"); + + constexpr std::size_t payloadSize = 14U; + constexpr std::size_t payloadOffset = kCtfEventOffset + kCtfEventHeaderSize; + require(readLe32(stream, kCtfPacketHeaderSize + 4U) == (kCtfEventOffset + kCtfEventHeaderSize + payloadSize) * 8U, + "CTF DWT address event content size mismatch"); + require(readLe32(stream, kCtfEventOffset) == 6U, "CTF DWT address event ID mismatch"); + require(readLe64(stream, kCtfEventOffset + 4U) == 99U, "CTF DWT address timestamp mismatch"); + require(stream[payloadOffset] == 3U && stream[payloadOffset + 1U] == 1U && stream[payloadOffset + 2U] == 1U, + "CTF DWT address comparator or presence flags mismatch"); + require(readLe32(stream, payloadOffset + 3U) == 0x12345678U && readLe16(stream, payloadOffset + 7U) == 0xabcdU, + "CTF DWT PC/address payload mismatch"); + + encoder.abort(); +} diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfSchemaTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfSchemaTests.cpp new file mode 100644 index 000000000..88e83ac27 --- /dev/null +++ b/tools/ctrace/test/unit/src/output/ctf/CtfSchemaTests.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.hpp" + +#include + +#include "ctf/CtfExceptionLaneTracker.hpp" +#include "ctf/CtfSchema.hpp" +#include "TraceEvent.hpp" + +#include +#include +#include + +TEST(CtraceUnitTests, testCtfValueTypes) +{ + struct SupportedType { + const char* name; + CtfSchema::ValueTag tag; + std::uint8_t size; + }; + const std::vector supported{ + {"unsigned int", CtfSchema::ValueTag::Unsigned8, 1U}, {"unsigned int", CtfSchema::ValueTag::Unsigned16, 2U}, + {"unsigned int", CtfSchema::ValueTag::Unsigned32, 4U}, {"signed int", CtfSchema::ValueTag::Signed8, 1U}, + {"signed int", CtfSchema::ValueTag::Signed16, 2U}, {"signed int", CtfSchema::ValueTag::Signed32, 4U}, + {"float", CtfSchema::ValueTag::Float32, 4U}, + }; + for (const auto& item : supported) { + const auto* resolved = CtfSchema::valueVariantForTraceRunType(item.name, item.size); + require(resolved != nullptr, std::string("supported CTF data type was rejected: ") + item.name); + require(resolved->tag == item.tag, std::string("CTF type tag mismatch: ") + item.name); + require(resolved->byteSize == item.size, std::string("CTF type size mismatch: ") + item.name); + } + + require(CtfSchema::valueVariantForTraceRunType("uint", 4U) == nullptr, "unsupported CTF type alias was accepted"); + require(CtfSchema::valueVariantForTraceRunType("int", 4U) == nullptr, "unsupported signed type alias was accepted"); + require(CtfSchema::valueVariantForTraceRunType("float32", 4U) == nullptr, + "unsupported float type alias was accepted"); + require(CtfSchema::valueVariantForTraceRunType("double", 4U) == nullptr, "unsupported CTF type was accepted"); + require(CtfSchema::valueVariantForTraceRunType("float", 2U) == nullptr, "incompatible CTF float width was accepted"); + require(CtfSchema::valueVariantForTraceRunType("unsigned int", 8U) == nullptr, "invalid CTF data size was accepted"); +} + +TEST(CtraceUnitTests, testCtfExceptionLaneTracker) +{ + CtfExceptionLaneTracker tracker; + std::vector records; + const auto emit = [&records](std::uint32_t number, CtfExceptionLaneTracker::RecordAction action) { + records.push_back(std::to_string(number) + + (action == CtfExceptionLaneTracker::RecordAction::Enter ? ":enter" : ":exit")); + }; + + tracker.startThreadMode(emit); + tracker.consume(ExceptionTraceEvent{3, ExceptionAction::Entered}, emit); + tracker.consume(ExceptionTraceEvent{15, ExceptionAction::Entered}, emit); + tracker.consume(ExceptionTraceEvent{15, ExceptionAction::Exited}, emit); + tracker.consume(ExceptionTraceEvent{54, ExceptionAction::Entered}, emit); + tracker.consume(ExceptionTraceEvent{54, ExceptionAction::Exited}, emit); + tracker.consume(ExceptionTraceEvent{3, ExceptionAction::Returned}, emit); + tracker.consume(ExceptionTraceEvent{15, ExceptionAction::Exited}, emit); + tracker.consume(ExceptionTraceEvent{3, ExceptionAction::Exited}, emit); + + require(records == std::vector({ + "0:enter", + "0:exit", + "3:enter", + "3:exit", + "15:enter", + "15:exit", + "3:enter", + "3:exit", + "54:enter", + "54:exit", + "3:enter", + "3:exit", + "0:enter", + }), + "CtfExceptionLaneTracker nested and tail-chain records mismatch"); + require(tracker.observedExceptionNumbers() == std::vector({0U, 3U, 15U, 54U}), + "CtfExceptionLaneTracker observed lanes mismatch"); + + tracker.resetForDiscontinuity(emit); + require(records.back() == "0:exit", "CtfExceptionLaneTracker discontinuity must close the active lane"); + const auto recordCount = records.size(); + tracker.consume(ExceptionTraceEvent{0, ExceptionAction::Unknown}, emit); + require(records.size() == recordCount, "CtfExceptionLaneTracker must ignore unknown exception actions"); + + tracker.reset(); + tracker.startThreadMode(emit); + require(tracker.observedExceptionNumbers() == std::vector({0U}), + "CtfExceptionLaneTracker reset must clear observed lanes"); + + records.clear(); + tracker.consume(ExceptionTraceEvent{3, ExceptionAction::Entered}, emit); + tracker.consume(ExceptionTraceEvent{15, ExceptionAction::Entered}, emit); + tracker.consume(ExceptionTraceEvent{15, ExceptionAction::Exited}, emit); + const auto preemptedRecordCount = records.size(); + tracker.consume(ExceptionTraceEvent{3, ExceptionAction::Exited}, emit); + require(records.size() == preemptedRecordCount, + "CtfExceptionLaneTracker must not exit a preempted context before its return packet"); + tracker.consume(ExceptionTraceEvent{3, ExceptionAction::Returned}, emit); + tracker.consume(ExceptionTraceEvent{3, ExceptionAction::Exited}, emit); + require(records.size() == preemptedRecordCount + 2U && records[records.size() - 2U] == "3:exit" && + records.back() == "0:enter", + "CtfExceptionLaneTracker must exit a resumed context"); +} diff --git a/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp b/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp new file mode 100644 index 000000000..e98973453 --- /dev/null +++ b/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.hpp" +#include +#include "CtraceRunMeta.hpp" +#include "TraceRunConfig.hpp" +#include +#include + +TEST(CtraceUnitTests, testTimestampPrescalerMetadataDefaults) +{ + const auto ctraceRunMeta = CtraceRunMeta::fromConfig(TraceRunConfig{}); + require(!ctraceRunMeta.timestampPrescaler().has_value(), + "a missing timestamp setup must leave the metadata prescaler unspecified"); + + TraceRunConfig incompleteTraceRun; + TraceRunSetup incompleteSetup; + incompleteSetup.timestamps = TraceRunTimestampSetup{}; + incompleteTraceRun.setups.push_back(incompleteSetup); + const auto missingPrescalerMeta = CtraceRunMeta::fromConfig(incompleteTraceRun); + require(missingPrescalerMeta.timestampPrescaler() == + std::optional(TraceRunSchema::kDefaultTimestampPrescaler), + "timestamp setup must resolve an omitted prescaler to the specified default"); + TraceRunConfig traceRun; + TraceRunSetup setup; + setup.timestamps = TraceRunTimestampSetup{std::nullopt, 4U}; + traceRun.setups.push_back(setup); + const auto prescalerOnlyMeta = CtraceRunMeta::fromConfig(traceRun); + require(prescalerOnlyMeta.timestampPrescaler() == std::optional(4U), + "explicit timestamp prescaler metadata mismatch"); +} diff --git a/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp b/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp new file mode 100644 index 000000000..b7906d9e7 --- /dev/null +++ b/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtraceRunMeta.hpp" +#include "TestPath.hpp" +#include "TestSupport.hpp" +#include "YmlTraceRunConfigReader.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace { + +const CtraceRunSourceMeta* findSource(const CtraceRunMeta& meta, const std::string& type, std::uint32_t source) +{ + for (const auto& candidate : meta.sources()) { + if (candidate.type == type && candidate.source == source) { + return &candidate; + } + } + return nullptr; +} + +class TemporaryTraceRunFile { +public: + explicit TemporaryTraceRunFile(const std::string& name) : root_(name), path_(root_.path() / "Board.ctrace-run.yml") {} + + const std::filesystem::path& path() const + { + return path_; + } + +private: + TemporaryTestPath root_; + std::filesystem::path path_; +}; + +} // namespace + +TEST(CtraceUnitTests, TraceRunReaderConsumesOnlyRelevantFields) +{ + TemporaryTraceRunFile file("ctrace-run-reader-relevant-fields-test"); + writeTestFile(file.path(), R"yml(format-version: ignored +producer-data: + malformed: [but, irrelevant] +ctrace-run: + generated-by: ignored + created-by: ignored + vendor-extension: { arbitrary: [content] } + ctrace-setup: + - pname: core0 + timestamps: + clock: 400000000 + itm-prescaler: 4 + timestamp-extension: [ignored] + itm: + enable: 0x00000006 + privileged: [ignored] + data: + - location: ignored + symbol-file: ignored + type: [obsolete-and-ignored] + size: malformed-but-ignored + symbol-type: signed int + symbol-size: 1 + data-extension: { ignored: true } + exceptions: [ignored] + pcsampling: { ignored: true } + synchronization: malformed-but-ignored + - unknown-setup: [ignored] + ctrace-refs: + - ctrace-ref: core0/itm + pname: core0 + type: itm + stream: 2 + source: 1 + label: Console + regs: [ignored] + - ctrace-ref: core0/data#0 + pname: core0 + type: dwt + stream: 2 + source: 0 + symbol-address: 0x20000100 + label: Current + reference-extension: [ignored] + - { ctrace-ref: ignored/etm, type: etm, source: [invalid] } + - { ctrace-ref: ignored/exception, type: exception, source: [invalid] } + - { ctrace-ref: ignored/missing-type, source: [invalid] } + - malformed-entry-is-ignored +)yml"); + + const auto config = YmlTraceRunConfigReader().read(file.path().string()); + ASSERT_EQ(config.references.size(), 2U); + ASSERT_EQ(config.setups.size(), 1U); + + const auto meta = CtraceRunMeta::fromConfig(config); + EXPECT_EQ(meta.processorCount(), 1U); + EXPECT_EQ(meta.timestampClockHz(), std::optional(400000000U)); + EXPECT_EQ(meta.timestampPrescaler(), std::optional(4U)); + ASSERT_EQ(meta.itmEnableMasksByTraceBusId().at(2U), 0x00000006U); + + const auto* itm = findSource(meta, "itm", 1U); + ASSERT_NE(itm, nullptr); + EXPECT_EQ(itm->label, std::optional("Console")); + + const auto* dwt = findSource(meta, "dwt", 0U); + ASSERT_NE(dwt, nullptr); + EXPECT_EQ(dwt->valueType, "signed int"); + EXPECT_EQ(dwt->valueSize, 1U); + EXPECT_EQ(dwt->symbolAddress, std::optional(0x20000100U)); + EXPECT_EQ(dwt->label, std::optional("Current")); + + writeTestFile(file.path(), R"yml(unrelated-root: [ignored] +ctrace-run: + generated-by: ignored + unsupported-content: { malformed: [but, irrelevant] } +)yml"); + const auto empty = YmlTraceRunConfigReader().read(file.path().string()); + EXPECT_TRUE(empty.references.empty()); + EXPECT_TRUE(empty.setups.empty()); +} + +TEST(CtraceUnitTests, TraceRunReaderValidatesConsumedFields) +{ + TemporaryTraceRunFile file("ctrace-run-reader-consumed-fields-test"); + writeTestFile(file.path(), R"yml(ctrace-run: + ctrace-refs: + - ctrace-ref: relevant/itm + type: itm + source: [1] +)yml"); + + EXPECT_THROW((void)YmlTraceRunConfigReader().read(file.path().string()), std::runtime_error); +} diff --git a/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp b/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp new file mode 100644 index 000000000..431884dad --- /dev/null +++ b/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestPath.hpp" +#include "TestSupport.hpp" +#include +#include "TraceRunDiscovery.hpp" +#include +#include +#include +#include + +TEST(CtraceUnitTests, testTraceRunDiscovery) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-run-discovery-test"); + const auto& traceDir = temporaryPath.path(); + writeTestFile(traceDir / "Beta.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Alpha.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Alpha.SWO.raw"); + writeTestFile(traceDir / "Alpha.TB.raw"); + writeTestFile(traceDir / "Alpha.ER.raw"); + writeTestFile(traceDir / "Alpha.swo.raw"); + writeTestFile(traceDir / "Alpha.custom.raw"); + writeTestFile(traceDir / "unrelated.raw"); + + const auto batch = TraceRunDiscovery::selectConfigFiles(traceDir, std::nullopt); + require(batch.size() == 2U, "TraceRunDiscovery batch configuration count mismatch"); + require(batch[0].filename() == "Alpha.ctrace-run.yml", "TraceRunDiscovery batch sort mismatch"); + require(batch[1].filename() == "Beta.ctrace-run.yml", "TraceRunDiscovery second batch item mismatch"); + + const auto selected = TraceRunDiscovery::selectConfigFiles(traceDir, std::string("Alpha")); + require(selected.size() == 1U, "TraceRunDiscovery target selection count mismatch"); + require(selected[0].filename() == "Alpha.ctrace-run.yml", "TraceRunDiscovery target path mismatch"); + require(TraceRunDiscovery::solutionSetName(selected[0]) == "Alpha", "TraceRunDiscovery solution-set mismatch"); + + const auto rawInputs = TraceRunDiscovery::rawInputs(selected[0]); + require(rawInputs.size() == 3U, "TraceRunDiscovery must accept only the specified trace channels"); + require(rawInputs[0].channel == "ER", "TraceRunDiscovery ER channel mismatch"); + require(rawInputs[1].channel == "SWO", "TraceRunDiscovery SWO channel mismatch"); + require(rawInputs[2].channel == "TB", "TraceRunDiscovery TB channel mismatch"); + + const std::vector unsafeTargets{ + "../Alpha", "..\\Alpha", "/Alpha", "C:Alpha", "C:/Alpha", "C:\\Alpha", + "\\\\server\\share", "Alpha:stream", "Alpha?Beta", "Alpha.", "NUL", "COM1.log", + }; + for (const auto& unsafeTarget : unsafeTargets) { + bool rejected = false; + try { + (void)TraceRunDiscovery::selectConfigFiles(traceDir, unsafeTarget); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).find("solution-set name") != std::string::npos; + } + require(rejected, "TraceRunDiscovery should reject unsafe target name: " + unsafeTarget); + } +} diff --git a/tools/ctrace/test/unit/support/CtfTestSupport.hpp b/tools/ctrace/test/unit/support/CtfTestSupport.hpp new file mode 100644 index 000000000..0747cf26a --- /dev/null +++ b/tools/ctrace/test/unit/support/CtfTestSupport.hpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include +#include +#include + +namespace CtfTestSupport { + +inline constexpr std::size_t kCtfPacketHeaderSize = 24U; +inline constexpr std::size_t kCtfPacketContextSize = 32U; +inline constexpr std::size_t kCtfEventHeaderSize = 13U; +inline constexpr std::size_t kCtfEventOffset = kCtfPacketHeaderSize + kCtfPacketContextSize; +inline constexpr std::size_t kCtfPacketSize = 65536U; + +inline std::uint16_t readLe16(const std::vector& bytes, std::size_t offset) +{ + return static_cast(static_cast(bytes[offset]) | + (static_cast(bytes[offset + 1U]) << 8U)); +} + +inline std::uint32_t readLe32(const std::vector& bytes, std::size_t offset) +{ + return static_cast(bytes[offset]) | (static_cast(bytes[offset + 1U]) << 8U) | + (static_cast(bytes[offset + 2U]) << 16U) | + (static_cast(bytes[offset + 3U]) << 24U); +} + +inline std::uint64_t readLe64(const std::vector& bytes, std::size_t offset) +{ + std::uint64_t value = 0U; + for (std::uint32_t shift = 0U; shift < 64U; shift += 8U) { + value |= static_cast(bytes[offset++]) << shift; + } + return value; +} + +} // namespace CtfTestSupport diff --git a/tools/ctrace/test/unit/support/TestPath.hpp b/tools/ctrace/test/unit/support/TestPath.hpp new file mode 100644 index 000000000..264dcca82 --- /dev/null +++ b/tools/ctrace/test/unit/support/TestPath.hpp @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#else +#include +#endif + +inline std::filesystem::path testTemporaryPath(const std::string& name) +{ +#if defined(_WIN32) + const auto processId = _getpid(); +#else + const auto processId = getpid(); +#endif + return std::filesystem::temp_directory_path() / (name + '-' + std::to_string(processId)); +} + +class TemporaryTestPath { +public: + explicit TemporaryTestPath(const std::string& name) : path_(testTemporaryPath(name)) + { + std::error_code error; + std::filesystem::remove_all(path_, error); + if (error) { + throw std::runtime_error("failed to prepare temporary test path: " + path_.string()); + } + } + + ~TemporaryTestPath() + { + std::error_code ignored; + std::filesystem::remove_all(path_, ignored); + } + + TemporaryTestPath(const TemporaryTestPath&) = delete; + TemporaryTestPath& operator=(const TemporaryTestPath&) = delete; + + const std::filesystem::path& path() const + { + return path_; + } + +private: + std::filesystem::path path_; +}; diff --git a/tools/ctrace/test/unit/support/TestSupport.hpp b/tools/ctrace/test/unit/support/TestSupport.hpp new file mode 100644 index 000000000..07fb38d2b --- /dev/null +++ b/tools/ctrace/test/unit/support/TestSupport.hpp @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#pragma once + +#include "DiagnosticSink.hpp" +#include "TraceEvent.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +inline void require(bool condition, const std::string& message) +{ + if (!condition) { + throw std::runtime_error(message); + } +} + +class CollectingDiagnosticSink final : public DiagnosticSink { +public: + const std::vector& events() const + { + return events_; + } + + void clear() + { + events_.clear(); + } + +protected: + void write(const Event& event) override + { + events_.push_back(event); + } + +private: + std::vector events_; +}; + +class CollectingEventSink final : public TraceEventSink { +public: + void append(const TraceEvent& event) override + { + events.push_back(event); + } + + std::vector events; +}; + +inline void writeTestFile(const std::filesystem::path& path, const std::string& contents = {}) +{ + std::filesystem::create_directories(path.parent_path()); + std::ofstream out(path, std::ios::binary | std::ios::trunc); + if (!out) { + throw std::runtime_error("failed to create test file: " + path.string()); + } + out << contents; +} + +inline std::string readTestTextFile(const std::filesystem::path& path) +{ + std::ifstream in(path, std::ios::binary); + if (!in) { + throw std::runtime_error("failed to read test file: " + path.string()); + } + return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; +} + +inline std::vector readTestLines(const std::filesystem::path& path) +{ + std::ifstream in(path); + if (!in) { + throw std::runtime_error("failed to read test file: " + path.string()); + } + std::vector lines; + for (std::string line; std::getline(in, line);) { + lines.push_back(std::move(line)); + } + return lines; +} + +inline std::vector readTestBinaryFile(const std::filesystem::path& path) +{ + std::ifstream in(path, std::ios::binary); + if (!in) { + throw std::runtime_error("failed to read binary test file: " + path.string()); + } + return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; +} + +inline std::vector splitCsvTestLine(const std::string& line) +{ + std::vector fields; + std::istringstream in(line); + std::string field; + while (std::getline(in, field, ',')) { + fields.push_back(field); + } + if (!line.empty() && line.back() == ',') { + fields.emplace_back(); + } + return fields; +} + +inline TraceEvent exceptionPacket(std::uint32_t number, ExceptionAction action, std::uint64_t tcyc = 0) +{ + TraceEvent packet{ExceptionTraceEvent{number, action}}; + packet.tcyc = tcyc; + return packet; +} + +inline TraceEvent overflowPacket(std::uint64_t tcyc) +{ + TraceEvent packet{OverflowTraceEvent{}}; + packet.tcyc = tcyc; + packet.quality = TraceQuality{true, false, 1U}; + return packet; +} + +inline TraceEvent softwarePacket(std::uint32_t channel, std::uint8_t size = 1, std::uint32_t value = 0) +{ + return TraceEvent{SoftwareTraceEvent{channel, size, value}}; +} + +inline TraceEvent issuePacket(std::string code, std::string message = {}, + TraceIssueSeverity severity = TraceIssueSeverity::Error) +{ + return TraceEvent{TraceIssueEvent{ + std::move(code), + severity, + std::move(message), + }}; +} From f9f19287778e8bf173b9eb096fddd4d4fc493ec3 Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Thu, 30 Jul 2026 16:17:34 +0200 Subject: [PATCH 02/35] [ctrace] Expand architecture documentation --- tools/ctrace/docs/architecture.md | 96 +++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 5 deletions(-) diff --git a/tools/ctrace/docs/architecture.md b/tools/ctrace/docs/architecture.md index 783818f70..8aba702cd 100644 --- a/tools/ctrace/docs/architecture.md +++ b/tools/ctrace/docs/architecture.md @@ -21,6 +21,57 @@ implemented. 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 common base name joins configuration and trace data; for example, +`Board.ctrace-run.yml` describes the sources and timing metadata required to decode `Board.SWO.raw`. A matching +`Board.TB.raw` is discovered as part of the same solution set but is skipped by the first release profile. + +The main in-memory path is: + +```text +command line + trace-run YAML + SWO raw file + | + v + TraceDirectoryJob / FileDecodeJob + discovery, metadata, output plan + | + v + 64 KiB non-owning byte views + | + v + OpenCSD + ITM framing, packets, recovery + | + v + OpenCsdTraceElement values + | + v + CortexMStreamDecoder / CortexMPostDecoder + timestamps, DWT pairing, quality + | + v + TraceEvent values + | + v + DecodeConsumers + / | \ + diagnostics CSV backend CTF backend +``` + +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. + +| Stage | Owner | Transformation | +| --- | --- | --- | +| Discover | `TraceDirectoryJob` | Trace directory and target selection to solution-set configuration and raw inputs | +| Prepare | `CtraceRunMeta`, `FileDecodeJob` | YAML representation to normalized runtime metadata and an output plan | +| Decode protocol | `DecodePipeline`, `OpenCsdItmDecoder` | Raw byte chunks to recoverable OpenCSD trace elements | +| Interpret | `CortexMStreamDecoder`, `CortexMPostDecoder` | Protocol elements to timestamped semantic events | +| Consume | `DecodeConsumers` | One ordered event stream to diagnostics and every enabled output backend | +| Complete | `TraceOutputLifecycle` | Complete active artifacts, or remove them after output or decoder failure | + ## Runtime flow 1. `CtraceMain` parses and validates the command line. @@ -38,12 +89,47 @@ formats independent of OpenCSD and allows another raw trace channel to reuse the Without `--csv`, `--ctf`, or `--all`, the same pipeline runs in validation-only mode without creating output files. +## Processing state and ownership + +One `DecodePipeline` is created for each supported raw file. It owns the OpenCSD adapter and Cortex-M stream decoder, +so protocol, timestamp, and DWT state survive arbitrary file-read boundaries. `RawFileReader` owns a single 64 KiB +buffer; each `RawByteView` borrows that buffer only for the synchronous `DecodePipeline::push` call. Calling +`DecodePipeline::finish` flushes both the OpenCSD and Cortex-M layers before the pipeline is destroyed. + +`CortexMStreamDecoder` maintains an independent post-decoder for each observed Trace Bus ID. All post-decoders emit +into the same `TraceEventSink`, preserving input order while keeping stream-specific timestamp and DWT state apart. + +There is no application-wide event queue. `DecodeConsumers` forwards each event synchronously to the output +lifecycle and issue reporter. Output backends own their files and are isolated from one another: failure of one +backend aborts its incomplete artifact but does not directly stop another active backend. A decoder-fatal error +aborts every still-active output for that raw file. + +The diagnostic sink lives for the complete command invocation. It therefore aggregates failures across solution sets +and determines the final process status after processing has continued wherever possible. + +## Suggested code-reading path + +1. Start at [`CtraceMain.cpp`](../src/CtraceMain.cpp) for command-line handling and top-level error policy. +2. Follow [`TraceDirectoryJob.cpp`](../src/control/TraceDirectoryJob.cpp) to see how solution sets, YAML, SWO, and + unsupported Trace Bus input are associated. +3. Read [`FileDecodeJob.cpp`](../src/control/FileDecodeJob.cpp) for output preflight, chunked input, pipeline + construction, and finalization. +4. Continue through [`DecodePipeline.cpp`](../src/decode/DecodePipeline.cpp), + [`OpenCsdItmDecoder.cpp`](../src/decode/OpenCsdItmDecoder.cpp), and + [`CortexMStreamDecoder.cpp`](../src/decode/CortexMStreamDecoder.cpp) for the two decode representations. +5. Use [`TraceEvent.hpp`](../src/model/TraceEvent.hpp) as the semantic contract between decoding and all consumers. +6. Finish with [`DecodeConsumers.cpp`](../src/control/DecodeConsumers.cpp) and + [`TraceOutputLifecycle.cpp`](../src/output/TraceOutputLifecycle.cpp), then inspect either the CSV or CTF backend. + +This path follows one trace file through the system without requiring the build-target graph or every backend detail +up front. + ## Module boundaries ### Entry point and orchestration | Module | Responsibility | -|---|---| +| --- | --- | | `src/CtraceMain.hpp` | Platform-independent entry point used by the executable trampoline | | `src/cli` | Command-line parsing, value normalization, and validation | | `src/control` | Solution-set orchestration, raw-file access, output setup, and per-file decode jobs | @@ -56,7 +142,7 @@ must not depend on control jobs or command-line details. ### Trace-run metadata | Module | Responsibility | -|---|---| +| --- | --- | | `src/tracerun` | File discovery, YAML parsing, schema subset validation, and normalized metadata | The YAML reader intentionally consumes only data required by `ctrace`. A malformed consumed field is an error, while @@ -69,7 +155,7 @@ normalized metadata instead of navigating YAML nodes. ### Decode and event model | Module | Responsibility | -|---|---| +| --- | --- | | `src/decode` | Raw-byte decoder interface, OpenCSD integration, packet recovery, timestamps, and Cortex-M semantics | | `src/model` | Backend-independent event types, quality information, and event selection | @@ -84,7 +170,7 @@ to make independent decisions without reconstructing decoder state. ### Output | Module | Responsibility | -|---|---| +| --- | --- | | `src/output` | Backend requirements, output planning, and lifecycle management | | `src/output/csv` | Stable CSV schema, row mapping, filtering, and file output | | `src/output/ctf` | CTF metadata and stream encoding plus Trace Compass analysis XML | @@ -110,7 +196,7 @@ fatal diagnostic occurred. ## External dependencies | Dependency | Use | -|---|---| +| --- | --- | | `cxxopts` | Command-line parsing | | `yaml-cpp` | Trace-run YAML parsing | | `OpenCSD` 1.8.3 | ITM protocol decoding; pinned as a repository submodule | From 84c724c24d029d1616d4dc3a6a51ba60e0f95851 Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Thu, 30 Jul 2026 17:42:24 +0200 Subject: [PATCH 03/35] [ctrace] Fix static analysis and Windows tests --- .github/.cppcheck_suppressions | 26 +++ .../src/decode/OpenCsdErrorController.cpp | 160 ++++++-------- tools/ctrace/src/decode/OpenCsdItmSession.cpp | 6 +- .../ctrace/src/diagnostics/DiagnosticSink.cpp | 5 - .../ctrace/src/diagnostics/DiagnosticSink.hpp | 1 - .../ctrace/src/output/OutputRequirements.cpp | 55 +++-- tools/ctrace/src/output/csv/CsvFileOutput.cpp | 4 +- .../ctrace/src/output/ctf/CtfBundleOutput.cpp | 2 +- .../src/output/ctf/CtfMetadataWriter.cpp | 105 ++++++--- .../ctrace/src/output/ctf/CtfStreamWriter.cpp | 2 +- .../src/output/ctf/TraceCompassXmlWriter.cpp | 100 +++++---- .../src/tracerun/YmlTraceRunConfigReader.cpp | 204 ++++++++++-------- .../test/unit/src/cli/CliParserTests.cpp | 105 ++++----- .../src/control/TraceDirectoryJobTests.cpp | 6 +- .../src/output/OutputRequirementsTests.cpp | 45 ++-- 15 files changed, 442 insertions(+), 384 deletions(-) diff --git a/.github/.cppcheck_suppressions b/.github/.cppcheck_suppressions index cfb00bf2a..c5d1baf54 100644 --- a/.github/.cppcheck_suppressions +++ b/.github/.cppcheck_suppressions @@ -4,3 +4,29 @@ unusedFunction:libs/xmltree/src/XMLTree.cpp:47 unusedFunction:libs/xmlschemachecker/src/XmlErrorHandler.cpp:27 unusedFunction:libs/xmlschemachecker/src/XmlErrorHandler.cpp:32 unusedFunction:tools/projmgr/src/ProjMgrRpcServer.cpp + +# OpenCSD invokes these ITraceErrorLog overrides through its external library. +unusedFunction:tools/ctrace/src/decode/OpenCsdErrorController.cpp:288 +unusedFunction:tools/ctrace/src/decode/OpenCsdErrorController.cpp:304 +unusedFunction:tools/ctrace/src/decode/OpenCsdErrorController.cpp:316 +unusedFunction:tools/ctrace/src/decode/OpenCsdErrorController.cpp:324 +unusedFunction:tools/ctrace/src/decode/OpenCsdErrorController.cpp:329 +unusedFunction:tools/ctrace/src/decode/OpenCsdErrorController.cpp:338 +unusedFunction:tools/ctrace/src/decode/OpenCsdErrorController.cpp:343 + +# cppcheck 2.7 treats lightweight std::string_view parameters as expensive copies. +passedByValue:tools/ctrace/src/model/TraceSelection.cpp:83 +passedByValue:tools/ctrace/src/model/TraceSelection.cpp:95 +passedByValue:tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp:145 +passedByValue:tools/ctrace/src/output/ctf/CtfSchema.hpp:87 +passedByValue:tools/ctrace/src/tracerun/TraceRunConfig.hpp:48 +passedByValue:tools/ctrace/src/tracerun/TraceRunConfig.hpp:58 +passedByValue:tools/ctrace/src/tracerun/TraceRunConfig.hpp:63 +passedByValue:tools/ctrace/src/tracerun/TraceRunConfig.hpp:68 +passedByValue:tools/ctrace/src/tracerun/TraceRunDiscovery.cpp:30 +passedByValue:tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp:64 +passedByValue:tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp:94 +passedByValue:tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp:100 +passedByValue:tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp:158 +passedByValue:tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp:168 +passedByValue:tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp:180 diff --git a/tools/ctrace/src/decode/OpenCsdErrorController.cpp b/tools/ctrace/src/decode/OpenCsdErrorController.cpp index 1c8f0188e..052059b9a 100644 --- a/tools/ctrace/src/decode/OpenCsdErrorController.cpp +++ b/tools/ctrace/src/decode/OpenCsdErrorController.cpp @@ -28,6 +28,62 @@ std::string trimTrailingWhitespace(std::string value) return value; } +struct OpenCsdErrorName { + ocsd_err_t code; + const char* name; +}; + +constexpr OpenCsdErrorName kOpenCsdErrorNames[] = { + {OCSD_OK, "OCSD_OK"}, + {OCSD_ERR_FAIL, "OCSD_ERR_FAIL"}, + {OCSD_ERR_MEM, "OCSD_ERR_MEM"}, + {OCSD_ERR_NOT_INIT, "OCSD_ERR_NOT_INIT"}, + {OCSD_ERR_INVALID_ID, "OCSD_ERR_INVALID_ID"}, + {OCSD_ERR_BAD_HANDLE, "OCSD_ERR_BAD_HANDLE"}, + {OCSD_ERR_INVALID_PARAM_VAL, "OCSD_ERR_INVALID_PARAM_VAL"}, + {OCSD_ERR_INVALID_PARAM_TYPE, "OCSD_ERR_INVALID_PARAM_TYPE"}, + {OCSD_ERR_FILE_ERROR, "OCSD_ERR_FILE_ERROR"}, + {OCSD_ERR_NO_PROTOCOL, "OCSD_ERR_NO_PROTOCOL"}, + {OCSD_ERR_ATTACH_TOO_MANY, "OCSD_ERR_ATTACH_TOO_MANY"}, + {OCSD_ERR_ATTACH_INVALID_PARAM, "OCSD_ERR_ATTACH_INVALID_PARAM"}, + {OCSD_ERR_ATTACH_COMP_NOT_FOUND, "OCSD_ERR_ATTACH_COMP_NOT_FOUND"}, + {OCSD_ERR_RDR_FILE_NOT_FOUND, "OCSD_ERR_RDR_FILE_NOT_FOUND"}, + {OCSD_ERR_RDR_INVALID_INIT, "OCSD_ERR_RDR_INVALID_INIT"}, + {OCSD_ERR_RDR_NO_DECODER, "OCSD_ERR_RDR_NO_DECODER"}, + {OCSD_ERR_DATA_DECODE_FATAL, "OCSD_ERR_DATA_DECODE_FATAL"}, + {OCSD_ERR_DFMTR_NOTCONTTRACE, "OCSD_ERR_DFMTR_NOTCONTTRACE"}, + {OCSD_ERR_DFMTR_BAD_FHSYNC, "OCSD_ERR_DFMTR_BAD_FHSYNC"}, + {OCSD_ERR_BAD_PACKET_SEQ, "OCSD_ERR_BAD_PACKET_SEQ"}, + {OCSD_ERR_INVALID_PCKT_HDR, "OCSD_ERR_INVALID_PCKT_HDR"}, + {OCSD_ERR_PKT_INTERP_FAIL, "OCSD_ERR_PKT_INTERP_FAIL"}, + {OCSD_ERR_UNSUPPORTED_ISA, "OCSD_ERR_UNSUPPORTED_ISA"}, + {OCSD_ERR_HW_CFG_UNSUPP, "OCSD_ERR_HW_CFG_UNSUPP"}, + {OCSD_ERR_UNSUPP_DECODE_PKT, "OCSD_ERR_UNSUPP_DECODE_PKT"}, + {OCSD_ERR_BAD_DECODE_PKT, "OCSD_ERR_BAD_DECODE_PKT"}, + {OCSD_ERR_COMMIT_PKT_OVERRUN, "OCSD_ERR_COMMIT_PKT_OVERRUN"}, + {OCSD_ERR_MEM_NACC, "OCSD_ERR_MEM_NACC"}, + {OCSD_ERR_RET_STACK_OVERFLOW, "OCSD_ERR_RET_STACK_OVERFLOW"}, + {OCSD_ERR_DCDT_NO_FORMATTER, "OCSD_ERR_DCDT_NO_FORMATTER"}, + {OCSD_ERR_MEM_ACC_OVERLAP, "OCSD_ERR_MEM_ACC_OVERLAP"}, + {OCSD_ERR_MEM_ACC_FILE_NOT_FOUND, "OCSD_ERR_MEM_ACC_FILE_NOT_FOUND"}, + {OCSD_ERR_MEM_ACC_FILE_DIFF_RANGE, "OCSD_ERR_MEM_ACC_FILE_DIFF_RANGE"}, + {OCSD_ERR_MEM_ACC_RANGE_INVALID, "OCSD_ERR_MEM_ACC_RANGE_INVALID"}, + {OCSD_ERR_MEM_ACC_BAD_LEN, "OCSD_ERR_MEM_ACC_BAD_LEN"}, + {OCSD_ERR_TEST_SNAPSHOT_PARSE, "OCSD_ERR_TEST_SNAPSHOT_PARSE"}, + {OCSD_ERR_TEST_SNAPSHOT_PARSE_INFO, "OCSD_ERR_TEST_SNAPSHOT_PARSE_INFO"}, + {OCSD_ERR_TEST_SNAPSHOT_READ, "OCSD_ERR_TEST_SNAPSHOT_READ"}, + {OCSD_ERR_TEST_SS_TO_DECODER, "OCSD_ERR_TEST_SS_TO_DECODER"}, + {OCSD_ERR_DCDREG_NAME_REPEAT, "OCSD_ERR_DCDREG_NAME_REPEAT"}, + {OCSD_ERR_DCDREG_NAME_UNKNOWN, "OCSD_ERR_DCDREG_NAME_UNKNOWN"}, + {OCSD_ERR_DCDREG_TYPE_UNKNOWN, "OCSD_ERR_DCDREG_TYPE_UNKNOWN"}, + {OCSD_ERR_DCDREG_TOOMANY, "OCSD_ERR_DCDREG_TOOMANY"}, + {OCSD_ERR_DCD_INTERFACE_UNUSED, "OCSD_ERR_DCD_INTERFACE_UNUSED"}, + {OCSD_ERR_INVALID_OPCODE, "OCSD_ERR_INVALID_OPCODE"}, + {OCSD_ERR_I_RANGE_LIMIT_OVERRUN, "OCSD_ERR_I_RANGE_LIMIT_OVERRUN"}, + {OCSD_ERR_BAD_DECODE_IMAGE, "OCSD_ERR_BAD_DECODE_IMAGE"}, + {OCSD_ERR_LAST, "OCSD_ERR_LAST"}, +}; + } // namespace void OpenCsdErrorController::beginDataPathCall() @@ -202,106 +258,12 @@ std::string OpenCsdErrorController::responseName(ocsd_datapath_resp_t response) std::string OpenCsdErrorController::errorCodeName(ocsd_err_t code) { - switch (code) { - case OCSD_OK: - return "OCSD_OK"; - case OCSD_ERR_FAIL: - return "OCSD_ERR_FAIL"; - case OCSD_ERR_MEM: - return "OCSD_ERR_MEM"; - case OCSD_ERR_NOT_INIT: - return "OCSD_ERR_NOT_INIT"; - case OCSD_ERR_INVALID_ID: - return "OCSD_ERR_INVALID_ID"; - case OCSD_ERR_BAD_HANDLE: - return "OCSD_ERR_BAD_HANDLE"; - case OCSD_ERR_INVALID_PARAM_VAL: - return "OCSD_ERR_INVALID_PARAM_VAL"; - case OCSD_ERR_INVALID_PARAM_TYPE: - return "OCSD_ERR_INVALID_PARAM_TYPE"; - case OCSD_ERR_FILE_ERROR: - return "OCSD_ERR_FILE_ERROR"; - case OCSD_ERR_NO_PROTOCOL: - return "OCSD_ERR_NO_PROTOCOL"; - case OCSD_ERR_ATTACH_TOO_MANY: - return "OCSD_ERR_ATTACH_TOO_MANY"; - case OCSD_ERR_ATTACH_INVALID_PARAM: - return "OCSD_ERR_ATTACH_INVALID_PARAM"; - case OCSD_ERR_ATTACH_COMP_NOT_FOUND: - return "OCSD_ERR_ATTACH_COMP_NOT_FOUND"; - case OCSD_ERR_RDR_FILE_NOT_FOUND: - return "OCSD_ERR_RDR_FILE_NOT_FOUND"; - case OCSD_ERR_RDR_INVALID_INIT: - return "OCSD_ERR_RDR_INVALID_INIT"; - case OCSD_ERR_RDR_NO_DECODER: - return "OCSD_ERR_RDR_NO_DECODER"; - case OCSD_ERR_DATA_DECODE_FATAL: - return "OCSD_ERR_DATA_DECODE_FATAL"; - case OCSD_ERR_DFMTR_NOTCONTTRACE: - return "OCSD_ERR_DFMTR_NOTCONTTRACE"; - case OCSD_ERR_DFMTR_BAD_FHSYNC: - return "OCSD_ERR_DFMTR_BAD_FHSYNC"; - case OCSD_ERR_BAD_PACKET_SEQ: - return "OCSD_ERR_BAD_PACKET_SEQ"; - case OCSD_ERR_INVALID_PCKT_HDR: - return "OCSD_ERR_INVALID_PCKT_HDR"; - case OCSD_ERR_PKT_INTERP_FAIL: - return "OCSD_ERR_PKT_INTERP_FAIL"; - case OCSD_ERR_UNSUPPORTED_ISA: - return "OCSD_ERR_UNSUPPORTED_ISA"; - case OCSD_ERR_HW_CFG_UNSUPP: - return "OCSD_ERR_HW_CFG_UNSUPP"; - case OCSD_ERR_UNSUPP_DECODE_PKT: - return "OCSD_ERR_UNSUPP_DECODE_PKT"; - case OCSD_ERR_BAD_DECODE_PKT: - return "OCSD_ERR_BAD_DECODE_PKT"; - case OCSD_ERR_COMMIT_PKT_OVERRUN: - return "OCSD_ERR_COMMIT_PKT_OVERRUN"; - case OCSD_ERR_MEM_NACC: - return "OCSD_ERR_MEM_NACC"; - case OCSD_ERR_RET_STACK_OVERFLOW: - return "OCSD_ERR_RET_STACK_OVERFLOW"; - case OCSD_ERR_DCDT_NO_FORMATTER: - return "OCSD_ERR_DCDT_NO_FORMATTER"; - case OCSD_ERR_MEM_ACC_OVERLAP: - return "OCSD_ERR_MEM_ACC_OVERLAP"; - case OCSD_ERR_MEM_ACC_FILE_NOT_FOUND: - return "OCSD_ERR_MEM_ACC_FILE_NOT_FOUND"; - case OCSD_ERR_MEM_ACC_FILE_DIFF_RANGE: - return "OCSD_ERR_MEM_ACC_FILE_DIFF_RANGE"; - case OCSD_ERR_MEM_ACC_RANGE_INVALID: - return "OCSD_ERR_MEM_ACC_RANGE_INVALID"; - case OCSD_ERR_MEM_ACC_BAD_LEN: - return "OCSD_ERR_MEM_ACC_BAD_LEN"; - case OCSD_ERR_TEST_SNAPSHOT_PARSE: - return "OCSD_ERR_TEST_SNAPSHOT_PARSE"; - case OCSD_ERR_TEST_SNAPSHOT_PARSE_INFO: - return "OCSD_ERR_TEST_SNAPSHOT_PARSE_INFO"; - case OCSD_ERR_TEST_SNAPSHOT_READ: - return "OCSD_ERR_TEST_SNAPSHOT_READ"; - case OCSD_ERR_TEST_SS_TO_DECODER: - return "OCSD_ERR_TEST_SS_TO_DECODER"; - case OCSD_ERR_DCDREG_NAME_REPEAT: - return "OCSD_ERR_DCDREG_NAME_REPEAT"; - case OCSD_ERR_DCDREG_NAME_UNKNOWN: - return "OCSD_ERR_DCDREG_NAME_UNKNOWN"; - case OCSD_ERR_DCDREG_TYPE_UNKNOWN: - return "OCSD_ERR_DCDREG_TYPE_UNKNOWN"; - case OCSD_ERR_DCDREG_TOOMANY: - return "OCSD_ERR_DCDREG_TOOMANY"; - case OCSD_ERR_DCD_INTERFACE_UNUSED: - return "OCSD_ERR_DCD_INTERFACE_UNUSED"; - case OCSD_ERR_INVALID_OPCODE: - return "OCSD_ERR_INVALID_OPCODE"; - case OCSD_ERR_I_RANGE_LIMIT_OVERRUN: - return "OCSD_ERR_I_RANGE_LIMIT_OVERRUN"; - case OCSD_ERR_BAD_DECODE_IMAGE: - return "OCSD_ERR_BAD_DECODE_IMAGE"; - case OCSD_ERR_LAST: - return "OCSD_ERR_LAST"; - default: - return "OCSD_ERR_UNKNOWN(" + std::to_string(static_cast(code)) + ")"; + const auto found = std::find_if(std::begin(kOpenCsdErrorNames), std::end(kOpenCsdErrorNames), + [code](const auto& entry) { return entry.code == code; }); + if (found != std::end(kOpenCsdErrorNames)) { + return found->name; } + return "OCSD_ERR_UNKNOWN(" + std::to_string(static_cast(code)) + ")"; } std::string OpenCsdErrorController::describe(const Decision& decision) diff --git a/tools/ctrace/src/decode/OpenCsdItmSession.cpp b/tools/ctrace/src/decode/OpenCsdItmSession.cpp index 5a28413e7..a949d79fe 100644 --- a/tools/ctrace/src/decode/OpenCsdItmSession.cpp +++ b/tools/ctrace/src/decode/OpenCsdItmSession.cpp @@ -21,17 +21,15 @@ namespace { constexpr std::uint32_t kItmTcrSwoEnable = 1U << 4U; +constexpr ocsd_itm_cfg kItmConfig{kItmTcrSwoEnable}; } // namespace OpenCsdItmSession::OpenCsdItmSession(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController) + : config_(&kItmConfig) { - ocsd_itm_cfg config{}; // Keep OpenCSD timestamps in raw ITM ticks. They are scaled after decode, // where the originating CoreSight stream and processor are known. - config.reg_tcr = kItmTcrSwoEnable; - config_ = &config; - try { createDecoder(collector, errorController); } catch (...) { diff --git a/tools/ctrace/src/diagnostics/DiagnosticSink.cpp b/tools/ctrace/src/diagnostics/DiagnosticSink.cpp index 389c8f3cf..138d98b80 100644 --- a/tools/ctrace/src/diagnostics/DiagnosticSink.cpp +++ b/tools/ctrace/src/diagnostics/DiagnosticSink.cpp @@ -43,11 +43,6 @@ std::uint64_t DiagnosticSink::fatalCount() const noexcept return fatalCount_; } -bool DiagnosticSink::hasFatalSince(std::uint64_t checkpoint) const noexcept -{ - return fatalCount_ > checkpoint; -} - std::string_view toString(DiagnosticSink::Severity severity) { switch (severity) { diff --git a/tools/ctrace/src/diagnostics/DiagnosticSink.hpp b/tools/ctrace/src/diagnostics/DiagnosticSink.hpp index a30670b3c..44618de20 100644 --- a/tools/ctrace/src/diagnostics/DiagnosticSink.hpp +++ b/tools/ctrace/src/diagnostics/DiagnosticSink.hpp @@ -60,7 +60,6 @@ class DiagnosticSink { void report(const Event& event); std::uint64_t fatalCount() const noexcept; - bool hasFatalSince(std::uint64_t checkpoint) const noexcept; protected: virtual void write(const Event& event) = 0; diff --git a/tools/ctrace/src/output/OutputRequirements.cpp b/tools/ctrace/src/output/OutputRequirements.cpp index c77f36546..12db99e1d 100644 --- a/tools/ctrace/src/output/OutputRequirements.cpp +++ b/tools/ctrace/src/output/OutputRequirements.cpp @@ -128,22 +128,23 @@ bool validateCtfRouteIdentity(const CtraceRunMeta& ctraceRunMeta, const TraceSel return valid; } -std::optional resolveCtfClock(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, - DiagnosticSink& diagnostics) +struct SelectedClockResolution { + std::optional clockHz; + bool hasRoutes{false}; + bool valid{true}; +}; + +SelectedClockResolution resolveSelectedCtfClock(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, + DiagnosticSink& diagnostics) { - std::vector> routes; + SelectedClockResolution result; for (const auto& [traceBusId, timestamp] : ctraceRunMeta.timestampsByTraceBusId()) { if (!selection.includesStream(traceBusId)) { continue; } - routes.emplace_back(traceBusId, timestamp); - } - - std::optional clockHz; - bool valid = true; - for (const auto& [traceBusId, timestamp] : routes) { + result.hasRoutes = true; if (timestamp.clockError.has_value()) { - valid = false; + result.valid = false; reportRequirementError(diagnostics, "ctf-timestamp-clock-invalid", "CTF output cannot use the configured timestamps.clock", { @@ -156,7 +157,7 @@ std::optional resolveCtfClock(const CtraceRunMeta& ctraceRunMeta, continue; } if (!timestamp.clockHz.has_value()) { - valid = false; + result.valid = false; reportRequirementError(diagnostics, "ctf-timestamp-clock-missing", "CTF output requires timestamps.clock for the processor assigned to this Trace Bus ID", { @@ -168,7 +169,7 @@ std::optional resolveCtfClock(const CtraceRunMeta& ctraceRunMeta, continue; } if (*timestamp.clockHz == 0U) { - valid = false; + result.valid = false; reportRequirementError(diagnostics, "ctf-timestamp-clock-invalid", "CTF output requires timestamps.clock to be greater than zero", { @@ -179,8 +180,8 @@ std::optional resolveCtfClock(const CtraceRunMeta& ctraceRunMeta, }); continue; } - if (clockHz.has_value() && *clockHz != *timestamp.clockHz) { - valid = false; + if (result.clockHz.has_value() && *result.clockHz != *timestamp.clockHz) { + result.valid = false; reportRequirementError(diagnostics, "ctf-timestamp-clock-ambiguous", "CTF output cannot combine selected Trace Bus IDs with different timestamps.clock values", { @@ -191,15 +192,14 @@ std::optional resolveCtfClock(const CtraceRunMeta& ctraceRunMeta, }); continue; } - clockHz = timestamp.clockHz; - } - if (!valid) { - return std::nullopt; - } - if (clockHz.has_value()) { - return clockHz; + result.clockHz = timestamp.clockHz; } + return result; +} +std::optional resolveDefaultCtfClock(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, + DiagnosticSink& diagnostics) +{ for (const auto& error : ctraceRunMeta.timestampClockErrors()) { reportRequirementError(diagnostics, "ctf-timestamp-clock-invalid", "CTF output cannot use the configured timestamps.clock", @@ -243,6 +243,19 @@ std::optional resolveCtfClock(const CtraceRunMeta& ctraceRunMeta, return ctraceRunMeta.timestampClockHz(); } +std::optional resolveCtfClock(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, + DiagnosticSink& diagnostics) +{ + const auto selected = resolveSelectedCtfClock(ctraceRunMeta, selection, diagnostics); + if (!selected.valid) { + return std::nullopt; + } + if (selected.hasRoutes) { + return selected.clockHz; + } + return resolveDefaultCtfClock(ctraceRunMeta, selection, diagnostics); +} + bool validateCtfDwtMetadata(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, DiagnosticSink& diagnostics) { diff --git a/tools/ctrace/src/output/csv/CsvFileOutput.cpp b/tools/ctrace/src/output/csv/CsvFileOutput.cpp index ecd492091..688693c7a 100644 --- a/tools/ctrace/src/output/csv/CsvFileOutput.cpp +++ b/tools/ctrace/src/output/csv/CsvFileOutput.cpp @@ -66,7 +66,7 @@ CsvFileOutput::CsvFileOutput(std::filesystem::path outputFile, TraceSelection se CsvFileOutput::~CsvFileOutput() { try { - abort(); + CsvFileOutput::abort(); } catch (...) { // Destructors cannot report a best-effort cleanup failure. (void)0; @@ -91,7 +91,7 @@ void CsvFileOutput::start() createParentDirectory(outputPath); active_ = true; stream_.clear(); - stream_.open(outputPath, std::ios::out | std::ios::trunc); + stream_.open(outputPath, std::ios::out | std::ios::trunc | std::ios::binary); if (!stream_.is_open()) { abort(); throw std::runtime_error("Failed to open CSV output " + outputPath.string()); diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp index 76df7e34f..a2c7e31bc 100644 --- a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp @@ -174,7 +174,7 @@ CtfBundleOutput::CtfBundleOutput(CtfOutputConfig config, DiagnosticSink* diagnos CtfBundleOutput::~CtfBundleOutput() { try { - abort(); + CtfBundleOutput::abort(); } catch (...) { // Destructors cannot report a best-effort cleanup failure. (void)0; diff --git a/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp b/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp index e763debda..9d068f354 100644 --- a/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp +++ b/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp @@ -162,51 +162,46 @@ static std::string ctfValueFields(std::string_view prefix) return out.str(); } -void CtfMetadataWriter::write(const std::filesystem::path& outputDir, const std::string& uuidString, - std::uint64_t coreClockHz, const std::vector& sources, - const std::vector& observedExceptionNumbers) -{ +struct MetadataSymbols { std::map dwtValueTypes; std::map itmNames; std::map dwtNames; std::map dwtAddressStarts; std::map dwtAddressEnds; +}; +static MetadataSymbols collectMetadataSymbols(const std::vector& sources) +{ + MetadataSymbols symbols; for (const auto& source : sources) { const auto id = source.source; if (source.type == "itm") { if (source.label.has_value()) { - itmNames[id] = *source.label; + symbols.itmNames[id] = *source.label; } continue; } if (source.type != "dwt") { continue; } - dwtValueTypes[id] = source.valueType; + symbols.dwtValueTypes[id] = source.valueType; if (source.label.has_value()) { - dwtNames[id] = *source.label; + symbols.dwtNames[id] = *source.label; } if (source.symbolAddress.has_value()) { - dwtAddressStarts[id] = *source.symbolAddress; + symbols.dwtAddressStarts[id] = *source.symbolAddress; const auto extent = static_cast(source.valueSize - 1U); if (*source.symbolAddress <= std::numeric_limits::max() - extent) { - dwtAddressEnds[id] = *source.symbolAddress + extent; + symbols.dwtAddressEnds[id] = *source.symbolAddress + extent; } } } + return symbols; +} - std::ostringstream exceptionEnum; - for (const auto number : exceptionNumbersWithDefaults(observedExceptionNumbers)) { - exceptionEnum << " " << tsdlString(exceptionName(number)) << " = " << number << ",\n"; - } - - const auto metadataPath = outputDir / "metadata"; - std::ofstream out(metadataPath, std::ios::out | std::ios::trunc); - if (!out) { - throw std::runtime_error("Failed to write CTF metadata " + metadataPath.string()); - } - +static void writeTraceEnvironment(std::ostream& out, const std::string& uuidString, std::uint64_t coreClockHz, + const MetadataSymbols& symbols) +{ out << R"(/* CTF 1.8 */ trace { major = 1; @@ -225,13 +220,13 @@ env { cmsis_ctf_profile = "cmsis.ctf"; cmsis_ctf_profile_version = 1; )"; - for (const auto& entry : dwtValueTypes) { + for (const auto& entry : symbols.dwtValueTypes) { out << " cmsis_dwt" << entry.first << "_value_type = " << tsdlString(entry.second) << ";\n"; } - for (const auto& entry : dwtAddressStarts) { + for (const auto& entry : symbols.dwtAddressStarts) { out << " cmsis_dwt" << entry.first << "_address_start = " << tsdlString("0x" + hexValue(entry.second)) << ";\n"; } - for (const auto& entry : dwtAddressEnds) { + for (const auto& entry : symbols.dwtAddressEnds) { out << " cmsis_dwt" << entry.first << "_address_end = " << tsdlString("0x" + hexValue(entry.second)) << ";\n"; } out << R"(}; @@ -245,7 +240,13 @@ clock { freq = )" << coreClockHz << R"(; }; +)"; +} +static void writeTypeDefinitions(std::ostream& out, const MetadataSymbols& symbols, + const std::vector& observedExceptionNumbers) +{ + out << R"( typealias integer { size = 8; align = 8; signed = false; } := uint8_t; typealias integer { size = 16; align = 8; signed = false; byte_order = le; } := uint16_t; typealias integer { size = 32; align = 8; signed = false; byte_order = le; } := uint32_t; @@ -285,24 +286,32 @@ typealias enum : uint8_t { std::set itmLabels; for (std::uint32_t channel = 1U; channel < 32U; ++channel) { const auto fallback = "ITM" + std::to_string(channel); - out << " " << tsdlString(uniqueEnumLabel(mapValueOrEmpty(itmNames, channel), fallback, itmLabels)) << " = " - << channel << ",\n"; + out << " " << tsdlString(uniqueEnumLabel(mapValueOrEmpty(symbols.itmNames, channel), fallback, itmLabels)) + << " = " << channel << ",\n"; } out << R"(} := cmsis_itm_channel_t; typealias enum : uint8_t { )"; std::set dwtLabels; for (std::uint32_t comparator = 0U; comparator < 4U; ++comparator) { - const auto label = mapValueOrEmpty(dwtNames, comparator); + const auto label = mapValueOrEmpty(symbols.dwtNames, comparator); const auto fallback = "DWT" + std::to_string(comparator); out << " " << tsdlString(uniqueEnumLabel(label, fallback, dwtLabels)) << " = " << comparator << ",\n"; } out << R"( } := cmsis_dwt_comparator_t; typealias enum : uint16_t { -)" << exceptionEnum.str() - << R"(} := cmsis_exception_number_t; +)"; + for (const auto number : exceptionNumbersWithDefaults(observedExceptionNumbers)) { + out << " " << tsdlString(exceptionName(number)) << " = " << number << ",\n"; + } + out << R"(} := cmsis_exception_number_t; +)"; +} +static void writeStreamDefinition(std::ostream& out) +{ + out << R"( stream { id = )" << CtfSchema::SwoStreamId << R"(; @@ -322,7 +331,12 @@ stream { uint32_t packet_seq_num; }; }; +)"; +} +static void writeItmEvent(std::ostream& out) +{ + out << R"( event { id = )" << CtfSchema::value(CtfSchema::EventId::Itm) << R"(; @@ -337,7 +351,12 @@ event { uint32_t cmsis_overflow_count; }; }; +)"; +} +static void writeDwtValueEvent(std::ostream& out) +{ + out << R"( event { id = )" << CtfSchema::value(CtfSchema::EventId::DwtValue) << R"(; @@ -357,7 +376,12 @@ event { uint32_t cmsis_overflow_count; }; }; +)"; +} +static void writeDwtAddressEvent(std::ostream& out) +{ + out << R"( event { id = )" << CtfSchema::value(CtfSchema::EventId::DwtAddress) << R"(; @@ -375,7 +399,12 @@ event { uint32_t cmsis_overflow_count; }; }; +)"; +} +static void writeStatusEvents(std::ostream& out) +{ + out << R"( event { id = )" << CtfSchema::value(CtfSchema::EventId::TraceStatus) << R"(; @@ -416,6 +445,26 @@ event { }; }; )"; +} + +void CtfMetadataWriter::write(const std::filesystem::path& outputDir, const std::string& uuidString, + std::uint64_t coreClockHz, const std::vector& sources, + const std::vector& observedExceptionNumbers) +{ + const auto metadataPath = outputDir / "metadata"; + std::ofstream out(metadataPath, std::ios::out | std::ios::trunc); + if (!out) { + throw std::runtime_error("Failed to write CTF metadata " + metadataPath.string()); + } + + const auto symbols = collectMetadataSymbols(sources); + writeTraceEnvironment(out, uuidString, coreClockHz, symbols); + writeTypeDefinitions(out, symbols, observedExceptionNumbers); + writeStreamDefinition(out); + writeItmEvent(out); + writeDwtValueEvent(out); + writeDwtAddressEvent(out); + writeStatusEvents(out); out.close(); if (!out) { throw std::runtime_error("Failed to write CTF metadata " + metadataPath.string()); diff --git a/tools/ctrace/src/output/ctf/CtfStreamWriter.cpp b/tools/ctrace/src/output/ctf/CtfStreamWriter.cpp index b90802362..ed5e242b0 100644 --- a/tools/ctrace/src/output/ctf/CtfStreamWriter.cpp +++ b/tools/ctrace/src/output/ctf/CtfStreamWriter.cpp @@ -184,7 +184,7 @@ const std::string& CtfStreamWriter::uuidString() const noexcept void CtfStreamWriter::beginPacket() { - std::fill(packetBuffer_.begin(), packetBuffer_.end(), 0U); + std::fill(packetBuffer_.begin(), packetBuffer_.end(), std::uint8_t{0}); contentOffset_ = kPacketOverhead; eventCount_ = 0U; timestampBegin_ = 0U; diff --git a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp index 6a96f1b62..307be4f59 100644 --- a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp +++ b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp @@ -46,68 +46,44 @@ static std::string withTraceCompassAnalysisVersion(std::string xml) return xml; } -void TraceCompassXmlWriter::writeFile(const std::filesystem::path& filePath) +static std::string valueHandlers(CtfSchema::EventId eventId, const char* prefix, const char* routeField, + const char* valueAttribute) { - std::ostringstream dwtHandlers; + std::ostringstream handlers; for (const auto& arm : CtfSchema::ValueVariants) { - dwtHandlers << R"( + handlers << R"( - + + << arm.name << R"(" /> - - - - - -)"; - } - - std::ostringstream itmHandlers; - for (const auto& arm : CtfSchema::ValueVariants) { - itmHandlers << R"( - - - - - - - + << CtfSchema::eventName(eventId) << R"(" /> + - - - + << valueAttribute << R"(" /> + )"; } + return handlers.str(); +} - if (!filePath.parent_path().empty()) { - std::filesystem::create_directories(filePath.parent_path()); - } - std::ofstream out(filePath, std::ios::out | std::ios::trunc); - if (!out) { - throw std::runtime_error("Failed to write Trace Compass XML " + filePath.string()); - } +static std::string stateProviderXml() +{ std::ostringstream xml; - xml << R"( - - + xml << R"( - +)"; + return xml.str(); +} + +static std::string viewsXml() +{ + std::ostringstream xml; + xml << R"(