Add foundation tracing and platform build wrapper

This commit is contained in:
2026-05-31 23:51:41 +02:00
parent e0ea4597e6
commit ec5ecbdb54
10 changed files with 376 additions and 13 deletions

View File

@@ -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")

View File

@@ -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:

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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"

98
src/foundation/trace.cpp Normal file
View File

@@ -0,0 +1,98 @@
#include "foundation/trace.h"
#include <limits>
namespace pp::foundation {
Result<TraceSpanId> TraceRecorder::begin_span(TraceSpanDesc desc, std::uint64_t start_us)
{
if (desc.component.empty()) {
return Result<TraceSpanId>::failure(
Status::invalid_argument("trace component must not be empty"));
}
if (desc.name.empty()) {
return Result<TraceSpanId>::failure(
Status::invalid_argument("trace span name must not be empty"));
}
if (next_id_ == std::numeric_limits<TraceSpanId>::max()) {
return Result<TraceSpanId>::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<TraceSpanId>::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<const TraceEvent> 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;
}
}

60
src/foundation/trace.h Normal file
View File

@@ -0,0 +1,60 @@
#pragma once
#include "foundation/result.h"
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <vector>
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<TraceSpanId> begin_span(TraceSpanDesc desc, std::uint64_t start_us);
[[nodiscard]] Status end_span(TraceSpanId id, std::uint64_t end_us);
[[nodiscard]] std::span<const TraceEvent> 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<ActiveSpan> active_spans_;
std::vector<TraceEvent> events_;
TraceSpanId next_id_ = 1;
};
}

View File

@@ -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)

View File

@@ -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();
}