Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 135 additions & 2 deletions .github/workflows/nightly-slow-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ name: Nightly slow checks
# `github.event_name` is `schedule`/`workflow_dispatch`, never `push`, so the
# unmodified "Save sccache" `if:` condition they inherit is always false and
# they only ever restore what the last push already wrote.
#
# A third job, `tracy-capture`, is not a copy of anything in ci.yml: it is
# the check that morph's profiler zones fire under a real capture. See its
# own comment.

on:
schedule:
Expand Down Expand Up @@ -309,9 +313,132 @@ jobs:
path: /tmp/failure-excerpt-all-features-${{ matrix.compiler }}.txt
retention-days: 7

# ── Tracy capture: the profiler zones fire ────────────────────────────
# Builds the benchmarks with MORPH_ENABLE_TRACY=ON, runs each under
# tracy-capture, exports the zone statistics with tracy-csvexport, and
# fails unless every named zone has a non-zero count. A capture that
# "succeeds" with no zones would prove nothing, so
# scripts/check_tracy_capture.sh fails closed: no CSV, no rows, a zone
# missing or counted zero, or a capture that never connected is a failure.
#
# morph_bench drives RemoteServer's dispatch (the wire codec, the
# dispatcher, the model strands); morph_bench_alloc drives Bridge over
# LocalBackend. Between them they reach the zones named below. Nightly,
# not on pull_request: it builds Tracy's tools and a release morph, and a
# missing zone is a slow regression, not one a PR needs to wait on.
#
# MORPH_TRACY_ON_DEMAND=OFF and TRACY_NO_EXIT=1 make the result
# independent of when tracy-capture connects: the client records from the
# first zone and waits, at exit, until the capture has drained it.
tracy-capture:
name: Tracy capture (nightly)
runs-on: ubuntu-24.04
env:
TRACY_TAG: v0.13.1
steps:
- uses: actions/checkout@v4

- name: Install clang, ninja, catch2
run: |
sudo apt-get update -q
sudo apt-get install -y ninja-build catch2
curl -sSL --fail -o /tmp/llvm.sh https://apt.llvm.org/llvm.sh
test -s /tmp/llvm.sh
sudo bash /tmp/llvm.sh ${{ env.CLANG_VERSION }}

- name: Self-test the capture checker
run: bash scripts/test_check_tracy_capture.sh

- name: Restore Tracy's tools
id: tracy-tools
uses: actions/cache@v4
with:
path: /home/runner/tracy-tools
key: tracy-tools-${{ env.TRACY_TAG }}-ubuntu-24.04-clang-${{ env.CLANG_VERSION }}

# The tag morph's own CMake fetches the client at, so the capture
# protocol matches the client the benchmarks link.
- name: Build tracy-capture and tracy-csvexport
if: steps.tracy-tools.outputs.cache-hit != 'true'
run: |
git clone --depth 1 --branch "$TRACY_TAG" https://github.com/wolfpld/tracy.git /tmp/tracy
for tool in capture csvexport; do
cmake -S "/tmp/tracy/${tool}" -B "/tmp/tracy-build/${tool}" -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DNO_ISA_EXTENSIONS=ON \
-DCMAKE_C_COMPILER=clang-${{ env.CLANG_VERSION }} \
-DCMAKE_CXX_COMPILER=clang++-${{ env.CLANG_VERSION }}
cmake --build "/tmp/tracy-build/${tool}"
done
mkdir -p /home/runner/tracy-tools
cp /tmp/tracy-build/capture/tracy-capture /tmp/tracy-build/csvexport/tracy-csvexport \
/home/runner/tracy-tools/

- name: Configure (Tracy on, recording from the first zone)
run: |
cmake -S . -B build/tracy -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang-${{ env.CLANG_VERSION }} \
-DCMAKE_CXX_COMPILER=clang++-${{ env.CLANG_VERSION }} \
-DMORPH_ENABLE_TRACY=ON \
-DMORPH_TRACY_ON_DEMAND=OFF \
-DMORPH_BUILD_LOAD_TESTS=ON \
-DMORPH_BUILD_EXAMPLES=OFF

- name: Build the benchmarks
run: cmake --build build/tracy --target morph_bench morph_bench_alloc -- -j 4

- name: Capture morph_bench (RemoteServer dispatch)
run: |
bash scripts/check_tracy_capture.sh capture \
/home/runner/tracy-tools/tracy-capture /home/runner/tracy-tools/tracy-csvexport \
build/tracy/capture \
"RemoteServer::dispatchMessage,RemoteServer::dispatchExecute,RemoteServer::startRemote,RemoteServer::finishRemote,wire::encode,wire::decode,ModelStrands::task,ActionDispatcher::dispatch,ActionDispatcher::prepareAction" \
-- build/tracy/tests/bench/morph_bench

- name: Capture morph_bench_alloc (Bridge over LocalBackend)
run: |
bash scripts/check_tracy_capture.sh capture \
/home/runner/tracy-tools/tracy-capture /home/runner/tracy-tools/tracy-csvexport \
build/tracy/capture \
"Bridge::executeVia,Bridge::dispatchNow,LocalBackend::executeInto,LocalBackend::startLocal,LocalBackend::finishLocal,BridgeSink::settleValue,ModelStrands::task" \
-- build/tracy/tests/bench/morph_bench_alloc

- name: Upload the zone tables
if: always()
uses: actions/upload-artifact@v4
with:
name: tracy-capture-zones
path: |
build/tracy/capture/*.csv
build/tracy/capture/*.log
if-no-files-found: ignore
retention-days: 14

- name: Capture a failure excerpt for the issue filer
if: failure()
run: |
{
echo "Job: Tracy capture (nightly)"
echo "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "Commit: ${{ github.sha }}"
for log in build/tracy/capture/*.log; do
[ -f "$log" ] || continue
echo "── ${log} (last 30 lines) ──"
tail -n 30 "$log"
done
} > /tmp/failure-excerpt-tracy-capture.txt

- name: Upload failure excerpt
if: failure()
uses: actions/upload-artifact@v4
with:
name: failure-excerpt-tracy-capture
path: /tmp/failure-excerpt-tracy-capture.txt
retention-days: 7

# ── Reconcile GitHub issues against tonight's result ──────────────────
#
# Runs regardless of the two jobs above (`if: always()`), because a
# Runs regardless of the jobs above (`if: always()`), because a
# recovery (a previously-red job going green) has to close a stale issue
# just as reliably as a new failure has to open one -- an `if: failure()`
# gate here would only ever open issues and never close them.
Expand All @@ -330,7 +457,7 @@ jobs:
# same auth every workflow already has; no new secret.
reconcile-issues:
name: File or close issues for tonight's result
needs: [valgrind, linux-all-features]
needs: [valgrind, linux-all-features, tracy-capture]
if: always()
runs-on: ubuntu-24.04
permissions:
Expand Down Expand Up @@ -362,6 +489,9 @@ jobs:
gh label create "nightly-check:all-features" --repo "$REPO" \
--description "Auto-filed by nightly-slow-checks.yml for the all-optional-features leg" \
--color BFDADC 2>/dev/null || true
gh label create "nightly-check:tracy-capture" --repo "$REPO" \
--description "Auto-filed by nightly-slow-checks.yml for the Tracy capture leg" \
--color BFDADC 2>/dev/null || true

- name: Reconcile
env:
Expand All @@ -371,6 +501,7 @@ jobs:
SHA: ${{ github.sha }}
VALGRIND_RESULT: ${{ needs.valgrind.result }}
ALL_FEATURES_RESULT: ${{ needs.linux-all-features.result }}
TRACY_CAPTURE_RESULT: ${{ needs.tracy-capture.result }}
run: |
set -euo pipefail

Expand Down Expand Up @@ -451,3 +582,5 @@ jobs:
"/tmp/excerpts/failure-excerpt-valgrind.txt"
reconcile_one "nightly-check:all-features" "Linux / all optional features" "$ALL_FEATURES_RESULT" \
"/tmp/excerpts/failure-excerpt-all-features-clang.txt"
reconcile_one "nightly-check:tracy-capture" "Tracy capture" "$TRACY_CAPTURE_RESULT" \
"/tmp/excerpts/failure-excerpt-tracy-capture.txt"
62 changes: 62 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,63 @@ if(NOT glaze_FOUND)
OPTIONS "glaze_ENABLE_TESTS OFF")
endif()

# ── Tracy (optional) ────────────────────────────────────────────────────────
# MORPH_ENABLE_TRACY turns morph's MORPH_ZONE/MORPH_PLOT/... macros
# (include/morph/core/profiler.hpp) into Tracy calls. Found first, as glaze
# is, and fetched only when no installed Tracy is found.
#
# Before core-cpp, because core-cpp has Tracy instrumentation of its own
# (CORE_CPP_WITH_TRACY) and, with morph passing CORE_CPP_FETCH_DEPS OFF, takes
# the Tracy::TracyClient its parent has already provided. Added after it, a
# build with both on would have no client for core-cpp to find.
#
# The tag and the CPM name are Lightweight's, deliberately: an application
# that links both libraries must end up with exactly one TracyClient, because
# two copies of the client -- let alone two versions -- in one process do not
# work. With the same NAME, whichever of the two adds Tracy first fetches it
# and the other's CPMAddPackage reuses that copy; with the same tag, neither
# side is handed a version it did not ask for. TRACY_ON_DEMAND is
# Lightweight's choice too, and the one a library should make: without it the
# client buffers every event from process start until a profiler connects,
# which in a process nobody profiles is a leak. MORPH_TRACY_ON_DEMAND=OFF is
# for a capture that must see a program from its first zone -- the nightly
# capture job -- and, like the tag, applies only when morph is the one that
# fetches Tracy. It is a morph option rather than TRACY_ON_DEMAND itself
# because CPM passes OPTIONS as normal variables, which a -D on the command
# line cannot override.
#
# The define and the link are INTERFACE: morph is header-only, so the macros
# expand in the consumer's own translation units, and every one of them has
# to agree on MORPH_TRACY_ENABLED (see profiler.hpp on why that is an ODR
# matter).
option(MORPH_ENABLE_TRACY "Instrument morph's hot paths with Tracy profiler zones (fetches Tracy if not installed)" OFF)
option(MORPH_TRACY_ON_DEMAND "Build a fetched Tracy client that records only while a profiler is connected" ON)
mark_as_advanced(MORPH_TRACY_ON_DEMAND)
set(MORPH_TRACY_GIT_TAG v0.13.1)
if(MORPH_ENABLE_TRACY)
find_package(Tracy CONFIG QUIET)
if(NOT Tracy_FOUND AND NOT TARGET Tracy::TracyClient)
morph_use_cpm(Tracy)
CPMAddPackage(
NAME tracy
GITHUB_REPOSITORY wolfpld/tracy
GIT_TAG ${MORPH_TRACY_GIT_TAG}
EXCLUDE_FROM_ALL YES
SYSTEM YES
OPTIONS "TRACY_ENABLE ON" "TRACY_ON_DEMAND ${MORPH_TRACY_ON_DEMAND}")
endif()
# A TracyClient built without TRACY_ENABLE compiles every Tracy macro to
# nothing: the build would report profiling on and record no zone at all.
get_target_property(_morph_tracy_defs Tracy::TracyClient INTERFACE_COMPILE_DEFINITIONS)
if(NOT "${_morph_tracy_defs}" MATCHES "TRACY_ENABLE")
message(FATAL_ERROR
"MORPH_ENABLE_TRACY=ON, but the Tracy::TracyClient this configure found does not define "
"TRACY_ENABLE in its interface, so every zone would compile to nothing. Use a Tracy "
"built with TRACY_ENABLE=ON, or let morph fetch ${MORPH_TRACY_GIT_TAG}.")
endif()
unset(_morph_tracy_defs)
endif()

# ── core-cpp ────────────────────────────────────────────────────────────────
# The shared C++23 foundation of the Contour Terminal projects: morph takes
# its event loop and timers, base64 and the wakeup primitive from it. Only the
Expand Down Expand Up @@ -310,6 +367,10 @@ endif()
if(MORPH_CLIENT_ONLY)
target_compile_definitions(morph INTERFACE MORPH_CLIENT_ONLY)
endif()
if(MORPH_ENABLE_TRACY)
target_compile_definitions(morph INTERFACE MORPH_TRACY_ENABLED)
target_link_libraries(morph INTERFACE Tracy::TracyClient)
endif()

set_target_properties(morph PROPERTIES
VERIFY_INTERFACE_HEADER_SETS ON
Expand All @@ -324,6 +385,7 @@ target_sources(morph
include/morph/attributes.hpp
include/morph/core/logger.hpp
include/morph/core/observability.hpp
include/morph/core/profiler.hpp
include/morph/core/executor.hpp
include/morph/core/strand.hpp
include/morph/core/owner_strand.hpp
Expand Down
7 changes: 7 additions & 0 deletions cmake/morphConfig.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ if(NOT WIN32 AND NOT EMSCRIPTEN)
find_dependency(Threads)
endif()

# An install built with MORPH_ENABLE_TRACY=ON carries MORPH_TRACY_ENABLED and
# Tracy::TracyClient in morph::morph's interface, so the consumer links the
# same one client the build did.
if(@MORPH_ENABLE_TRACY@)
find_dependency(Tracy CONFIG)
endif()

# Optional components, each present only if its MORPH_BUILD_* option was on
# for the build that produced this install. A component that was not built is
# absent here, so find_package(morph COMPONENTS qt REQUIRED) fails with
Expand Down
2 changes: 2 additions & 0 deletions docs/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ set(DOXYGEN_EXCLUDE_SYMBOLS
"morph::log::detail::*"
"morph::observe::detail"
"morph::observe::detail::*"
"morph::profiler::detail"
"morph::profiler::detail::*"
"morph::exec::detail"
"morph::exec::detail::*"
"morph::async::detail"
Expand Down
3 changes: 2 additions & 1 deletion docs/spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ behavioural differences between the two, collected in one table, are in
[`error_handling.md`](error_handling.md) ·
[`core/post_commit_tail.md`](core/post_commit_tail.md) ·
[`core/logger.md`](core/logger.md) ·
[`core/observability.md`](core/observability.md)
[`core/observability.md`](core/observability.md) ·
[`core/profiler.md`](core/profiler.md)

**Working offline**
[`offline/offline.md`](offline/offline.md) ·
Expand Down
4 changes: 4 additions & 0 deletions docs/spec/core/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,7 @@ See [backend.md](backend.md)'s `RemoteServer` API reference table.
`reconnectOutcome` metrics report on.
- [session.md](../session/session.md) — `Context::requestId`, reused as the
trace correlation id.
- [profiler.md](profiler.md) — compile-time Tracy instrumentation behind
`MORPH_ENABLE_TRACY`. A build option rather than a sink: it is not wired
through this seam, and it links the phases of one call by the same
`requestId`.
Loading
Loading