diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cd879b..0ea3012 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,7 +54,8 @@ add_custom_target(panopainter_modernization_status add_library(pp_foundation STATIC src/foundation/binary_stream.cpp - src/foundation/parse.cpp) + src/foundation/parse.cpp + src/foundation/trace.cpp) target_include_directories(pp_foundation PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index fc86906..e6910ba 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -64,7 +64,7 @@ ctest --preset desktop-fast --build-config Debug powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli cmake --preset android-arm64 -cmake --build --preset android-arm64 --target pp_foundation pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests +powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64 ``` Known local toolchain state: @@ -73,7 +73,8 @@ Known local toolchain state: - Local Visual Studio generator selected by CMake: Visual Studio 17 2022 - Android SDK: `C:\Users\omara\AppData\Local\Android\Sdk` - Android NDK: `C:\Users\omara\AppData\Local\Android\Sdk\ndk\29.0.14206865` -- Android arm64 headless configure/build passes through root CMake. +- Android arm64 headless configure/build passes through root CMake and the + `platform-build` automation wrapper. - `vcpkg` is not on PATH yet; see DEBT-0007. Known warnings after the current CMake app build: diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index b626d8e..1a742b1 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -28,7 +28,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0006 | Open | Modernization | `pano_cli create-document` validates and emits JSON command contracts but does not yet invoke the legacy document/app model | The document model has not been extracted from `Canvas`/`App` yet | `pano_cli create-document --width 64 --height 32 --layers 2`; CTest `pano_cli_create_document_smoke` | Replace command contract implementation with real `pp_document` creation once Phase 4 extracts the document model | | DEBT-0007 | Open | Modernization | `vcpkg.json` exists but CMake is not yet using a validated vcpkg toolchain on this machine | `vcpkg` is not available on PATH and Visual Studio reports manifest mode is disabled | `cmake --preset windows-msvc-default` currently configures with vendored dependencies | Add validated vcpkg toolchain/preset integration for desktop, Android, and Apple triplets | | DEBT-0008 | Open | Modernization | `windows-msvc-default` preset is used for local validation because the VS 2026 generator is not installed here | The target VS 2026 preset must remain, but this machine configures with Visual Studio 17 2022 | `cmake --preset windows-msvc-default`; `ctest --preset desktop-fast --build-config Debug` | Validate `windows-vs2026-x64` on a machine with Visual Studio 2026 installed and make it the default Windows validation preset | -| DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, not APK/package variants | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `cmake --preset android-arm64`; `cmake --build --preset android-arm64 --target pp_foundation pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands | +| DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, not APK/package variants | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index c5ca775..588d9e9 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -242,9 +242,10 @@ Gate: Goal: make each component reachable by automated tools and future agents. Status: in progress. `tests/` exists, `desktop-fast` runs headlessly, and -PowerShell/bash wrappers exist for configure/build/test/analyze. `pano_cli` -exists with a first JSON automation command for validating create-document -inputs; full document/app integration is debt-tracked as DEBT-0006. +PowerShell/bash wrappers exist for configure/build/test/analyze/platform-build. +`pano_cli` exists with a first JSON automation command for validating +create-document inputs; full document/app integration is debt-tracked as +DEBT-0006. Implementation tasks: @@ -300,8 +301,9 @@ Goal: split libraries while keeping current app behavior. Status: started. `pp_foundation` exists with binary stream utilities and boundary/overread tests. It also owns strict decimal `uint32` parsing used by `pano_cli`, with rejection tests for empty, signed, mixed, and overflowing -input. Continue extracting legacy-safe utilities before moving assets, paint, -or document behavior. +input. A deterministic `TraceRecorder` now records component/name/thread/frame +and stroke timing spans with invalid-end tests. Continue extracting legacy-safe +utilities before moving assets, paint, or document behavior. Implementation tasks: @@ -501,25 +503,26 @@ Last verified on 2026-05-31: ```powershell cmake --preset windows-msvc-default -cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_parse_tests pano_cli PanoPainter +cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pano_cli PanoPainter ctest --preset desktop-fast --build-config Debug powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli cmake --preset android-arm64 -cmake --build --preset android-arm64 --target pp_foundation pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests +powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64 ``` Results: - `pp_foundation_binary_stream_tests` passed. - `pp_foundation_parse_tests` passed. +- `pp_foundation_trace_tests` passed. - `pano_cli_create_document_smoke` passed. - `PanoPainter.exe` built through CMake at `out/build/windows-msvc-default/Debug/PanoPainter.exe`. - PowerShell build/test automation wrappers return JSON summaries and passed local smoke checks. -- Android arm64 configured with NDK 29.0.14206865 and compiled headless - foundation/tool/test targets. +- Android arm64 configured with NDK 29.0.14206865 through the platform-build + wrapper and compiled headless foundation/tool/test targets. - Known remaining warnings: legacy project/vendor diagnostics, Visual Studio vcpkg-manifest warning, `LNK4099` missing libyuv PDBs, and `LNK4098` runtime library conflict from retained vendor binaries. diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 new file mode 100644 index 0000000..1e6b731 --- /dev/null +++ b/scripts/automation/platform-build.ps1 @@ -0,0 +1,52 @@ +[CmdletBinding()] +param( + [string[]]$Presets = @("android-arm64"), + [string[]]$Targets = @("pp_foundation", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_parse_tests", "pp_foundation_trace_tests") +) + +$ErrorActionPreference = "Stop" +$started = Get-Date +$results = @() +$overallExitCode = 0 + +foreach ($preset in $Presets) { + & cmake --preset $preset + $configureExitCode = $LASTEXITCODE + if ($configureExitCode -ne 0) { + $overallExitCode = $configureExitCode + $results += [ordered]@{ + preset = $preset + stage = "configure" + exitCode = $configureExitCode + } + continue + } + + $buildArgs = @("--build", "--preset", $preset) + foreach ($target in $Targets) { + $buildArgs += @("--target", $target) + } + + & cmake @buildArgs + $buildExitCode = $LASTEXITCODE + if ($buildExitCode -ne 0 -and $overallExitCode -eq 0) { + $overallExitCode = $buildExitCode + } + + $results += [ordered]@{ + preset = $preset + stage = "build" + targets = $Targets + exitCode = $buildExitCode + } +} + +$elapsed = [int]((Get-Date) - $started).TotalMilliseconds +[ordered]@{ + command = "platform-build" + exitCode = $overallExitCode + elapsedMs = $elapsed + results = $results +} | ConvertTo-Json -Compress -Depth 6 + +exit $overallExitCode diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh new file mode 100644 index 0000000..a4a9c2c --- /dev/null +++ b/scripts/automation/platform-build.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env sh +set -u + +preset="${1:-android-arm64}" +shift || true +targets="${*:-pp_foundation pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests}" +start="$(date +%s)" + +cmake --preset "$preset" +configure_exit="$?" +if [ "$configure_exit" -ne 0 ]; then + end="$(date +%s)" + elapsed_ms="$(( (end - start) * 1000 ))" + printf '{"command":"platform-build","preset":"%s","stage":"configure","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configure_exit" "$elapsed_ms" + exit "$configure_exit" +fi + +build_args="" +for target in $targets; do + build_args="$build_args --target $target" +done + +# shellcheck disable=SC2086 +cmake --build --preset "$preset" $build_args +build_exit="$?" +end="$(date +%s)" +elapsed_ms="$(( (end - start) * 1000 ))" +printf '{"command":"platform-build","preset":"%s","targets":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$targets" "$build_exit" "$elapsed_ms" +exit "$build_exit" diff --git a/src/foundation/trace.cpp b/src/foundation/trace.cpp new file mode 100644 index 0000000..73d6910 --- /dev/null +++ b/src/foundation/trace.cpp @@ -0,0 +1,98 @@ +#include "foundation/trace.h" + +#include + +namespace pp::foundation { + +Result TraceRecorder::begin_span(TraceSpanDesc desc, std::uint64_t start_us) +{ + if (desc.component.empty()) { + return Result::failure( + Status::invalid_argument("trace component must not be empty")); + } + + if (desc.name.empty()) { + return Result::failure( + Status::invalid_argument("trace span name must not be empty")); + } + + if (next_id_ == std::numeric_limits::max()) { + return Result::failure( + Status::out_of_range("trace span id space is exhausted")); + } + + const auto id = next_id_++; + ActiveSpan span; + span.id = id; + span.component.assign(desc.component); + span.name.assign(desc.name); + span.desc = desc; + span.desc.component = span.component; + span.desc.name = span.name; + span.start_us = start_us; + span.active = true; + active_spans_.push_back(span); + + return Result::success(id); +} + +Status TraceRecorder::end_span(TraceSpanId id, std::uint64_t end_us) +{ + ActiveSpan* span = find_active_span(id); + if (span == nullptr) { + return Status::out_of_range("trace span id is not active"); + } + + if (end_us < span->start_us) { + return Status::invalid_argument("trace span cannot end before it starts"); + } + + TraceEvent event; + event.component = span->component; + event.name = span->name; + event.thread_id = span->desc.thread_id; + event.frame_id = span->desc.frame_id; + event.stroke_id = span->desc.stroke_id; + event.start_us = span->start_us; + event.duration_us = end_us - span->start_us; + events_.push_back(event); + + span->active = false; + return Status::success(); +} + +std::span TraceRecorder::events() const noexcept +{ + return events_; +} + +std::size_t TraceRecorder::active_span_count() const noexcept +{ + std::size_t count = 0; + for (const auto& span : active_spans_) { + if (span.active) { + ++count; + } + } + return count; +} + +void TraceRecorder::clear() noexcept +{ + active_spans_.clear(); + events_.clear(); + next_id_ = 1; +} + +TraceRecorder::ActiveSpan* TraceRecorder::find_active_span(TraceSpanId id) noexcept +{ + for (auto& span : active_spans_) { + if (span.active && span.id == id) { + return &span; + } + } + + return nullptr; +} + +} diff --git a/src/foundation/trace.h b/src/foundation/trace.h new file mode 100644 index 0000000..846700c --- /dev/null +++ b/src/foundation/trace.h @@ -0,0 +1,60 @@ +#pragma once + +#include "foundation/result.h" + +#include +#include +#include +#include +#include + +namespace pp::foundation { + +using TraceSpanId = std::uint64_t; + +struct TraceSpanDesc { + std::string_view component; + std::string_view name; + std::uint64_t thread_id = 0; + std::uint64_t frame_id = 0; + std::uint64_t stroke_id = 0; +}; + +struct TraceEvent { + std::string component; + std::string name; + std::uint64_t thread_id = 0; + std::uint64_t frame_id = 0; + std::uint64_t stroke_id = 0; + std::uint64_t start_us = 0; + std::uint64_t duration_us = 0; +}; + +class TraceRecorder { +public: + [[nodiscard]] Result begin_span(TraceSpanDesc desc, std::uint64_t start_us); + [[nodiscard]] Status end_span(TraceSpanId id, std::uint64_t end_us); + + [[nodiscard]] std::span events() const noexcept; + [[nodiscard]] std::size_t active_span_count() const noexcept; + + void clear() noexcept; + +private: + struct ActiveSpan { + TraceSpanId id = 0; + TraceSpanDesc desc; + std::string component; + std::string name; + std::uint64_t start_us = 0; + bool active = false; + }; + + [[nodiscard]] ActiveSpan* find_active_span(TraceSpanId id) noexcept; + + std::vector active_spans_; + std::vector events_; + TraceSpanId next_id_ = 1; +}; + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ee416a0..30c47fb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,6 +26,16 @@ add_test(NAME pp_foundation_parse_tests COMMAND pp_foundation_parse_tests) set_tests_properties(pp_foundation_parse_tests PROPERTIES LABELS "foundation;desktop-fast") +add_executable(pp_foundation_trace_tests + foundation/trace_tests.cpp) +target_link_libraries(pp_foundation_trace_tests PRIVATE + pp_foundation + pp_test_harness) + +add_test(NAME pp_foundation_trace_tests COMMAND pp_foundation_trace_tests) +set_tests_properties(pp_foundation_trace_tests PROPERTIES + LABELS "foundation;desktop-fast") + if(TARGET pano_cli) add_test(NAME pano_cli_create_document_smoke COMMAND pano_cli create-document --width 64 --height 32 --layers 2) diff --git a/tests/foundation/trace_tests.cpp b/tests/foundation/trace_tests.cpp new file mode 100644 index 0000000..69659d7 --- /dev/null +++ b/tests/foundation/trace_tests.cpp @@ -0,0 +1,109 @@ +#include "foundation/trace.h" +#include "test_harness.h" + +using pp::foundation::StatusCode; +using pp::foundation::TraceRecorder; +using pp::foundation::TraceSpanDesc; + +namespace { + +void records_completed_spans_with_context(pp::tests::Harness& h) +{ + TraceRecorder recorder; + + const auto id = recorder.begin_span( + TraceSpanDesc { + .component = "paint", + .name = "stroke_commit", + .thread_id = 7, + .frame_id = 11, + .stroke_id = 13, + }, + 100); + + PP_EXPECT(h, id.ok()); + PP_EXPECT(h, recorder.active_span_count() == 1U); + PP_EXPECT(h, recorder.end_span(id.value(), 145).ok()); + PP_EXPECT(h, recorder.active_span_count() == 0U); + PP_EXPECT(h, recorder.events().size() == 1U); + + const auto& event = recorder.events()[0]; + PP_EXPECT(h, event.component == "paint"); + PP_EXPECT(h, event.name == "stroke_commit"); + PP_EXPECT(h, event.thread_id == 7U); + PP_EXPECT(h, event.frame_id == 11U); + PP_EXPECT(h, event.stroke_id == 13U); + PP_EXPECT(h, event.start_us == 100U); + PP_EXPECT(h, event.duration_us == 45U); +} + +void rejects_invalid_span_descriptions(pp::tests::Harness& h) +{ + TraceRecorder recorder; + + const auto no_component = recorder.begin_span( + TraceSpanDesc { .component = "", .name = "load" }, + 1); + const auto no_name = recorder.begin_span( + TraceSpanDesc { .component = "assets", .name = "" }, + 1); + + PP_EXPECT(h, !no_component.ok()); + PP_EXPECT(h, no_component.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !no_name.ok()); + PP_EXPECT(h, no_name.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, recorder.events().empty()); +} + +void rejects_bad_end_calls_without_recording_events(pp::tests::Harness& h) +{ + TraceRecorder recorder; + const auto id = recorder.begin_span( + TraceSpanDesc { .component = "renderer", .name = "readback" }, + 50); + + PP_EXPECT(h, id.ok()); + + const auto backwards = recorder.end_span(id.value(), 49); + PP_EXPECT(h, !backwards.ok()); + PP_EXPECT(h, backwards.code == StatusCode::invalid_argument); + PP_EXPECT(h, recorder.active_span_count() == 1U); + PP_EXPECT(h, recorder.events().empty()); + + PP_EXPECT(h, recorder.end_span(id.value(), 51).ok()); + const auto duplicate = recorder.end_span(id.value(), 52); + PP_EXPECT(h, !duplicate.ok()); + PP_EXPECT(h, duplicate.code == StatusCode::out_of_range); + PP_EXPECT(h, recorder.events().size() == 1U); +} + +void clear_resets_events_and_span_ids(pp::tests::Harness& h) +{ + TraceRecorder recorder; + const auto first = recorder.begin_span( + TraceSpanDesc { .component = "ui", .name = "layout" }, + 10); + PP_EXPECT(h, first.ok()); + PP_EXPECT(h, recorder.end_span(first.value(), 20).ok()); + recorder.clear(); + + const auto second = recorder.begin_span( + TraceSpanDesc { .component = "ui", .name = "layout" }, + 30); + PP_EXPECT(h, second.ok()); + PP_EXPECT(h, second.value() == first.value()); + PP_EXPECT(h, recorder.events().empty()); + PP_EXPECT(h, recorder.active_span_count() == 1U); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("records_completed_spans_with_context", records_completed_spans_with_context); + harness.run("rejects_invalid_span_descriptions", rejects_invalid_span_descriptions); + harness.run("rejects_bad_end_calls_without_recording_events", rejects_bad_end_calls_without_recording_events); + harness.run("clear_resets_events_and_span_ids", clear_resets_events_and_span_ids); + return harness.finish(); +}