Compare commits

..

58 Commits

Author SHA1 Message Date
Bartosz Taudul
538af5c89c Learn the actual port of an adopted listen socket.
Adopting a pre-bound socket via SetReservedListenSocket left Worker()'s
port bookkeeping untouched, so GetPort() and the broadcast announcement
reported the computed default instead of the port the socket listens on.
The monitor kept this correct by mirroring the reserved port into
TRACY_PORT; direct users of the public API had no such coupling.

The bound socket is now authoritative: Worker() reads the local port
back via getsockname after binding. The search-loop bookkeeping stays
only as a fallback for a getsockname failure.
2026-08-31 19:30:23 +02:00
Bartosz Taudul
f52bf6a718 Merge pull request #1461 from ofoure/master
Added Profiler::GetPort() and TracyPort macro
2026-08-31 19:07:37 +02:00
Bartosz Taudul
70e4170e26 Cache the downloaded pandoc binary. 2026-08-31 18:54:55 +02:00
Bartosz Taudul
c6f1cb7585 Generate markdown manual at build time. 2026-08-31 18:54:55 +02:00
Bartosz Taudul
00e152b97a Allow embedding files from the build directory. 2026-08-31 18:54:53 +02:00
Olivier Fouré
17a4080713 Documented the TracyPort macro in the manual. 2026-08-31 09:58:26 +02:00
Bartosz Taudul
c756e7418e Fix android build. 2026-08-31 02:48:20 +02:00
Bartosz Taudul
71f0c992f7 Build monitor on CI. 2026-08-31 02:48:16 +02:00
Bartosz Taudul
06f4e13012 Document tracy-monitor in the user manual. 2026-08-31 01:58:38 +02:00
Bartosz Taudul
a66cc184e8 Annotate the file sections. 2026-08-31 01:58:37 +02:00
Bartosz Taudul
c9abfce098 Add a TRACY_ON_DEMAND build option. 2026-08-31 01:58:37 +02:00
Bartosz Taudul
a15e08f135 Rewrite the usage text. 2026-08-31 01:58:37 +02:00
Bartosz Taudul
fdff78584a Raise the fd limit for the sample events.
External sampling opens one event per CPU for each existing thread
(attach) or per CPU (launch) per event type, so a multithreaded target
needs many fds.
2026-08-31 01:12:41 +02:00
Bartosz Taudul
893439772f Reject client configurations that cannot work.
TRACY_NO_CALLSTACK, TRACY_NO_SYSTEM_TRACING, TRACY_NO_SAMPLING and
TRACY_SAMPLING_PROFILER_MANUAL_START either fail to build (the external
target API lives in the callstack code) or silently produce an empty
capture (no sampling event fails, so IsSystemTracingFailed stays false).
The first three are #error'd at compile time; the TRACY_NO_SYS_TRACE /
TRACY_NO_SAMPLING environment variables are refused at startup.
2026-08-31 01:12:41 +02:00
Bartosz Taudul
6d794801c6 Verify the exec stop in launch mode.
A caught signal (the handlers are inherited by the child) delivered
between PTRACE_TRACEME and exec first produces a signal-delivery stop;
without handling it, the setup would run on the still pre-exec child and
capture the monitor's own image as the target's. Re-inject the caught
signal and wait again until the exec stop is reached. Job-control stops
are the exception - re-injecting SIGSTOP/SIGTSTP re-stops the child, and
PTRACE_CONT rejects a stop carrying the 0x80 job-control bit - so resume
those without a signal instead.
2026-08-31 01:12:41 +02:00
Bartosz Taudul
b36c8f2fc5 Refuse to start without tracefs.
The client's system tracing cannot start without a tracefs (or debugfs)
mount - the tracepoint ids live under it - so starting anyway would
capture no samples.
2026-08-31 01:12:41 +02:00
Bartosz Taudul
2f2d8a7ba9 Fail when system tracing fails to start.
IsSystemTracingFailed reports a SysTraceStart that the preflight could
not predict (the target exiting between the checks, the kernel rejecting
the event setup); starting anyway would capture no samples.
2026-08-31 01:12:41 +02:00
Bartosz Taudul
d498829518 Verify the client is listening after startup.
Polling IsDataPortListening is the only reliable check: probing the port
itself cannot distinguish the client's listener from another process
holding it.
2026-08-31 01:12:40 +02:00
Bartosz Taudul
e87b7c873e Reserve the client listen port.
When TRACY_PORT is not pinned the client probes 8086..8105 at startup.
Reserve the first free port up front and hand the already-bound socket
to the client via SetReservedListenSocket, so the reservation is atomic
and the reported port matches what the client listens on. A pinned
TRACY_PORT is validated like --port, since the client pins to any
nonzero value with no fallback.
2026-08-31 01:12:40 +02:00
Bartosz Taudul
6109ab27b5 Add sample rate and port options. 2026-08-31 01:12:40 +02:00
Bartosz Taudul
7357b9fa52 Attach by process name.
The name is matched against /proc/<pid>/comm, which the kernel truncates
to 15 characters. Several matches are listed so the user can pick one
with -p; a match is only accepted if /proc/<pid>/exe is readable, which
excludes zombies (they keep a comm but have no executable to map) and
inaccessible processes.
2026-08-31 01:12:40 +02:00
Bartosz Taudul
46f2489f92 Report the active data sources at startup. 2026-08-31 01:12:40 +02:00
Bartosz Taudul
ac970771ee Preflight the exact sample event.
The old preflight opened one generic event, which said little about
whether the client's actual setup would work. PreflightSamplingEvent now
opens the client's exact external event shape (the per-CPU pid-filtered
CPU_CLOCK callstack event of SysTraceStart), verifies the ring mmap
works, and probes the hardware PMU counters informationally. On
EACCES/EPERM retry user-space only - both event and callchain, since the
callchain part still requires perf_allow_kernel - and report kernel
frames as unavailable; any other error is fatal, as the client's
identical shape would fail the same way.
2026-08-31 01:12:40 +02:00
Bartosz Taudul
d87cbed7eb Probe system-wide tracepoints and RAPL. 2026-08-31 01:12:40 +02:00
Bartosz Taudul
252a3dae43 Treat zombie targets as dead.
kill(pid, 0) succeeds for zombies, so a SIGKILLed attach target whose
parent had not reaped it yet kept the monitor polling indefinitely - in
attach mode the monitor cannot reap it. Liveness now also reads the
/proc state and treats Z as dead.
2026-08-31 01:12:40 +02:00
Bartosz Taudul
c9d3ec0088 Harden external thread name lookups against dead threads.
A thread exiting between fopen and read leaves the comm and status
buffers under-filled or uninitialized: zero-initialize them, treat a
short or empty read as unknown, and never scan the status when the read
returned nothing.
2026-08-31 01:12:39 +02:00
Bartosz Taudul
e5d20e1377 Free the tid buffer when thread enumeration yields nothing.
A task directory yielding no numeric entries (the target exiting
mid-enumeration) returned 0 with the freshly allocated array still
handed back through *out, and the caller's failure path returned without
freeing it.
2026-08-31 01:12:39 +02:00
Bartosz Taudul
da0d2328f5 Require per-target events for external sampling success.
SysTraceStart only failed when no event of any kind opened, but in
external mode the global sched/vsync tracepoints (pid -1) succeed
independently of the target: a target exiting after thread enumeration
left every per-target open failing while the startup still reported
success. s_ctxBufferIdx is the per-target ring count right after the
per-target setup, so in external mode require it to be non-zero as well.
2026-08-31 01:12:39 +02:00
Bartosz Taudul
8db1a298c4 Open external sampling as CPU-gated per-CPU events.
The previous external shape - one per-task (cpu = -1) inherit event per
existing thread - cannot be mmap'd at all: perf_mmap() in
kernel/events/core.c refuses inherited per-task counters (-EINVAL, all
children would write the same ring), so no sample ring was ever created.
A per-CPU event filtered on the target's tgid (the self-profiling shape)
only covers the group leader; the kernel does not retro-inherit onto
pre-existing sibling threads.

Open one CPU-gated event per ring instead - perf_event_open(attr, pid,
cpu), one open per CPU and per target thread, the same shape as perf's
open loop (tools/perf/util/evsel.c:3031). The thread enumeration picks
the fan-out: at launch, per-CPU events on the target pid, inherited by
every later-spawned thread; on attach, per-thread per-CPU events for
every existing tid. Failing opens degrade gracefully: the affected
thread is simply not sampled.
2026-08-31 01:12:39 +02:00
Bartosz Taudul
37410933db Always deliver the sample IP.
Add PERF_SAMPLE_IP to the callstack sample: for code compiled without
frame pointers the kernel delivers no user stack, and such samples were
dropped entirely. When the callchain count is zero, synthesize a
one-frame trace holding just the leaf IP.
2026-08-31 01:12:39 +02:00
Bartosz Taudul
afe1ec5faf Factor the sample event open into OpenSampleEvent.
The seven sampling event setups repeated the same open/mmap/retry
sequence. On a refused open the retry now stays user-space only
(exclude_kernel and exclude_callchain_kernel both still require
perf_allow_kernel), and a failed open no longer breaks out of the whole
event loop.
2026-08-31 01:12:39 +02:00
Bartosz Taudul
d440f5e055 Report whether the data port is listening. 2026-08-31 01:12:39 +02:00
Bartosz Taudul
ae70255b4e Adopt a pre-bound listen socket. 2026-08-31 01:12:39 +02:00
Bartosz Taudul
b887def929 Record and expose system tracing startup failure. 2026-08-31 01:12:38 +02:00
Bartosz Taudul
7aee673792 Use the target executable time for external captures.
The worker must key source and symbol transfers on the target's
executable, not the monitor's own binary; the exe mtime was read
straight from /proc/<pid>/exe by InitExternalTarget (the readlink'd path
is namespace-dependent and carries a " (deleted)" suffix). For an
external target the time may be unavailable, so drop the assert on a
nonzero exectime.
2026-08-31 01:12:38 +02:00
Bartosz Taudul
81f3c71ef3 Transfer machine code for external targets.
For an external target the symbol code query addresses are VMAs in the
target's address space, not pointers in the monitor's, so read them out
with ReadExternalTargetMemory. Kernel code (the top bit marks it) is
shared by the monitor and its target alike, so the kernel path applies
in both modes.
2026-08-31 01:12:38 +02:00
Bartosz Taudul
d8a011630d Add an API to read external target memory.
ReadExternalTargetMemory reads bytes out of the target's address space:
process_vm_readv for live bytes (gated like ptrace access), falling back
to the target's on-disk image located by scanning /proc/<pid>/maps
directly - the caller runs while the symbol thread may be rebuilding the
shared image cache on refresh.
2026-08-31 01:12:38 +02:00
Bartosz Taudul
1f1d3ab856 Resolve external callstacks to the full inline chain.
The resolver previously captured only the first pcinfo result, so inline
frames were lost, and reported ELF virtual addresses the server could
never re-map to the target. Mirror the in-process pcinfo+syminfo flow,
and report symAddr as the target VMA (loadBias + ELF vaddr) so the
server's re-queries can resolve it.
2026-08-31 01:12:38 +02:00
Bartosz Taudul
2e566513dd Create external backtrace states from openable paths only.
backtrace_create_state_for_file opens the file lazily on first use and,
on a failed open, silently falls back to /proc/self/exe - so a state
created from a path the monitor cannot open would symbolize the target's
addresses against the monitor's own binary. Probe the target's root path
(and, for unlinked images, the mapping's map_files entry), and only
create a state for a path that actually opens.
2026-08-31 01:12:38 +02:00
Bartosz Taudul
2a40f6f990 Converge the external image list on refresh.
The list only ever added entries, so a mapping the target re-mapped or
unloaded stayed in the list and could shadow the new mapping or keep
serving an already-unloaded library. Rebuild it from the target's
current maps on each refresh, carrying over the cached backtrace state
for unchanged mappings and dropping entries the current maps no longer
confirm. The dropped entries' paths are abandoned rather than freed:
queued callstack items still carry the path pointer, which the worker
thread reads at its own pace. Strip the kernel's " (deleted)" suffix so
unlinked images converge to their canonical path.
2026-08-31 01:12:38 +02:00
Bartosz Taudul
0e7040d73b Derive the external load base from the mapping's own segment.
The minVaddr heuristic assumes the mapping's file offset is measured
against the lowest PT_LOAD vaddr of the image. The kernel maps each
PT_LOAD independently, at load_bias + ELF_PAGESTART(p_vaddr) from
p_offset - ELF_PAGEOFFSET(p_vaddr) (fs/binfmt_elf.c, elf_map), so the
assumption only holds for the segment carrying the minimum vaddr. Match
the mapping to its PT_LOAD through the exact offset relation the kernel
used, and take the base from that segment's own vaddr; the minVaddr
heuristic remains as the fallback. The header must be ELF64
(e_ident[4]): the fixed-size elf_ehdr/elf_phdr structs misparse other
classes, and a false segment match on misparsed headers would return a
silently wrong base.
2026-08-31 01:12:38 +02:00
Bartosz Taudul
bd1d783017 Resolve external images through the target mount namespace.
The image paths from /proc/<pid>/maps are only valid in the target's
mount namespace, so resolve them through /proc/<pid>/root (proc(5)). For
unlinked images the kernel keeps the bytes alive in the mapping's
map_files entry, which is opened directly instead.
2026-08-31 01:12:38 +02:00
Bartosz Taudul
d352907e5d Drop the magic monitor globals. 2026-08-31 01:12:37 +02:00
Bartosz Taudul
4e520cfc18 Add the external target API. 2026-08-31 01:12:34 +02:00
Bartosz Taudul
ac9ea9dde7 Stop fetching vendor dependencies for the monitor build. 2026-08-30 21:25:08 +02:00
Bartosz Taudul
38b19d2be6 Constrain the monitor build to Linux x86_64 and aarch64. 2026-08-30 21:25:07 +02:00
Bartosz Taudul
601d57de35 Copy the filename in backtrace_create_state_for_file.
The state opens the file lazily on first use (fileline_initialize), so
it must own the filename: the caller's buffer may be freed as soon as
this returns. Pass a copy into backtrace_create_state.
2026-08-30 21:25:05 +02:00
Olivier Fouré
8e8f3073a7 Made Profiler::GetPort() thread-safe. 2026-08-28 15:39:19 +02:00
Bartosz Taudul
8b41c38ed9 Merge pull request #1464 from jorisv/topic/fix-git-ref-windows
Fix GitRef.hpp generation on Windows
2026-08-28 11:52:15 +02:00
Joris Vaillant
d6ef0f0fa4 Fix GitRef.hpp generation on Windows 2026-08-28 10:50:48 +02:00
Bartosz Taudul
031ef02614 Use MSVC for the meson build in windows CI. 2026-08-27 18:51:53 +02:00
Bartosz Taudul
0f65035ef3 Link ws2_32, dbghelp and secur32 for MinGW in meson. 2026-08-27 18:43:10 +02:00
Bartosz Taudul
253b68dcb6 Notify after queue can be done out of the lock. 2026-08-27 17:23:20 +02:00
Bartosz Taudul
32aa7ee1f5 Derive meson version via python3, not sh. 2026-08-27 17:23:20 +02:00
Bartosz Taudul
5ed42cfc6d Add meson build to windows CI. 2026-08-27 17:23:18 +02:00
Olivier Fouré
b2b8dc51ce Added Profiler::GetPort() and TracyPort macro to query the data port in use. 2026-08-26 14:28:11 +02:00
Bartosz Taudul
f207007548 Merge pull request #1460 from ofoure/master
Fixed buffer overflow after using VkCtx cmdbuf ctor.
2026-08-25 17:52:29 +02:00
Olivier Fouré
94e58d4e24 Fixed buffer overflow after using VkCtx cmdbuf ctor. 2026-08-25 14:40:54 +02:00
37 changed files with 1916 additions and 6116 deletions

View File

@@ -17,6 +17,7 @@ concurrency:
env:
CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm-cache
PANDOC_CACHE_DIR: ${{ github.workspace }}/pandoc-cache
jobs:
build-appimage:
@@ -40,6 +41,11 @@ jobs:
path: ${{ env.CPM_SOURCE_CACHE }}
key: ${{ runner.os }}-cpm-${{ hashFiles('**/vendor.cmake', '**/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cpm-
- name: Cache pandoc
uses: actions/cache@v4
with:
path: ${{ env.PANDOC_CACHE_DIR }}
key: ${{ runner.os }}-pandoc-${{ hashFiles('cmake/manual.cmake') }}
- name: Cache wayland
uses: actions/cache@v4
with:

View File

@@ -16,6 +16,7 @@ concurrency:
env:
CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm-cache
PANDOC_CACHE_DIR: ${{ github.workspace }}/pandoc-cache
jobs:
build-emscripten:
@@ -37,6 +38,11 @@ jobs:
path: ${{ env.CPM_SOURCE_CACHE }}
key: ${{ runner.os }}-cpm-${{ hashFiles('**/vendor.cmake', '**/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cpm-
- name: Cache pandoc
uses: actions/cache@v4
with:
path: ${{ env.PANDOC_CACHE_DIR }}
key: ${{ runner.os }}-pandoc-${{ hashFiles('cmake/manual.cmake') }}
- name: Profiler GUI
run: |
cmake -G Ninja -B profiler/build -S profiler -DCMAKE_BUILD_TYPE=MinSizeRel -DGIT_REV=${{ github.sha }} -DCMAKE_TOOLCHAIN_FILE=${{env.EMSDK}}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake

View File

@@ -57,6 +57,10 @@ jobs:
run: |
cmake -B merge/build -S merge -DCMAKE_BUILD_TYPE=Release -DNO_ISA_EXTENSIONS=ON -DGIT_REV=${{ github.sha }}
cmake --build merge/build --parallel
- name: Build monitor
run: |
cmake -B monitor/build -S monitor -DCMAKE_BUILD_TYPE=Release -DNO_ISA_EXTENSIONS=ON -DGIT_REV=${{ github.sha }}
cmake --build monitor/build --parallel
- name: Stage release files
run: |
mkdir -p linux-cli
@@ -67,6 +71,7 @@ jobs:
cp import/build/tracy-import-chrome linux-cli/
cp import/build/tracy-import-fuchsia linux-cli/
cp merge/build/tracy-merge linux-cli/
cp monitor/build/tracy-monitor linux-cli/
strip linux-cli/tracy-*
- uses: actions/upload-artifact@v4
with:

View File

@@ -16,6 +16,7 @@ concurrency:
env:
CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm-cache
PANDOC_CACHE_DIR: ${{ github.workspace }}/pandoc-cache
jobs:
build-linux:
@@ -33,6 +34,11 @@ jobs:
path: ${{ env.CPM_SOURCE_CACHE }}
key: ${{ runner.os }}-cpm-${{ hashFiles('**/vendor.cmake', '**/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cpm-
- name: Cache pandoc
uses: actions/cache@v4
with:
path: ${{ env.PANDOC_CACHE_DIR }}
key: ${{ runner.os }}-pandoc-${{ hashFiles('cmake/manual.cmake') }}
- name: Determine build parallelism
run: |
if [ "${ACT:-}" != "true" ] && [ "${FORGEJO_ACTIONS:-}" != "true" ]; then
@@ -72,6 +78,10 @@ jobs:
run: |
cmake -B merge/build -S merge -DCMAKE_BUILD_TYPE=Release -DGIT_REV=${{ github.sha }}
cmake --build merge/build $CMAKE_PARALLEL
- name: Monitor utility
run: |
cmake -B monitor/build -S monitor -DCMAKE_BUILD_TYPE=Release -DGIT_REV=${{ github.sha }}
cmake --build monitor/build $CMAKE_PARALLEL
- name: Library (cmake)
run: |
cmake -B build -DCMAKE_BUILD_TYPE=Release -DTRACY_ENABLE=ON
@@ -96,6 +106,7 @@ jobs:
cp import/build/tracy-import-chrome bin
cp import/build/tracy-import-fuchsia bin
cp merge/build/tracy-merge bin
cp monitor/build/tracy-monitor bin
strip bin/tracy-*
- uses: actions/upload-artifact@v4
with:

View File

@@ -17,6 +17,7 @@ concurrency:
env:
CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm-cache
PANDOC_CACHE_DIR: ${{ github.workspace }}/pandoc-cache
jobs:
build-macos:
@@ -29,6 +30,11 @@ jobs:
path: ${{ env.CPM_SOURCE_CACHE }}
key: ${{ runner.os }}-cpm-${{ hashFiles('**/vendor.cmake', '**/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cpm-
- name: Cache pandoc
uses: actions/cache@v4
with:
path: ${{ env.PANDOC_CACHE_DIR }}
key: ${{ runner.os }}-pandoc-${{ hashFiles('cmake/manual.cmake') }}
- name: Install dependencies
run: brew install pkg-config meson
- name: Trust git repo

View File

@@ -17,6 +17,7 @@ concurrency:
env:
CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm-cache
PANDOC_CACHE_DIR: ${{ github.workspace }}/pandoc-cache
jobs:
build-windows:
@@ -29,9 +30,26 @@ jobs:
path: ${{ env.CPM_SOURCE_CACHE }}
key: ${{ runner.os }}-cpm-${{ hashFiles('**/vendor.cmake', '**/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cpm-
- name: Cache pandoc
uses: actions/cache@v4
with:
path: ${{ env.PANDOC_CACHE_DIR }}
key: ${{ runner.os }}-pandoc-${{ hashFiles('cmake/manual.cmake') }}
- uses: microsoft/setup-msbuild@v2
- name: Trust git repo
run: git config --global --add safe.directory '*'
- name: Install meson
run: python -m pip install --upgrade meson
- name: Library (meson)
env:
CCACHE_DISABLE: '1'
run: |
$vs = & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
foreach ($line in & cmd /c "`"$vs\VC\Auxiliary\Build\vcvarsall.bat`" x64 && set") {
if ($line -match '^([^=]+)=(.*)$') { [Environment]::SetEnvironmentVariable($matches[1], $matches[2], 'Process') }
}
meson setup -Dtracy_enable=true build-meson
meson compile -C build-meson
- name: Test application
uses: ./.github/actions/test-tracy
- name: Build profiler

1
.gitignore vendored
View File

@@ -17,6 +17,7 @@ manual/t*.bbl
manual/t*.blg
manual/t*.fdb_latexmk
manual/t*.fls
manual/tracy.md
profiler/build-appimage
.deps/
.dirstamp

View File

@@ -288,7 +288,8 @@ fs.writeFileSync('/tmp/toolbar-zoom.png', buf);
| 10121036 | 2142681495 | ▶ next frame — focuses the timeline on the next frame (verified: view range 25.82 s → 203.38 ms) |
| 10521072 | 2961929287 | ▼ frame set selection (switch the active frame set) |
Button names and functions: the user manual, *Control menu* (`manual/tracy.md`). The
Button names and functions: the user manual, *Control menu* (`manual/tracy.md`,
generated into the profiler build directory by the build, e.g. `build/profiler/manual/tracy.md`). The
web build omits *Connection* (live capture only) and *Tracy Assist* (desktop only).
## User scale (DPI zoom)

View File

@@ -11,7 +11,7 @@ function(add_git_ref target)
if(Git_FOUND)
add_custom_target(git-ref
COMMAND ${CMAKE_COMMAND} -E echo "#pragma once" > GitRef.hpp.tmp
COMMAND ${GIT_EXECUTABLE} -C ${CMAKE_CURRENT_SOURCE_DIR} log -1 "--format=namespace tracy { static inline const char* GitRef = %x22%h%x22; }" ${GIT_REV} >> GitRef.hpp.tmp || echo "namespace tracy { static inline const char* GitRef = \"unknown\"; }" >> GitRef.hpp.tmp
COMMAND ${GIT_EXECUTABLE} -C ${CMAKE_CURRENT_SOURCE_DIR} log -1 "--format=namespace tracy { static inline const char* GitRef = %x22%h%x22; }" ${GIT_REV} >> GitRef.hpp.tmp || ${CMAKE_COMMAND} -E echo "namespace tracy { static inline const char* GitRef = \"unknown\"; }" >> GitRef.hpp.tmp
COMMAND ${CMAKE_COMMAND} -E copy_if_different GitRef.hpp.tmp GitRef.hpp
BYPRODUCTS GitRef.hpp GitRef.hpp.tmp
VERBATIM

88
cmake/manual.cmake Normal file
View File

@@ -0,0 +1,88 @@
set(PANDOC_VERSION 3.9.0.2)
find_package(Python3 COMPONENTS Interpreter REQUIRED)
find_program(TRACY_PANDOC pandoc)
if(TRACY_PANDOC)
execute_process(
COMMAND ${TRACY_PANDOC} --version
OUTPUT_VARIABLE _pandoc_version
OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE _pandoc_rc
ERROR_QUIET)
string(REGEX MATCH "pandoc [0-9.]+" _pandoc_version "${_pandoc_version}")
if(NOT _pandoc_rc EQUAL 0 OR NOT _pandoc_version STREQUAL "pandoc ${PANDOC_VERSION}")
set(TRACY_PANDOC "")
endif()
endif()
if(TRACY_PANDOC)
message(STATUS "Using system pandoc ${PANDOC_VERSION}: ${TRACY_PANDOC}")
else()
if(WIN32)
if(NOT CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$")
message(FATAL_ERROR "No pinned pandoc ${PANDOC_VERSION} binary for ${CMAKE_HOST_SYSTEM_PROCESSOR}; install pandoc ${PANDOC_VERSION}")
endif()
set(_pandoc_asset pandoc-${PANDOC_VERSION}-windows-x86_64.zip)
set(_pandoc_hash c97542f2800f446e788d9f74237856d995421ad1bb3cc8324286840c5f272d3a)
set(_pandoc_exe pandoc-${PANDOC_VERSION}/pandoc.exe)
elseif(APPLE)
if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "^(arm64|aarch64)$")
set(_pandoc_asset pandoc-${PANDOC_VERSION}-arm64-macOS.zip)
set(_pandoc_hash 6e9eca844076bcbb599bbeebbba78a70f93b5307782b85c2c272872812c88875)
set(_pandoc_exe pandoc-${PANDOC_VERSION}-arm64/bin/pandoc)
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$")
set(_pandoc_asset pandoc-${PANDOC_VERSION}-x86_64-macOS.zip)
set(_pandoc_hash b9fbceabccbc8f34ac021a50483fc32f8160568d0b4b2c22d81bb29e3054fd82)
set(_pandoc_exe pandoc-${PANDOC_VERSION}-x86_64/bin/pandoc)
else()
message(FATAL_ERROR "No pinned pandoc ${PANDOC_VERSION} binary for ${CMAKE_HOST_SYSTEM_PROCESSOR}; install pandoc ${PANDOC_VERSION}")
endif()
else()
if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$")
set(_pandoc_asset pandoc-${PANDOC_VERSION}-linux-arm64.tar.gz)
set(_pandoc_hash b6d21e8f9c3b15744f5a7ab40248019157ed7793875dbe0383d4c82ff572b528)
set(_pandoc_exe pandoc-${PANDOC_VERSION}/bin/pandoc)
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$")
set(_pandoc_asset pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz)
set(_pandoc_hash a69abfababda8a56969a254b09f9553a7be89ddec00d4e0fe9fd585d71a67508)
set(_pandoc_exe pandoc-${PANDOC_VERSION}/bin/pandoc)
else()
message(FATAL_ERROR "No pinned pandoc ${PANDOC_VERSION} binary for ${CMAKE_HOST_SYSTEM_PROCESSOR}; install pandoc ${PANDOC_VERSION}")
endif()
endif()
# Shared cache: $PANDOC_CACHE_DIR, else the platform user cache directory.
if(DEFINED ENV{PANDOC_CACHE_DIR})
set(_pandoc_cache $ENV{PANDOC_CACHE_DIR})
elseif(WIN32)
set(_pandoc_cache $ENV{LOCALAPPDATA}/tracy/pandoc)
elseif(APPLE)
set(_pandoc_cache $ENV{HOME}/Library/Caches/tracy/pandoc)
elseif(DEFINED ENV{XDG_CACHE_HOME})
set(_pandoc_cache $ENV{XDG_CACHE_HOME}/tracy/pandoc)
else()
set(_pandoc_cache $ENV{HOME}/.cache/tracy/pandoc)
endif()
set(TRACY_PANDOC ${_pandoc_cache}/${_pandoc_exe})
if(NOT EXISTS ${TRACY_PANDOC})
# Concurrent configures on a cold cache can collide; worst case the
# hash check fails and a rerun succeeds.
message(STATUS "Downloading pandoc ${PANDOC_VERSION} (${_pandoc_asset})")
file(MAKE_DIRECTORY ${_pandoc_cache})
file(DOWNLOAD
https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/${_pandoc_asset}
${_pandoc_cache}/pandoc.archive
EXPECTED_HASH SHA256=${_pandoc_hash}
STATUS _pandoc_download)
list(GET _pandoc_download 0 _pandoc_download_rc)
if(NOT _pandoc_download_rc STREQUAL "0")
file(REMOVE ${_pandoc_cache}/pandoc.archive)
message(FATAL_ERROR "Failed to download pandoc ${PANDOC_VERSION}: ${_pandoc_download}")
endif()
file(ARCHIVE_EXTRACT
INPUT ${_pandoc_cache}/pandoc.archive
DESTINATION ${_pandoc_cache})
file(REMOVE ${_pandoc_cache}/pandoc.archive)
endif()
endif()

23
extra/tracy-version.py Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python3
# Derive the project version from the single source of truth:
# public/common/TracyVersion.hpp. Prints X.Y.Z, exits nonzero on failure.
import re
import sys
header = sys.argv[1]
try:
with open(header, encoding='utf-8') as f:
text = f.read()
except OSError as exc:
print('tracy-version: cannot read %s: %s' % (header, exc), file=sys.stderr)
sys.exit(1)
numbers = {}
for name in ('Major', 'Minor', 'Patch'):
match = re.search(r'%s\s*=\s*(\d+)' % name, text)
if not match:
print('tracy-version: could not parse version from %s' % header, file=sys.stderr)
sys.exit(1)
numbers[name] = match.group(1)
print('%s.%s.%s' % (numbers['Major'], numbers['Minor'], numbers['Patch']))

View File

@@ -1,21 +0,0 @@
#!/bin/sh
# Derive the project version from the single source of truth:
# public/common/TracyVersion.hpp. Prints X.Y.Z, exits nonzero on failure.
header="$1"
[ -r "$header" ] || {
echo "tracy-version: cannot read $header" >&2
exit 1
}
major=$(sed -n 's/.*Major = \([0-9][0-9]*\).*/\1/p' "$header")
minor=$(sed -n 's/.*Minor = \([0-9][0-9]*\).*/\1/p' "$header")
patch=$(sed -n 's/.*Patch = \([0-9][0-9]*\).*/\1/p' "$header")
if [ -n "$major" ] && [ -n "$minor" ] && [ -n "$patch" ]; then
printf '%s.%s.%s\n' "$major" "$minor" "$patch"
else
echo "tracy-version: could not parse version from $header" >&2
exit 1
fi

View File

@@ -1 +0,0 @@
The LaTeX source file (tracy.tex) and the resulting PDF file (tracy.pdf) are the only authorative version of the user manual. Do NOT modify the Markdown user manual (tracy.md) by hand. It is only meant to be updated via the latex2md.sh script.

View File

@@ -1,35 +0,0 @@
/\\begin\{bclogo\}\[/ {
in_bclogo = 1
bclogo_type = ""
next
}
in_bclogo && /logo=/ {
if (/\\bcbombe/) bclogo_type = "bcbombe"
else if (/\\bcattention/) bclogo_type = "bcattention"
else if (/\\bclampe/) bclogo_type = "bclampe"
else if (/\\bcquestion/) bclogo_type = "bcquestion"
next
}
in_bclogo && /noborder|couleur/ {
next
}
in_bclogo {
line = $0
sub(/^[ \t]*\]?\{/, "", line)
sub(/\}.*$/, "", line)
bclogo_title = line
if (bclogo_type == "bcbombe") prefix = "IMPORTANT"
else if (bclogo_type == "bcattention") prefix = "CAUTION"
else if (bclogo_type == "bclampe") prefix = "TIP"
else prefix = "NOTE"
printf "\\begin{quote}\\textbf{%s:%s}\\par\n", prefix, bclogo_title
in_bclogo = 0
next
}
/\\end\{bclogo\}/ {
printf "\\end{quote}\n"
next
}
{ print }

View File

@@ -1,64 +0,0 @@
#!/usr/bin/env python3
"""Replace Font Awesome icon macros in LaTeX with Unicode codepoints."""
import re
import sys
def pascal_to_snake(name):
"""Convert PascalCase to UPPER_SNAKE_CASE."""
result = name[0]
for i in range(1, len(name)):
if name[i].isupper() and name[i - 1].islower():
result += '_'
result += name[i]
return result.upper()
def main():
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} <header_path> <tex_path>", file=sys.stderr)
sys.exit(1)
header_path = sys.argv[1]
tex_path = sys.argv[2]
# Parse header: ICON_FA_SNAKE_CASE -> Unicode char
icons = {}
with open(header_path) as f:
for line in f:
m = re.match(
r'#define\s+ICON_FA_(\w+)\s+.*?//\s*(U\+([0-9a-fA-F]+))', line
)
if m:
snake = m.group(1)
parts = snake.split('_')
pascal = ''.join(p.capitalize() for p in parts)
codepoint = int(m.group(3), 16)
icons[pascal] = chr(codepoint)
# Read tex file
with open(tex_path) as f:
text = f.read()
# Find all \faXxx used in the text (uppercase first letter excludes \fancyhead etc.)
used = set()
for m in re.finditer(r'\\fa([A-Z][a-zA-Z0-9]*)', text):
used.add(m.group(1))
# Replace each used icon, longest names first to avoid prefix conflicts
for name in sorted(used, key=lambda n: (-len(n), n)):
if name not in icons:
print(f"Warning: \\fa{name} not found in header", file=sys.stderr)
continue
char = icons[name]
# Order matters: more specific patterns first
text = text.replace(f'\\fa{name}{{}}~', f'{char} ')
text = text.replace(f'\\fa{name}{{}}', char)
text = text.replace(f'\\fa{name}~', f'{char} ')
text = text.replace(f'\\fa{name}', char)
# Write back
with open(tex_path, 'w') as f:
f.write(text)
if __name__ == '__main__':
main()

View File

@@ -1,77 +0,0 @@
#!/usr/bin/env python3
"""Append icon legend blocks to each markdown section containing Font Awesome icons."""
import re
import sys
def _extract_icons(lines):
"""Return deduplicated icon chars from lines, in order of first appearance."""
seen = set()
icons = []
for line in lines:
for ch in line:
cp = ord(ch)
if 0xE000 <= cp <= 0xF8FF and ch not in seen:
seen.add(ch)
icons.append(ch)
return icons
def _append_legend(result_lines, icons, icon_names):
"""Append a legend block for the given icons."""
result_lines.append('')
result_lines.append('-----')
result_lines.append('')
for ch in icons:
name = icon_names.get(ch, f'Unknown(U+{ord(ch):04X})')
result_lines.append(f'{ch} - {name} icon')
result_lines.append('')
def main():
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} <header_path> <md_path>", file=sys.stderr)
sys.exit(1)
header_path = sys.argv[1]
md_path = sys.argv[2]
# Build char -> name mapping from header
icon_names = {}
with open(header_path) as f:
for line in f:
m = re.match(
r'#define\s+ICON_FA_(\w+)\s+.*?//\s*(U\+([0-9a-fA-F]+))', line
)
if m:
snake = m.group(1)
parts = snake.split('_')
pascal = ' '.join(p.capitalize() for p in parts)
codepoint = int(m.group(3), 16)
icon_names[chr(codepoint)] = pascal
with open(md_path, encoding='utf-8') as f:
lines = f.read().split('\n')
# Build chunk boundaries: header lines and EOF
chunk_starts = [i for i, line in enumerate(lines) if line.startswith('#')]
# Also add index 0 as a chunk start if there's pre-header content
if chunk_starts and chunk_starts[0] > 0:
chunk_starts.insert(0, 0)
result_lines = []
for ci, start in enumerate(chunk_starts):
end = chunk_starts[ci + 1] if ci + 1 < len(chunk_starts) else len(lines)
icons = _extract_icons(lines[start:end])
result_lines.extend(lines[start:end])
if icons:
_append_legend(result_lines, icons, icon_names)
with open(md_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(result_lines))
if __name__ == '__main__':
main()

232
manual/latex2md.py Normal file
View File

@@ -0,0 +1,232 @@
#!/usr/bin/env python3
"""Generate the Markdown user manual from the LaTeX source.
Cross-platform replacement for the previous sh/sed/awk pipeline. pandoc is
invoked as a subprocess; its Lua filter (filter.lua, next to the .tex file)
stays with pandoc.
"""
import argparse
import os
import re
import subprocess
import sys
PANDOC_MARKDOWN = 'markdown-simple_tables-multiline_tables-grid_tables+pipe_tables'
NAMES = (
('quicklook', 'A quick look at Tracy Profiler'),
('firststeps', 'First steps'),
('client', 'Client markup'),
('capturing', 'Capturing the data'),
('analyzingdata', 'Analyzing captured data'),
('tracyassist', 'Tracy Assist'),
('csvexport', 'Exporting zone statistics to CSV'),
('importingdata', 'Importing external profiling data'),
('configurationfiles', 'Configuration files'),
)
def parse_icon_header(path):
"""Return (pascal_name -> char, char -> 'Word Word') maps from the icon header."""
fa = {}
names = {}
with open(path, encoding='utf-8') as f:
for line in f:
m = re.match(
r'#define\s+ICON_FA_(\w+)\s+.*?//\s*(U\+([0-9a-fA-F]+))', line
)
if m:
parts = m.group(1).split('_')
ch = chr(int(m.group(3), 16))
fa[''.join(p.capitalize() for p in parts)] = ch
names[ch] = ' '.join(p.capitalize() for p in parts)
return fa, names
def substitute_icons(text, fa):
used = set(re.findall(r'\\fa([A-Z][a-zA-Z0-9]*)', text))
for name in sorted(used, key=lambda n: (-len(n), n)):
if name not in fa:
print(f"Warning: \\fa{name} not found in header", file=sys.stderr)
continue
ch = fa[name]
text = text.replace(f'\\fa{name}{{}}~', f'{ch} ')
text = text.replace(f'\\fa{name}{{}}', ch)
text = text.replace(f'\\fa{name}~', f'{ch} ')
text = text.replace(f'\\fa{name}', ch)
return text
def pre_sed(text):
text = text.replace('\\menu[,]', '')
text = text.replace('\\keys', '')
text = text.replace('\\ctrl', 'Ctrl')
text = text.replace('\\shift', 'Shift')
text = text.replace('\\Alt', 'Alt')
text = text.replace('\\del', 'Delete')
return text
def post_sed(text):
for m in ('LMB', 'MMB', 'RMB', 'Scroll'):
text = text.replace(f'\\{m}{{}}~', '')
text = text.replace('\\textsigma', 'σ')
for l in 'abc':
text = text.replace(f'@\\circled{{{l}}}@', f'({l})')
for l in 'abc':
text = text.replace(f'\\circled{{{l}}}', f'({l})')
text = text.replace('@\\ldots@', '')
text = re.sub(r',?escapeinside=\{\}\{\}', '', text)
for label, name in NAMES:
text = text.replace(f'\\nameref{{{label}}}', name)
return text
def bclogo2quote(text):
"""Convert \\begin{bclogo} admonition blocks to \\begin{quote}\\textbf{PREFIX:...}."""
prefix = {'bcbombe': 'IMPORTANT', 'bcattention': 'CAUTION', 'bclampe': 'TIP'}
out = []
in_bclogo = False
bclogo_type = ''
for line in text.split('\n'):
if not in_bclogo and r'\begin{bclogo}[' in line:
in_bclogo = True
bclogo_type = ''
continue
if in_bclogo and 'logo=' in line:
if r'\bcbombe' in line:
bclogo_type = 'bcbombe'
elif r'\bcattention' in line:
bclogo_type = 'bcattention'
elif r'\bclampe' in line:
bclogo_type = 'bclampe'
elif r'\bcquestion' in line:
bclogo_type = 'bcquestion'
continue
if in_bclogo and ('noborder' in line or 'couleur' in line):
continue
if in_bclogo:
title = re.sub(r'\}.*$', '', re.sub(r'^[ \t]*\]?\{', '', line))
out.append(f"\\begin{{quote}}\\textbf{{{prefix.get(bclogo_type, 'NOTE')}:{title}}}\\par")
in_bclogo = False
continue
if r'\end{bclogo}' in line:
out.append('\\end{quote}')
continue
out.append(line)
return '\n'.join(out)
def run_pandoc(pandoc, filter_path, tex):
args = [
pandoc,
'--wrap=none',
'--reference-location=block',
'--number-sections',
'-f', 'latex',
'-t', PANDOC_MARKDOWN,
'-s',
'-L', filter_path,
'-',
]
proc = subprocess.run(args, input=tex.encode('utf-8'), capture_output=True)
if proc.returncode != 0:
sys.stderr.write(proc.stderr.decode('utf-8', 'replace'))
sys.exit(proc.returncode)
return proc.stdout.decode('utf-8')
def tablecaption(text):
"""Pandoc emits table captions as ": ..." lines; italicize them instead."""
out = []
incap = False
for line in text.split('\n'):
if not incap and line.startswith(': '):
incap = True
line = '_' + line[2:]
if incap and not line.endswith('\\'):
out.append(line + '_')
incap = False
continue
out.append(line)
return '\n'.join(out)
def admonitions(text):
for kind in ('IMPORTANT', 'TIP', 'CAUTION', 'NOTE'):
text = re.sub(
rf'^> \*\*{kind}:([^*]*)\*\*',
f'> [!{kind}]\n> **\\1**',
text,
flags=re.M,
)
return text
def icon_explain(text, names):
"""Append an icon legend to each top-level section containing FA icons."""
lines = text.split('\n')
chunk_starts = [i for i, line in enumerate(lines) if line.startswith('#')]
if chunk_starts and chunk_starts[0] > 0:
chunk_starts.insert(0, 0)
out = []
for ci, start in enumerate(chunk_starts):
end = chunk_starts[ci + 1] if ci + 1 < len(chunk_starts) else len(lines)
chunk = lines[start:end]
icons = []
seen = set()
for line in chunk:
for ch in line:
cp = ord(ch)
if 0xE000 <= cp <= 0xF8FF and ch not in seen:
seen.add(ch)
icons.append(ch)
out.extend(chunk)
if icons:
out.append('')
out.append('-----')
out.append('')
for ch in icons:
out.append(f"{ch} - {names.get(ch, f'Unknown(U+{ord(ch):04X})')} icon")
out.append('')
return '\n'.join(out)
def main():
ap = argparse.ArgumentParser(description='Generate the Markdown user manual')
ap.add_argument('--tex', required=True, help='path to tracy.tex')
ap.add_argument('--out', required=True, help='path of the generated tracy.md')
ap.add_argument('--pandoc', required=True, help='path to the pandoc executable')
ap.add_argument('--icons', required=True, help='path to IconsFontAwesome7.h')
args = ap.parse_args()
fa, names = parse_icon_header(args.icons)
with open(args.tex, encoding='utf-8') as f:
text = f.read()
text = pre_sed(text)
text = substitute_icons(text, fa)
text = post_sed(text)
text = bclogo2quote(text)
md = run_pandoc(
args.pandoc,
os.path.join(os.path.dirname(os.path.abspath(args.tex)), 'filter.lua'),
text,
)
md = tablecaption(md)
md = admonitions(md)
md = icon_explain(md, names)
out_dir = os.path.dirname(os.path.abspath(args.out))
if out_dir:
os.makedirs(out_dir, exist_ok=True)
with open(args.out, 'w', encoding='utf-8', newline='\n') as f:
f.write(md)
if __name__ == '__main__':
main()

View File

@@ -1,54 +0,0 @@
#!/bin/sh
cp -f tracy.tex _tmp.tex
sed -i -e 's@\\menu\[,\]@@g' _tmp.tex
sed -i -e 's@\\keys@@g' _tmp.tex
sed -i -e 's@\\ctrl@Ctrl@g' _tmp.tex
sed -i -e 's@\\shift@Shift@g' _tmp.tex
sed -i -e 's@\\Alt@Alt@g' _tmp.tex
sed -i -e 's@\\del@Delete@g' _tmp.tex
python3 fa-icons.py ../profiler/src/profiler/IconsFontAwesome7.h _tmp.tex
sed -i -e 's@\\LMB{}~@@g' _tmp.tex
sed -i -e 's@\\MMB{}~@@g' _tmp.tex
sed -i -e 's@\\RMB{}~@@g' _tmp.tex
sed -i -e 's@\\Scroll{}~@@g' _tmp.tex
sed -i -e 's@\\textsigma@σ@g' _tmp.tex
# Resolve \circled{} markers and lstlisting escapeinside (@...@) snippets, which
# pandoc would otherwise emit verbatim or drop, to their Unicode equivalents.
sed -i -e 's|@\\circled{a}@|(a)|g' -e 's|@\\circled{b}@|(b)|g' -e 's|@\\circled{c}@|(c)|g' _tmp.tex
sed -i -e 's|\\circled{a}|(a)|g' -e 's|\\circled{b}|(b)|g' -e 's|\\circled{c}|(c)|g' _tmp.tex
sed -i -e 's|@\\ldots@|…|g' _tmp.tex
# pandoc doens't recognize this lstlisting option and emits it verbatim
sed -i -e 's|,\?escapeinside={}{}||g' _tmp.tex
sed -i -e 's@\\nameref{quicklook}@A quick look at Tracy Profiler@g' _tmp.tex
sed -i -e 's@\\nameref{firststeps}@First steps@g' _tmp.tex
sed -i -e 's@\\nameref{client}@Client markup@g' _tmp.tex
sed -i -e 's@\\nameref{capturing}@Capturing the data@g' _tmp.tex
sed -i -e 's@\\nameref{analyzingdata}@Analyzing captured data@g' _tmp.tex
sed -i -e 's@\\nameref{tracyassist}@Tracy Assist@g' _tmp.tex
sed -i -e 's@\\nameref{csvexport}@Exporting zone statistics to CSV@g' _tmp.tex
sed -i -e 's@\\nameref{importingdata}@Importing external profiling data@g' _tmp.tex
sed -i -e 's@\\nameref{configurationfiles}@Configuration files@g' _tmp.tex
awk -f bclogo2quote.awk _tmp.tex > _tmp_quoted.tex
mv _tmp_quoted.tex _tmp.tex
pandoc --wrap=none --reference-location=block --number-sections -L filter.lua -t 'markdown-simple_tables-multiline_tables-grid_tables+pipe_tables' -s _tmp.tex -o tracy.md
awk -f tablecaption.awk tracy.md > _tmp_caption.md
mv _tmp_caption.md tracy.md
sed -i -e 's/^> \*\*IMPORTANT:\([^*]*\)\*\*/> [!IMPORTANT]\
> **\1**/' tracy.md
sed -i -e 's/^> \*\*TIP:\([^*]*\)\*\*/> [!TIP]\
> **\1**/' tracy.md
sed -i -e 's/^> \*\*CAUTION:\([^*]*\)\*\*/> [!CAUTION]\
> **\1**/' tracy.md
sed -i -e 's/^> \*\*NOTE:\([^*]*\)\*\*/> [!NOTE]\
> **\1**/' tracy.md
python3 icon-explain.py ../profiler/src/profiler/IconsFontAwesome7.h tracy.md
rm -f _tmp.tex

View File

@@ -1,16 +0,0 @@
# Pandoc emits table captions as a line beginning with ": ", which GitHub
# renders literally instead of as a caption. Strip the marker and italicize
# the caption instead. Captions may span several physical lines when they
# contain a hard line break (a trailing backslash). Underscores are used for
# the emphasis so captions that already contain "*...*" markup are left intact.
!incap && /^: / {
incap = 1
$0 = "_" substr($0, 3)
}
incap && !/\\$/ {
print $0 "_"
incap = 0
next
}
incap { print; next }
{ print }

File diff suppressed because it is too large Load Diff

View File

@@ -804,6 +804,8 @@ Suppose for some reason you want to use another port\footnote{For example, other
If a custom port is not specified and the default listening port is already occupied, the profiler will automatically try to listen on a number of other ports.
Since the port in use may differ from the default one, the client application may query the actual data connection port using the \texttt{TracyPort} macro. It returns the port number once the listening socket has been bound, or zero otherwise.
\begin{bclogo}[
noborder=true,
couleur=black!5,
@@ -3329,6 +3331,43 @@ Each client is captured to a separate file in the output directory, named accord
Press \keys{\ctrl + C} to stop discovery and gracefully shut down all active captures. Each capture thread will finish writing its trace file before the daemon exits.
\subsection{External monitoring}
\label{externalmonitoring}
Sometimes, the application you want to profile cannot be built with the Tracy client. This is often the case with third-party or closed-source software, running system services, or programs written in languages without Tracy bindings. For such situations, there is the \texttt{tracy-monitor} utility, contained in the \texttt{monitor} directory. The monitor profiles a process from the outside, without modifying it in any way, on Linux (x86-64 and ARM64).
The monitor samples the target in the same manner as the compiled-in client (see section~\ref{sampling}), so a capture contains:
\begin{itemize}
\item Sampled call stacks of every thread of the target, resolved to function names, source file and line, including inlined functions. As with regular call stack sampling (chapter~\ref{collectingcallstacks}), the target's executable and shared libraries should carry symbol information for the resolution to work, and programs compiled without frame pointers (typical of optimized builds) will yield call stacks truncated to the innermost function.
\item Machine code of the profiled functions, so that the disassembly view (section~\ref{executableretrieval}) is fully functional, exactly as in a regular capture.
\item System-wide CPU usage plots.
\item Depending on your privileges and system configuration: kernel frames in the call stacks, context switches and wait stacks (sections~\ref{contextswitches} and \ref{waitstacks}), per-thread CPU time, power consumption (RAPL) plots, and hardware vsync events.
\end{itemize}
The monitor can either start the program you want to profile, or attach to an already running one:
\begin{lstlisting}[language=sh]
$ tracy-monitor ./my_program arg1 arg2
$ tracy-monitor -p 12345
$ tracy-monitor -n my_program
\end{lstlisting}
In the first form, the monitor launches the program and the profiling begins before its first instruction executes, so the entire lifetime of the program is covered. In this mode, stopping the monitor (\keys{\ctrl + C}) also terminates the profiled program.
The \texttt{-p} option attaches to an already running process by its ID, and \texttt{-n} does the same by its name (the name as known to the operating system, truncated to 15 characters; if several processes share the name, the monitor lists their IDs so you can pick one with \texttt{-p}). In attach mode, the target is left running when the monitor is stopped.
The following options are also available:
\begin{itemize}
\item \texttt{-{}-hz N} -- sampling frequency in Hz (default: 10000, range 1--1000000).
\item \texttt{-{}-port N} -- the port on which the monitor accepts connections from the profiler (default: 8086).
\end{itemize}
After starting, the monitor prints a short report listing the data sources that are actually active in your environment (sampling rate, kernel frames, context switches, hardware sampling statistics, power, vsync), together with the reason for any that are unavailable.
The monitor can also be built with the \texttt{TRACY\_ON\_DEMAND} define (section~\ref{ondemand}): in that case, samples are captured only while a Tracy server is connected, and samples taken before the first connection are discarded.
\subsection{Merging trace files}
\label{mergingtraces}

View File

@@ -1,4 +1,4 @@
project('tracy', ['cpp'], version: run_command('sh', files('extra/tracy-version.sh'), 'public/common/TracyVersion.hpp', check: true).stdout().strip(), meson_version: '>=1.3.0', default_options : ['cpp_std=c++11'])
project('tracy', ['cpp'], version: run_command(find_program('python3'), files('extra/tracy-version.py'), 'public/common/TracyVersion.hpp', check: true).stdout().strip(), meson_version: '>=1.3.0', default_options : ['cpp_std=c++11'])
# internal compiler flags
tracy_compile_args = []
@@ -227,10 +227,16 @@ tracy_compile_args += tracy_common_args
tracy_deps = [dependency('threads')] + tracy_public_deps
tracy_link_args = []
if host_machine.system() == 'windows' and (compiler.get_id() == 'gcc' or compiler.get_id() == 'clang')
tracy_link_args += ['-lws2_32', '-ldbghelp', '-lsecur32']
endif
tracy = library('tracy', tracy_src, tracy_header_files,
dependencies : tracy_deps,
include_directories : tracy_public_include_dirs,
cpp_args : tracy_compile_args,
link_args : tracy_link_args,
override_options : override_options,
install : true)

View File

@@ -12,10 +12,17 @@ project(
VERSION ${TRACY_VERSION_STRING}
)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(FATAL_ERROR "tracy-monitor is Linux-only")
endif()
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|aarch64|arm64)$")
message(FATAL_ERROR "tracy-monitor supports x86_64 and aarch64")
endif()
find_package(Threads REQUIRED)
option(TRACY_ON_DEMAND "On-demand profiling: record samples only while a Tracy server is connected" OFF)
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/config.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/vendor.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/GitRef.cmake)
set(PROGRAM_FILES
@@ -31,6 +38,9 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE
TRACY_MANUAL_LIFETIME
TRACY_NO_FRAME_IMAGE
)
if(TRACY_ON_DEMAND)
target_compile_definitions(${PROJECT_NAME} PRIVATE TRACY_ON_DEMAND)
endif()
target_link_libraries(${PROJECT_NAME} PRIVATE
Threads::Threads
${CMAKE_DL_LIBS}

File diff suppressed because it is too large Load Diff

View File

@@ -39,6 +39,7 @@ include(${CMAKE_CURRENT_LIST_DIR}/../cmake/config.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/vendor.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/server.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/GitRef.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/manual.cmake)
include(ExternalProject)
ExternalProject_Add(embed
@@ -48,18 +49,26 @@ ExternalProject_Add(embed
-DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}
)
# Embeds a file into the profiler as a compressed C array (data/NAME.cpp/.hpp).
# FILE is relative to the current source directory; with BUILD, relative to
# the current build directory. With TEXT, line endings are normalized to unix.
function(Embed LIST NAME FILE)
cmake_parse_arguments(EMBED "TEXT" "" "" ${ARGN})
cmake_parse_arguments(EMBED "TEXT;BUILD" "" "" ${ARGN})
if(EMBED_TEXT)
set(EMBED_FLAGS -t)
else()
set(EMBED_FLAGS)
endif()
if(EMBED_BUILD)
set(EMBED_SOURCE ${CMAKE_CURRENT_BINARY_DIR}/${FILE})
else()
set(EMBED_SOURCE ${CMAKE_CURRENT_LIST_DIR}/${FILE})
endif()
add_custom_command(
OUTPUT data/${NAME}.cpp data/${NAME}.hpp
COMMAND ${CMAKE_COMMAND} -E make_directory data
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/embed ${EMBED_FLAGS} ${NAME} ${CMAKE_CURRENT_LIST_DIR}/${FILE} data/${NAME}
DEPENDS embed ${CMAKE_CURRENT_LIST_DIR}/${FILE}
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/embed ${EMBED_FLAGS} ${NAME} ${EMBED_SOURCE} data/${NAME}
DEPENDS embed ${EMBED_SOURCE}
)
list(APPEND ${LIST} data/${NAME}.cpp)
return(PROPAGATE ${LIST})
@@ -173,8 +182,23 @@ Embed(PROFILER_FILES FontBold src/font/Roboto-Bold.ttf)
Embed(PROFILER_FILES FontItalic src/font/Roboto-Italic.ttf)
Embed(PROFILER_FILES FontBoldItalic src/font/Roboto-BoldItalic.ttf)
Embed(PROFILER_FILES FontEmoji src/font/NotoEmoji-Regular.ttf)
set(MANUAL_MD ${CMAKE_CURRENT_BINARY_DIR}/manual/tracy.md)
add_custom_command(
OUTPUT ${MANUAL_MD}
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_LIST_DIR}/../manual/latex2md.py
--tex ${CMAKE_CURRENT_LIST_DIR}/../manual/tracy.tex
--out ${MANUAL_MD}
--pandoc ${TRACY_PANDOC}
--icons ${CMAKE_CURRENT_LIST_DIR}/src/profiler/IconsFontAwesome7.h
DEPENDS
${CMAKE_CURRENT_LIST_DIR}/../manual/tracy.tex
${CMAKE_CURRENT_LIST_DIR}/../manual/filter.lua
${CMAKE_CURRENT_LIST_DIR}/../manual/latex2md.py
${CMAKE_CURRENT_LIST_DIR}/src/profiler/IconsFontAwesome7.h
COMMENT "Generating user manual"
VERBATIM)
Embed(PROFILER_FILES Manual ../manual/tracy.md TEXT)
Embed(PROFILER_FILES Manual manual/tracy.md TEXT BUILD)
Embed(PROFILER_FILES Text100Million src/achievements/100Million.md TEXT)
Embed(PROFILER_FILES TextConnectToClient src/achievements/ConnectToClient.md TEXT)

View File

@@ -346,7 +346,13 @@ void DestroyImageCaches()
}
#ifdef __linux__
#ifdef TRACY_HAS_EXTERNAL_TARGET
# include <errno.h>
# include <fcntl.h>
# include <signal.h>
# include <sys/stat.h>
# include <sys/uio.h>
# include <unistd.h>
static constexpr uint32_t ExtPT_LOAD = 1;
@@ -355,22 +361,43 @@ struct ExternalImageEntry
uint64_t startAddress;
uint64_t endAddress;
uint64_t loadBias;
uint64_t mapsOffset;
char* path;
backtrace_state* btState;
bool btAttempted;
};
static FastVector<ExternalImageEntry>* s_extImages = nullptr;
static pid_t s_externalPid = 0;
static bool s_extImagesSorted = true;
static pid_t s_externalTargetPid = 0;
static char s_externalTargetName[64] = {};
static uint64_t s_externalTargetExeMtime = 0;
// Wall-clock second of the last /proc/<pid>/maps re-parse. Used to rate-limit
// refreshes so addresses that never resolve (JIT, vDSO, stack) do not trigger
// a full re-parse on every symbolization.
static int64_t s_lastMapsRefresh = 0;
static uint64_t ReadElfMinLoadVaddr( const char* path )
static int MakeExternalTargetPath( char* buf, size_t bufSize, const char* targetPath )
{
int fd = open( path, O_RDONLY );
const int n = snprintf( buf, bufSize, "/proc/%d/root%s", (int)s_externalTargetPid, targetPath );
return ( n < 0 || (size_t)n >= bufSize ) ? -1 : n;
}
static int OpenExternalImageFile( const char* path, uint64_t mapStart, uint64_t mapEnd )
{
char rootPath[4096];
if( MakeExternalTargetPath( rootPath, sizeof( rootPath ), path ) >= 0 )
{
const int fd = open( rootPath, O_RDONLY );
if( fd >= 0 ) return fd;
}
char mfPath[80];
snprintf( mfPath, sizeof( mfPath ), "/proc/%d/map_files/%lx-%lx", (int)s_externalTargetPid, (unsigned long)mapStart, (unsigned long)mapEnd );
return open( mfPath, O_RDONLY );
}
static uint64_t ReadElfMinLoadVaddr( const char* path, uint64_t mapStart, uint64_t mapEnd )
{
const int fd = OpenExternalImageFile( path, mapStart, mapEnd );
if( fd < 0 ) return UINT64_MAX;
elf_ehdr ehdr;
@@ -411,6 +438,46 @@ static uint64_t ReadElfMinLoadVaddr( const char* path )
return minVaddr;
}
static uint64_t ReadElfSegmentLoadBias( const char* path, uint64_t start, uint64_t end, uint64_t offset, uint64_t pageSize )
{
const int fd = OpenExternalImageFile( path, start, end );
if( fd < 0 ) return UINT64_MAX;
elf_ehdr ehdr;
if( read( fd, &ehdr, sizeof( ehdr ) ) != sizeof( ehdr ) ||
ehdr.e_ident[0] != 0x7f || ehdr.e_ident[1] != 'E' ||
ehdr.e_ident[2] != 'L' || ehdr.e_ident[3] != 'F' ||
ehdr.e_ident[4] != 2 ||
ehdr.e_phoff == 0 || ehdr.e_phnum == 0 )
{
close( fd );
return UINT64_MAX;
}
if( lseek( fd, ehdr.e_phoff, SEEK_SET ) == (off_t)-1 )
{
close( fd );
return UINT64_MAX;
}
uint64_t loadBias = UINT64_MAX;
for( uint16_t i = 0; i < ehdr.e_phnum; i++ )
{
elf_phdr phdr;
if( read( fd, &phdr, sizeof( phdr ) ) != sizeof( phdr ) ) break;
if( phdr.p_type != ExtPT_LOAD ) continue;
const uint64_t vaddr = static_cast<uint64_t>( phdr.p_vaddr );
if( static_cast<uint64_t>( phdr.p_offset ) - ( vaddr & ( pageSize - 1 ) ) == offset )
{
loadBias = start - ( vaddr & ~( pageSize - 1 ) );
break;
}
}
close( fd );
return loadBias;
}
static void ParseExternalProcMaps( pid_t pid )
{
char mapPath[64];
@@ -418,6 +485,8 @@ static void ParseExternalProcMaps( pid_t pid )
FILE* f = fopen( mapPath, "r" );
if( !f ) return;
FastVector<ExternalImageEntry> fresh( 64 );
char line[1024];
while( fgets( line, sizeof( line ), f ) )
{
@@ -434,46 +503,48 @@ static void ParseExternalProcMaps( pid_t pid )
while( *pathname == ' ' || *pathname == '\t' ) pathname++;
size_t plen = strlen( pathname );
while( plen > 0 && ( pathname[plen-1] == '\n' || pathname[plen-1] == '\r' ) ) plen--;
if( plen >= 10 && strncmp( pathname + plen - 10, " (deleted)", 10 ) == 0 ) plen -= 10;
pathname[plen] = '\0';
if( plen == 0 || pathname[0] != '/' ) continue;
if( std::find_if( s_extImages->begin(), s_extImages->end(), [start]( const ExternalImageEntry& e ) { return e.startAddress == start; } ) != s_extImages->end() ) continue;
uint64_t minVaddr = ReadElfMinLoadVaddr( pathname );
uint64_t loadBias;
if( minVaddr == UINT64_MAX )
// list is sorted by start address
auto it = std::lower_bound( s_extImages->begin(), s_extImages->end(), start,
[]( const ExternalImageEntry& e, uint64_t a ) { return e.startAddress > a; } );
if( it != s_extImages->end() && it->startAddress == start
&& it->endAddress == end && it->mapsOffset == offset
&& strcmp( it->path, pathname ) == 0 )
{
loadBias = start;
fresh.push_next()[0] = *it;
continue;
}
else
uint64_t pageSize = sysconf( _SC_PAGESIZE );
uint64_t loadBias = ReadElfSegmentLoadBias( pathname, start, end, offset, pageSize );
if( loadBias == UINT64_MAX )
{
uint64_t pageSize = sysconf( _SC_PAGESIZE );
uint64_t alignedVaddr = minVaddr & ~(pageSize - 1);
loadBias = start - alignedVaddr - offset;
uint64_t minVaddr = ReadElfMinLoadVaddr( pathname, start, end );
loadBias = ( minVaddr == UINT64_MAX ) ? start : start - ( minVaddr & ~( pageSize - 1 ) ) - offset;
}
ExternalImageEntry entry = {
.startAddress = start,
.endAddress = end,
.loadBias = loadBias,
.mapsOffset = offset,
.path = (char*)tracy_malloc( plen + 1 ),
.btState = nullptr,
.btAttempted = false
};
memcpy( entry.path, pathname, plen + 1 );
s_extImagesSorted = false;
s_extImages->push_next()[0] = entry;
fresh.push_next()[0] = entry;
}
fclose( f );
if( !s_extImagesSorted )
{
std::sort( s_extImages->begin(), s_extImages->end(),
[]( const ExternalImageEntry& a, const ExternalImageEntry& b ) { return a.startAddress > b.startAddress; } );
s_extImagesSorted = true;
}
std::sort( fresh.begin(), fresh.end(),
[]( const ExternalImageEntry& a, const ExternalImageEntry& b ) { return a.startAddress > b.startAddress; } );
s_extImages->swap( fresh );
}
static const ExternalImageEntry* FindExternalImage( uint64_t address )
@@ -495,13 +566,13 @@ static const ExternalImageEntry* FindExternalImageRefresh( uint64_t address )
auto entry = FindExternalImage( address );
if( entry ) return entry;
if( s_externalPid != 0 )
if( s_externalTargetPid != 0 )
{
const int64_t now = (int64_t)time( nullptr );
if( now != s_lastMapsRefresh )
{
s_lastMapsRefresh = now;
ParseExternalProcMaps( s_externalPid );
ParseExternalProcMaps( s_externalTargetPid );
return FindExternalImage( address );
}
}
@@ -517,40 +588,37 @@ static backtrace_state* GetExternalBtState( const ExternalImageEntry* entry )
auto* e = const_cast<ExternalImageEntry*>( entry );
if( e->btAttempted ) return e->btState;
e->btAttempted = true;
e->btState = backtrace_create_state_for_file( e->path, 0, ExternalBacktraceErrorCb, nullptr );
const size_t rootPathSize = strlen( e->path ) + 32;
char* rootPath = (char*)tracy_malloc( rootPathSize );
const char* statePath = nullptr;
char mfPath[80];
if( MakeExternalTargetPath( rootPath, rootPathSize, e->path ) >= 0 )
{
const int probe = open( rootPath, O_RDONLY );
if( probe >= 0 )
{
close( probe );
statePath = rootPath;
}
}
if( !statePath )
{
snprintf( mfPath, sizeof( mfPath ), "/proc/%d/map_files/%lx-%lx", (int)s_externalTargetPid, (unsigned long)e->startAddress, (unsigned long)e->endAddress );
const int probe = open( mfPath, O_RDONLY );
if( probe >= 0 )
{
close( probe );
statePath = mfPath;
}
}
if( statePath )
{
e->btState = backtrace_create_state_for_file( statePath, 0, ExternalBacktraceErrorCb, nullptr );
}
tracy_free( rootPath );
return e->btState;
}
struct ExternalResolveData
{
const char* name;
const char* file;
uint32_t line;
int count;
};
static int ExternalPcInfoCb( void* data, uintptr_t pc, uintptr_t lowaddr, const char* filename, int lineno, const char* function )
{
auto& rd = *(ExternalResolveData*)data;
if( rd.count > 0 ) return 1;
rd.count++;
if( function )
{
const char* demangled = ___tracy_demangle( function );
rd.name = demangled ? demangled : function;
}
else
{
rd.name = nullptr;
}
rd.file = filename;
rd.line = lineno;
return 0;
}
struct ExternalSymInfoData
{
@@ -567,18 +635,143 @@ static void ExternalSymInfoCb( void* data, uintptr_t pc, const char* symname, ui
sd.symsize = symsize;
}
void InitExternalImageCache( pid_t pid )
bool InitExternalTarget( pid_t targetPid )
{
s_externalPid = pid;
if( kill( targetPid, 0 ) != 0 )
{
fprintf( stderr, "Tracy: cannot profile pid %d: %s\n", (int)targetPid, strerror( errno ) );
return false;
}
char path[64];
snprintf( path, sizeof( path ), "/proc/%d/comm", (int)targetPid );
FILE* f = fopen( path, "r" );
if( !f )
{
fprintf( stderr, "Tracy: cannot read %s: %s\n", path, strerror( errno ) );
return false;
}
char comm[64] = {};
if( !fgets( comm, sizeof( comm ), f ) )
{
fclose( f );
fprintf( stderr, "Tracy: cannot read %s: %s\n", path, strerror( errno ) );
return false;
}
fclose( f );
size_t len = strlen( comm );
while( len > 0 && ( comm[len-1] == '\n' || comm[len-1] == '\r' ) ) len--;
if( len >= sizeof( s_externalTargetName ) ) len = sizeof( s_externalTargetName ) - 1;
memcpy( s_externalTargetName, comm, len );
s_externalTargetName[len] = '\0';
snprintf( path, sizeof( path ), "/proc/%d/exe", (int)targetPid );
{
const int exeFd = open( path, O_RDONLY );
if( exeFd < 0 )
{
fprintf( stderr, "Tracy: cannot read %s: %s\n", path, strerror( errno ) );
return false;
}
struct stat exeSt;
if( fstat( exeFd, &exeSt ) == 0 ) s_externalTargetExeMtime = (uint64_t)exeSt.st_mtime;
close( exeFd );
}
s_externalTargetPid = targetPid;
if( !s_extImages )
{
s_extImages = (FastVector<ExternalImageEntry>*)tracy_malloc( sizeof( FastVector<ExternalImageEntry> ) );
new (s_extImages) FastVector<ExternalImageEntry>( 64 );
}
ParseExternalProcMaps( pid );
ParseExternalProcMaps( targetPid );
return true;
}
#endif // __linux__
uint32_t GetExternalTargetPid()
{
return (uint32_t)s_externalTargetPid;
}
const char* GetExternalTargetName()
{
return s_externalTargetName;
}
uint64_t GetExternalTargetExeTime()
{
return s_externalTargetExeMtime;
}
static bool FindExternalMapping( pid_t pid, uint64_t addr, uint64_t& mapStart, uint64_t& mapEnd, uint64_t& fileOff, char* path, size_t pathSize )
{
char mapPath[64];
snprintf( mapPath, sizeof( mapPath ), "/proc/%d/maps", (int)pid );
FILE* f = fopen( mapPath, "r" );
if( !f ) return false;
bool found = false;
char line[1024];
while( fgets( line, sizeof( line ), f ) )
{
uint64_t start, end, offset;
uint32_t devMaj, devMin;
uint64_t inode;
char perms[8];
int consumed = 0;
if( sscanf( line, "%lx-%lx %7s %lx %x:%x %lu %n", &start, &end, perms, &offset, &devMaj, &devMin, &inode, &consumed ) < 7 ) continue;
if( !strchr( perms, 'x' ) ) continue;
if( addr < start || addr >= end ) continue;
char* pathname = line + consumed;
while( *pathname == ' ' || *pathname == '\t' ) pathname++;
size_t plen = strlen( pathname );
while( plen > 0 && ( pathname[plen-1] == '\n' || pathname[plen-1] == '\r' ) ) plen--;
pathname[plen] = '\0';
if( plen >= 10 && strncmp( pathname + plen - 10, " (deleted)", 10 ) == 0 ) plen -= 10;
if( plen == 0 || pathname[0] != '/' ) continue;
if( plen >= pathSize ) plen = pathSize - 1;
memcpy( path, pathname, plen );
path[plen] = '\0';
mapStart = start;
mapEnd = end;
fileOff = offset + ( addr - start );
found = true;
break;
}
fclose( f );
return found;
}
size_t ReadExternalTargetMemory( uint64_t addr, uint32_t size, char* buf )
{
const auto pid = (pid_t)GetExternalTargetPid();
if( pid == 0 || size == 0 ) return 0;
struct iovec local = { buf, size };
struct iovec remote = { (void*)addr, size };
if( process_vm_readv( pid, &local, 1, &remote, 1, 0 ) == (ssize_t)size ) return size;
uint64_t mapStart = 0, mapEnd = 0, fileOff = 0;
char path[1024] = {};
if( FindExternalMapping( pid, addr, mapStart, mapEnd, fileOff, path, sizeof( path ) ) && addr + size <= mapEnd )
{
const int fd = OpenExternalImageFile( path, mapStart, mapEnd );
if( fd >= 0 )
{
const ssize_t rd = pread( fd, buf, size, (off_t)fileOff );
close( fd );
if( rd == (ssize_t)size ) return size;
}
}
return 0;
}
#endif // TRACY_HAS_EXTERNAL_TARGET
// when "TRACY_SYMBOL_OFFLINE_RESOLVE" is set, instead of fully resolving symbols at runtime,
@@ -1445,7 +1638,7 @@ void EndCallstack()
#endif
}
#ifdef __linux__
#ifdef TRACY_HAS_EXTERNAL_TARGET
static const char* DecodeCallstackPtrFastExternal( uint64_t ptr )
{
static char ret[1024];
@@ -1484,8 +1677,8 @@ const char* DecodeCallstackPtrFast( uint64_t ptr )
{
static char ret[1024];
#ifdef __linux__
if( s_externalPid != 0 && s_extImages ) return DecodeCallstackPtrFastExternal( ptr );
#ifdef TRACY_HAS_EXTERNAL_TARGET
if( s_externalTargetPid != 0 && s_extImages ) return DecodeCallstackPtrFastExternal( ptr );
#endif
auto vptr = (void*)ptr;
@@ -1535,7 +1728,7 @@ static void SymbolAddressErrorCb( void* data, const char* /*msg*/, int /*errnum*
sym.needFree = false;
}
#ifdef __linux__
#ifdef TRACY_HAS_EXTERNAL_TARGET
static CallstackSymbolData DecodeSymbolAddressExternal( uint64_t ptr )
{
CallstackSymbolData sym;
@@ -1559,8 +1752,8 @@ CallstackSymbolData DecodeSymbolAddress( uint64_t ptr )
{
CallstackSymbolData sym;
#ifdef __linux__
if( s_externalPid != 0 && s_extImages ) return DecodeSymbolAddressExternal( ptr );
#ifdef TRACY_HAS_EXTERNAL_TARGET
if( s_externalTargetPid != 0 && s_extImages ) return DecodeSymbolAddressExternal( ptr );
#endif
if( cb_bts )
@@ -1685,105 +1878,127 @@ void GetSymbolForOfflineResolve(void* address, uint64_t imageBaseAddress, Callst
cbEntry.line = 0;
}
#ifdef __linux__
CallstackEntryData DecodeCallstackPtrExternal( uint64_t ptr )
#ifdef TRACY_HAS_EXTERNAL_TARGET
static int ExternalCallstackDataCb( void* data, uintptr_t /*pc*/, uintptr_t lowaddr, const char* fn, int lineno, const char* function )
{
const auto* extImg = FindExternalImageRefresh( ptr );
if( extImg )
auto* img = (const ExternalImageEntry*)data;
cb_data[cb_num].symLen = 0;
cb_data[cb_num].symAddr = (uint64_t)img->loadBias + (uint64_t)lowaddr;
if( !fn && !function )
{
const char* imageName = extImg->path;
// Convert VMA (target process virtual address) to ELF virtual address.
// elf_vaddr = vma - load_bias
// libbacktrace indexes DWARF data by ELF virtual address when
// the backtrace_state is created from a file (base_address=0).
const auto elfVaddr = (uintptr_t)( ptr - extImg->loadBias );
auto* bts = GetExternalBtState( extImg );
if( bts )
// no symbol from pcinfo: name stays null, repaired from the symtab by ResolveExternalCallstack
cb_data[cb_num].name = nullptr;
cb_data[cb_num].file = nullptr;
cb_data[cb_num].line = 0;
}
else
{
if( !fn ) fn = "[unknown]";
if( !function )
{
// Try DWARF-based resolution
ExternalResolveData rd = {};
backtrace_pcinfo( bts, elfVaddr, ExternalPcInfoCb, ExternalBacktraceErrorCb, &rd );
function = "[unknown]";
}
else
{
const char* demangled = ___tracy_demangle( function );
if( demangled ) function = demangled;
}
if( rd.name || rd.file )
{
cb_num = 1;
if( rd.name )
{
const auto len = std::min<size_t>( strlen( rd.name ), std::numeric_limits<uint16_t>::max() );
cb_data[0].name = CopyStringFast( rd.name, len );
}
else
{
cb_data[0].name = CopyStringFast( "[unknown]" );
}
if( rd.file )
{
cb_data[0].file = NormalizePath( rd.file );
if( !cb_data[0].file ) cb_data[0].file = CopyStringFast( rd.file );
}
else
{
cb_data[0].file = CopyStringFast( "[unknown]" );
}
cb_data[0].line = rd.line;
cb_data[0].symLen = 0;
cb_data[0].symAddr = elfVaddr;
const auto len = std::min<size_t>( strlen( function ), std::numeric_limits<uint16_t>::max() );
cb_data[cb_num].name = CopyStringFast( function, len );
cb_data[cb_num].file = NormalizePath( fn );
if( !cb_data[cb_num].file ) cb_data[cb_num].file = CopyStringFast( fn );
cb_data[cb_num].line = lineno;
}
// Try to get symbol size info
ExternalSymInfoData sid = {};
backtrace_syminfo( bts, elfVaddr, ExternalSymInfoCb, ExternalBacktraceErrorCb, &sid );
if( sid.symsize > 0 )
{
cb_data[0].symLen = (uint32_t)sid.symsize;
cb_data[0].symAddr = (uint64_t)sid.symval;
}
if( ++cb_num >= MaxCbTrace )
{
return 1;
}
else
{
return 0;
}
}
// If DWARF gave us no function name, try the symbol table
if( !rd.name && sid.symname )
{
tracy_free_fast( (void*)cb_data[0].name );
const char* demangled = ___tracy_demangle( sid.symname );
if( demangled )
{
cb_data[0].name = CopyStringFast( demangled );
}
else
{
cb_data[0].name = CopyStringFast( sid.symname );
}
}
static void ExternalPcinfoErrorCb( void* /*data*/, const char* /*msg*/, int /*errnum*/ )
{
for( int i=0; i<cb_num; i++ )
{
tracy_free_fast( (void*)cb_data[i].name );
tracy_free_fast( (void*)cb_data[i].file );
}
cb_num = 0;
}
return { cb_data, 1, imageName ? imageName : "[unknown]" };
}
static CallstackEntryData ResolveExternalCallstack( const ExternalImageEntry* img, uint64_t vma )
{
const char* imageName = img->path ? img->path : "[unknown]";
// DWARF resolution failed; try symtab-only fallback
const auto elfVaddr = (uintptr_t)( vma - img->loadBias );
auto* bts = GetExternalBtState( img );
if( bts )
{
cb_num = 0;
backtrace_pcinfo( bts, elfVaddr, ExternalCallstackDataCb, ExternalPcinfoErrorCb, const_cast<ExternalImageEntry*>( img ) );
if( cb_num > 0 )
{
ExternalSymInfoData sid = {};
backtrace_syminfo( bts, elfVaddr, ExternalSymInfoCb, ExternalBacktraceErrorCb, &sid );
if( sid.symname )
{
cb_num = 1;
const char* demangled = ___tracy_demangle( sid.symname );
cb_data[0].name = CopyStringFast( demangled ? demangled : sid.symname );
cb_data[0].file = CopyStringFast( imageName ? imageName : "[unknown]" );
cb_data[0].line = 0;
cb_data[0].symLen = (uint32_t)sid.symsize;
cb_data[0].symAddr = (uint64_t)sid.symval;
return { cb_data, 1, imageName ? imageName : "[unknown]" };
cb_data[cb_num-1].symLen = (uint32_t)sid.symsize;
cb_data[cb_num-1].symAddr = (uint64_t)img->loadBias + (uint64_t)sid.symval;
if( !cb_data[cb_num-1].name )
{
const char* demangled = ___tracy_demangle( sid.symname );
cb_data[cb_num-1].name = CopyStringFast( demangled ? demangled : sid.symname );
cb_data[cb_num-1].file = CopyStringFast( imageName );
}
}
else if( !cb_data[cb_num-1].name )
{
cb_data[cb_num-1].name = CopyStringFast( "[unresolved]" );
cb_data[cb_num-1].file = CopyStringFast( imageName );
cb_data[cb_num-1].symLen = 0;
cb_data[cb_num-1].symAddr = vma;
}
return { cb_data, uint8_t( cb_num ), imageName };
}
// Fallback: return unresolved with offset
cb_num = 1;
cb_data[0].name = CopyStringFast( "[unresolved]" );
cb_data[0].file = CopyStringFast( imageName ? imageName : "[unknown]" );
cb_data[0].line = 0;
cb_data[0].symLen = 0;
cb_data[0].symAddr = elfVaddr;
return { cb_data, 1, imageName ? imageName : "[unknown]" };
ExternalSymInfoData sid = {};
backtrace_syminfo( bts, elfVaddr, ExternalSymInfoCb, ExternalBacktraceErrorCb, &sid );
if( sid.symname )
{
cb_num = 1;
const char* demangled = ___tracy_demangle( sid.symname );
cb_data[0].name = CopyStringFast( demangled ? demangled : sid.symname );
cb_data[0].file = CopyStringFast( imageName );
cb_data[0].line = 0;
cb_data[0].symLen = (uint32_t)sid.symsize;
cb_data[0].symAddr = (uint64_t)img->loadBias + (uint64_t)sid.symval;
return { cb_data, 1, imageName };
}
}
cb_num = 1;
cb_data[0].name = CopyStringFast( "[unresolved]" );
cb_data[0].file = CopyStringFast( imageName );
cb_data[0].line = 0;
cb_data[0].symLen = 0;
cb_data[0].symAddr = vma;
return { cb_data, 1, imageName };
}
CallstackEntryData DecodeCallstackPtrExternal( uint64_t ptr )
{
const auto* extImg = FindExternalImageRefresh( ptr );
if( extImg ) return ResolveExternalCallstack( extImg, ptr );
// Address doesn't belong to any known mapping
cb_num = 1;
cb_data[0].name = CopyStringFast( "[unknown]" );
@@ -1800,8 +2015,8 @@ CallstackEntryData DecodeCallstackPtr( uint64_t ptr )
InitAllocator();
if( !IsKernelAddress( ptr ) )
{
#ifdef __linux__
if( s_externalPid != 0 && s_extImages ) return DecodeCallstackPtrExternal( ptr );
#ifdef TRACY_HAS_EXTERNAL_TARGET
if( s_externalTargetPid != 0 && s_extImages ) return DecodeCallstackPtrExternal( ptr );
#endif
const char* imageName = nullptr;

View File

@@ -7,6 +7,13 @@
#include "../common/TracyForceInline.hpp"
#include "TracyCallstack.h"
// External target API: desktop Linux only. The tracy-monitor that drives
// it builds for Linux x86_64/aarch64 only, and Android, although __linux__,
// lacks process_vm_readv on 32-bit ABIs.
#if defined(__linux__) && !defined(__ANDROID__) && defined(TRACY_HAS_CALLSTACK)
# define TRACY_HAS_EXTERNAL_TARGET
#endif
namespace tracy
{
@@ -88,8 +95,13 @@ void InitCallstackCritical();
void EndCallstack();
const char* GetKernelModulePath( uint64_t addr );
#ifdef __linux__
void InitExternalImageCache( pid_t pid );
#ifdef TRACY_HAS_EXTERNAL_TARGET
bool InitExternalTarget( pid_t targetPid );
uint32_t GetExternalTargetPid();
const char* GetExternalTargetName();
uint64_t GetExternalTargetExeTime();
size_t ReadExternalTargetMemory( uint64_t addr, uint32_t size, char* buf );
#endif
#ifdef TRACY_DEBUGINFOD

View File

@@ -416,12 +416,11 @@ static int64_t SetupHwTimer()
}
#endif
uint32_t ___tracy_magic_pid_override = 0;
char ___tracy_magic_process_name[64] = {};
static const char* GetProcessName()
{
if( *___tracy_magic_process_name != 0 ) return ___tracy_magic_process_name;
#ifdef TRACY_HAS_EXTERNAL_TARGET
if( GetExternalTargetPid() != 0 ) return GetExternalTargetName();
#endif
const char* processName = "unknown";
#ifdef _WIN32
@@ -750,7 +749,10 @@ static const char* GetHostInfo()
static uint64_t GetPid()
{
if( ___tracy_magic_pid_override != 0 ) return uint64_t( ___tracy_magic_pid_override );
#ifdef TRACY_HAS_EXTERNAL_TARGET
const auto externalPid = GetExternalTargetPid();
if( externalPid != 0 ) return uint64_t( externalPid );
#endif
#if defined _WIN32
return uint64_t( GetCurrentProcessId() );
@@ -918,6 +920,7 @@ std::atomic<bool> s_symbolThreadGone { false };
#endif
#ifdef TRACY_HAS_SYSTEM_TRACING
static std::atomic<Thread*> s_sysTraceThread(nullptr);
static std::atomic<bool> s_sysTraceStartFailed(false);
#endif
#if defined __linux__ && !defined TRACY_NO_CRASH_HANDLER
@@ -1173,12 +1176,17 @@ static void StartSystemTracing( int64_t& samplingPeriod )
}
else if( SysTraceStart( samplingPeriod ) )
{
s_sysTraceStartFailed.store( false, std::memory_order_release );
Thread* sysTraceThread = (Thread*)tracy_malloc( sizeof( Thread ) );
new( sysTraceThread ) Thread( SysTraceWorker, nullptr );
Thread* prev = s_sysTraceThread.exchange( sysTraceThread );
TRACY_ASSERT( prev == nullptr );
std::this_thread::sleep_for( std::chrono::milliseconds( 1 ) );
}
else
{
s_sysTraceStartFailed.store( true, std::memory_order_release );
}
}
static void StopSystemTracing()
@@ -1510,6 +1518,26 @@ TRACY_API bool ProfilerAllocatorAvailable() { return !RpThreadShutdown; }
TRACY_API bool BeginSamplingProfiling() { return GetProfiler().BeginSamplingProfiling(); }
TRACY_API void EndSamplingProfiling() { return GetProfiler().EndSamplingProfiling(); }
TRACY_API bool IsSystemTracingFailed()
{
#if defined(TRACY_HAS_SYSTEM_TRACING)
return s_sysTraceStartFailed.load( std::memory_order_acquire );
#else
return false;
#endif
}
static std::atomic<int> s_reservedListenFd( -1 );
TRACY_API void SetReservedListenSocket( int fd )
{
TRACY_ASSERT( fd >= 0 );
s_reservedListenFd.store( fd, std::memory_order_release );
}
static std::atomic<bool> s_dataPortListening(false);
TRACY_API bool IsDataPortListening() { return s_dataPortListening.load( std::memory_order_acquire ); }
constexpr static size_t SafeSendBufferSize = 65536;
@@ -1524,6 +1552,7 @@ Profiler::Profiler()
, m_broadcast( nullptr )
, m_noExit( false )
, m_userPort( 0 )
, m_dataPort( 0 )
, m_zoneId( 1 )
, m_sectionId( 1 )
, m_samplingPeriod( 0 )
@@ -1795,13 +1824,22 @@ void Profiler::Worker()
#endif
m_exectime = 0;
const auto execname = GetProcessExecutablePath();
if( execname )
#ifdef TRACY_HAS_EXTERNAL_TARGET
if( GetExternalTargetPid() != 0 )
{
struct stat st;
if( stat( execname, &st ) == 0 )
m_exectime = GetExternalTargetExeTime();
}
else
#endif
{
const auto execname = GetProcessExecutablePath();
if( execname )
{
m_exectime = (uint64_t)st.st_mtime;
struct stat st;
if( stat( execname, &st ) == 0 )
{
m_exectime = (uint64_t)st.st_mtime;
}
}
}
@@ -1880,7 +1918,13 @@ void Profiler::Worker()
ListenSocket listen;
bool isListening = false;
if( !dataPortSearch )
const int reservedFd = s_reservedListenFd.exchange( -1, std::memory_order_acquire );
if( reservedFd != -1 )
{
listen.Adopt( reservedFd );
isListening = true;
}
else if( !dataPortSearch )
{
isListening = listen.Listen( dataPort, 4 );
}
@@ -1896,6 +1940,9 @@ void Profiler::Worker()
}
}
}
if( isListening ) dataPort = listen.LocalPort();
s_dataPortListening.store( isListening, std::memory_order_release );
if( !isListening )
{
for(;;)
@@ -1910,6 +1957,7 @@ void Profiler::Worker()
std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) );
}
}
m_dataPort.store( dataPort, std::memory_order_release );
#ifndef TRACY_NO_BROADCAST
m_broadcast = (UdpBroadcast*)tracy_malloc( sizeof( UdpBroadcast ) );
@@ -3741,7 +3789,6 @@ void Profiler::QueueKernelCode( uint64_t symbol, uint32_t size )
void Profiler::QueueSourceCodeQuery( uint32_t id )
{
TRACY_ASSERT( m_exectime != 0 );
TRACY_ASSERT( m_queryData );
m_symbolQueue.emplace( SymbolQueueItem { SymbolQueueItemType::SourceCode, uint64_t( m_queryData ), uint64_t( m_queryImage ), id } );
m_queryData = nullptr;
@@ -4430,29 +4477,35 @@ void Profiler::HandleParameter( uint64_t payload )
void Profiler::HandleSymbolCodeQuery( uint64_t symbol, uint32_t size )
{
#ifdef __linux__
// When profiling an external process, symbol addresses are ELF virtual
// addresses, not pointers in the monitor's address space. We cannot
// read code bytes directly.
if( ___tracy_magic_pid_override != 0 )
{
AckSymbolCodeNotAvailable();
return;
}
#endif
if( symbol >> 63 != 0 )
{
QueueKernelCode( symbol, size );
return;
}
else
{
auto&& lambda = [ this, symbol ]( const char* buf, size_t size ) {
SendLongString( symbol, buf, size, QueueType::SymbolCode );
};
// 'symbol' may have come from a module that has since unloaded, perform a safe copy before sending
if( !WithSafeCopy( (const char*)symbol, size, lambda ) ) AckSymbolCodeNotAvailable();
auto&& lambda = [ this, symbol ]( const char* buf, size_t size ) {
SendLongString( symbol, buf, size, QueueType::SymbolCode );
};
#ifdef TRACY_HAS_EXTERNAL_TARGET
if( GetExternalTargetPid() != 0 )
{
auto buf = (char*)tracy_malloc_fast( size );
if( ReadExternalTargetMemory( symbol, size, buf ) == size )
{
lambda( buf, size );
}
else
{
AckSymbolCodeNotAvailable();
}
tracy_free_fast( buf );
return;
}
#endif
// 'symbol' may have come from a module that has since unloaded, perform a safe copy before sending
if( !WithSafeCopy( (const char*)symbol, size, lambda ) ) AckSymbolCodeNotAvailable();
}
void Profiler::HandleSourceCodeQuery( char* data, char* image, uint32_t id )

View File

@@ -80,6 +80,11 @@ TRACY_API bool IsProfilerStarted();
TRACY_API bool BeginSamplingProfiling();
TRACY_API void EndSamplingProfiling();
TRACY_API bool IsSystemTracingFailed();
TRACY_API void SetReservedListenSocket( int fd );
TRACY_API bool IsDataPortListening();
class GpuCtx;
class Profiler;
@@ -909,6 +914,12 @@ public:
return m_isConnected.load( std::memory_order_acquire );
}
// Returns 0 until the listen socket is bound
tracy_force_inline uint32_t GetPort() const
{
return m_dataPort.load( std::memory_order_acquire );
}
tracy_force_inline void SetProgramName( const char* name )
{
m_programNameLock.lock();
@@ -1170,6 +1181,7 @@ private:
UdpBroadcast* m_broadcast;
bool m_noExit;
uint32_t m_userPort;
std::atomic<uint32_t> m_dataPort;
std::atomic<uint32_t> m_zoneId;
std::atomic<uint32_t> m_sectionId;
int64_t m_samplingPeriod;

View File

@@ -404,6 +404,7 @@ void SysTraceGetExternalName( uint64_t thread, const char*& threadName, const ch
# include "TracyCpuid.hpp"
# endif
# include "TracyCallstack.hpp"
# include "TracyProfiler.hpp"
# include "TracyRingBuffer.hpp"
# include "TracyThread.hpp"
@@ -419,12 +420,7 @@ static bool s_ctxSwitchCallchain = false;
static RingBuffer* s_ring = nullptr;
extern uint32_t ___tracy_magic_pid_override;
// (pid, cpu) pair for a per-task perf event open. In self-profiling mode we
// iterate one entry per CPU with pid = our tgid. In monitor mode we iterate
// one entry per existing thread of the target, with cpu = -1, so inherit=1
// can cover all descendants without multiplying ring buffers by CPU count.
struct PerfIterTarget
{
pid_t pid;
@@ -461,6 +457,12 @@ static int EnumerateTaskTids( pid_t pid, uint32_t** out )
tids[count++] = (uint32_t)tid;
}
closedir( dir );
if( count == 0 )
{
tracy_free( tids );
*out = nullptr;
return 0;
}
*out = tids;
return (int)count;
}
@@ -476,13 +478,16 @@ static bool CurrentProcOwnsThread( uint32_t tid )
if( hv == -tid ) return false;
char path[256];
if( ___tracy_magic_pid_override != 0 )
#ifdef TRACY_HAS_EXTERNAL_TARGET
const auto externalPid = GetExternalTargetPid();
if( externalPid != 0 )
{
sprintf( path, "/proc/%d/task/%d", (int)___tracy_magic_pid_override, tid );
sprintf( path, "/proc/%" PRIu32 "/task/%" PRIu32, externalPid, tid );
}
else
#endif
{
sprintf( path, "/proc/self/task/%d", tid );
sprintf( path, "/proc/self/task/%" PRIu32, tid );
}
struct stat st;
if( stat( path, &st ) == 0 )
@@ -516,6 +521,36 @@ enum TraceEventId
EventWaking,
};
static void ProbePreciseIp( perf_event_attr& pe, pid_t pid );
static bool OpenSampleEvent( const PerfIterTarget& tgt, const perf_event_attr& inPe, int eventId )
{
static bool noKernelAccessLogged = false;
perf_event_attr pe = inPe;
int fd = perf_event_open( &pe, tgt.pid, tgt.cpu, -1, PERF_FLAG_FD_CLOEXEC );
if( fd == -1 )
{
pe.exclude_kernel = 1;
pe.exclude_callchain_kernel = 1;
ProbePreciseIp( pe, tgt.pid );
fd = perf_event_open( &pe, tgt.pid, tgt.cpu, -1, PERF_FLAG_FD_CLOEXEC );
if( fd != -1 && !noKernelAccessLogged )
{
noKernelAccessLogged = true;
TracyDebug( " No access to kernel samples; user-space only (perf_event_paranoid / capabilities)" );
}
}
if( fd == -1 )
{
TracyDebug( " Failed to setup!" );
return false;
}
new( s_ring + s_numBuffers ) RingBuffer( 64 * 1024, fd, eventId );
if( !s_ring[s_numBuffers].IsValid() ) return false;
s_numBuffers++;
return true;
}
static void ProbePreciseIp( perf_event_attr& pe, unsigned long long config0, unsigned long long config1, pid_t pid )
{
pe.config = config1;
@@ -780,16 +815,19 @@ bool SysTraceStart( int64_t& samplingPeriod )
}
}
samplingPeriod = SamplingFrequencyToPeriodNs( samplingFrequency );
uint32_t currentPid = ___tracy_magic_pid_override != 0 ? ___tracy_magic_pid_override : (uint32_t)getpid();
#ifdef TRACY_HAS_EXTERNAL_TARGET
const auto externalPid = GetExternalTargetPid();
#else
const uint32_t externalPid = 0;
#endif
uint32_t currentPid = externalPid != 0 ? externalPid : (uint32_t)getpid();
s_numCpus = (int)std::thread::hardware_concurrency();
// Build the per-task iteration list. In monitor mode this is all existing
// threads of the target (one event per thread, any CPU); in self-profiling
// it is per-CPU bound to our own tgid.
PerfIterTarget* iter = nullptr;
int numIter = 0;
if( ___tracy_magic_pid_override != 0 )
PerfIterTarget* iter;
int numIter;
#ifdef TRACY_HAS_EXTERNAL_TARGET
if( externalPid != 0 )
{
uint32_t* tids = nullptr;
const int numTids = EnumerateTaskTids( (pid_t)currentPid, &tids );
@@ -798,13 +836,28 @@ bool SysTraceStart( int64_t& samplingPeriod )
TracyDebug( "Failed to enumerate threads of pid %u; target may have exited.", currentPid );
return false;
}
iter = (PerfIterTarget*)tracy_malloc( sizeof( PerfIterTarget ) * numTids );
for( int i=0; i<numTids; i++ ) iter[i] = { (pid_t)tids[i], -1 };
numIter = numTids;
if( numTids == 1 )
{
iter = (PerfIterTarget*)tracy_malloc( sizeof( PerfIterTarget ) * s_numCpus );
for( int i=0; i<s_numCpus; i++ ) iter[i] = { (pid_t)tids[0], i };
numIter = s_numCpus;
TracyDebug( "Monitor mode: per-CPU events on pid %u (launch)", currentPid );
}
else
{
iter = (PerfIterTarget*)tracy_malloc( sizeof( PerfIterTarget ) * numTids * s_numCpus );
int k = 0;
for( int i=0; i<numTids; i++ )
{
for( int c=0; c<s_numCpus; c++ ) iter[k++] = { (pid_t)tids[i], c };
}
numIter = numTids * s_numCpus;
TracyDebug( "Monitor mode: per-thread per-CPU events for %i threads of pid %u (attach)", numTids, currentPid );
}
tracy_free( tids );
TracyDebug( "Monitor mode: tracing %i existing threads of pid %u", numIter, currentPid );
}
else
#endif
{
iter = (PerfIterTarget*)tracy_malloc( sizeof( PerfIterTarget ) * s_numCpus );
for( int i=0; i<s_numCpus; i++ ) iter[i] = { (pid_t)currentPid, i };
@@ -829,7 +882,7 @@ bool SysTraceStart( int64_t& samplingPeriod )
pe.size = sizeof( perf_event_attr );
pe.config = PERF_COUNT_SW_CPU_CLOCK;
pe.sample_freq = samplingFrequency;
pe.sample_type = PERF_SAMPLE_TID | PERF_SAMPLE_TIME | PERF_SAMPLE_CALLCHAIN;
pe.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_TID | PERF_SAMPLE_TIME | PERF_SAMPLE_CALLCHAIN;
#if LINUX_VERSION_CODE >= KERNEL_VERSION( 4, 8, 0 )
if( perfAbi >= PerfAbi48AndNewer ) pe.sample_max_stack = 127;
#endif
@@ -848,25 +901,7 @@ bool SysTraceStart( int64_t& samplingPeriod )
ProbePreciseIp( pe, currentPid );
for( int i=0; i<numIter; i++ )
{
int fd = perf_event_open( &pe, iter[i].pid, iter[i].cpu, -1, PERF_FLAG_FD_CLOEXEC );
if( fd == -1 )
{
pe.exclude_kernel = 1;
ProbePreciseIp( pe, currentPid );
fd = perf_event_open( &pe, iter[i].pid, iter[i].cpu, -1, PERF_FLAG_FD_CLOEXEC );
if( fd == -1 )
{
TracyDebug( " Failed to setup!");
break;
}
TracyDebug( " No access to kernel samples" );
}
new( s_ring+s_numBuffers ) RingBuffer( 64*1024, fd, EventCallstack );
if( s_ring[s_numBuffers].IsValid() )
{
s_numBuffers++;
TracyDebug( " Target %i ok", i );
}
if( OpenSampleEvent( iter[i], pe, EventCallstack ) ) TracyDebug( " Target %i ok (EventCallstack)", i );
}
}
@@ -894,31 +929,13 @@ bool SysTraceStart( int64_t& samplingPeriod )
ProbePreciseIp( pe, PERF_COUNT_HW_CPU_CYCLES, PERF_COUNT_HW_INSTRUCTIONS, currentPid );
for( int i=0; i<numIter; i++ )
{
const int fd = perf_event_open( &pe, iter[i].pid, iter[i].cpu, -1, PERF_FLAG_FD_CLOEXEC );
if( fd != -1 )
{
new( s_ring+s_numBuffers ) RingBuffer( 64*1024, fd, EventCpuCycles );
if( s_ring[s_numBuffers].IsValid() )
{
s_numBuffers++;
TracyDebug( " Target %i ok", i );
}
}
if( OpenSampleEvent( iter[i], pe, EventCpuCycles ) ) TracyDebug( " Target %i ok (EventCpuCycles)", i );
}
pe.config = PERF_COUNT_HW_INSTRUCTIONS;
for( int i=0; i<numIter; i++ )
{
const int fd = perf_event_open( &pe, iter[i].pid, iter[i].cpu, -1, PERF_FLAG_FD_CLOEXEC );
if( fd != -1 )
{
new( s_ring+s_numBuffers ) RingBuffer( 64*1024, fd, EventInstructionsRetired );
if( s_ring[s_numBuffers].IsValid() )
{
s_numBuffers++;
TracyDebug( " Target %i ok", i );
}
}
if( OpenSampleEvent( iter[i], pe, EventInstructionsRetired ) ) TracyDebug( " Target %i ok (EventInstructionsRetired)", i );
}
}
@@ -934,31 +951,13 @@ bool SysTraceStart( int64_t& samplingPeriod )
}
for( int i=0; i<numIter; i++ )
{
const int fd = perf_event_open( &pe, iter[i].pid, iter[i].cpu, -1, PERF_FLAG_FD_CLOEXEC );
if( fd != -1 )
{
new( s_ring+s_numBuffers ) RingBuffer( 64*1024, fd, EventCacheReference );
if( s_ring[s_numBuffers].IsValid() )
{
s_numBuffers++;
TracyDebug( " Target %i ok", i );
}
}
if( OpenSampleEvent( iter[i], pe, EventCacheReference ) ) TracyDebug( " Target %i ok (EventCacheReference)", i );
}
pe.config = PERF_COUNT_HW_CACHE_MISSES;
for( int i=0; i<numIter; i++ )
{
const int fd = perf_event_open( &pe, iter[i].pid, iter[i].cpu, -1, PERF_FLAG_FD_CLOEXEC );
if( fd != -1 )
{
new( s_ring+s_numBuffers ) RingBuffer( 64*1024, fd, EventCacheMiss );
if( s_ring[s_numBuffers].IsValid() )
{
s_numBuffers++;
TracyDebug( " Target %i ok", i );
}
}
if( OpenSampleEvent( iter[i], pe, EventCacheMiss ) ) TracyDebug( " Target %i ok (EventCacheMiss)", i );
}
}
@@ -969,31 +968,13 @@ bool SysTraceStart( int64_t& samplingPeriod )
ProbePreciseIp( pe, PERF_COUNT_HW_BRANCH_INSTRUCTIONS, PERF_COUNT_HW_BRANCH_MISSES, currentPid );
for( int i=0; i<numIter; i++ )
{
const int fd = perf_event_open( &pe, iter[i].pid, iter[i].cpu, -1, PERF_FLAG_FD_CLOEXEC );
if( fd != -1 )
{
new( s_ring+s_numBuffers ) RingBuffer( 64*1024, fd, EventBranchRetired );
if( s_ring[s_numBuffers].IsValid() )
{
s_numBuffers++;
TracyDebug( " Target %i ok", i );
}
}
if( OpenSampleEvent( iter[i], pe, EventBranchRetired ) ) TracyDebug( " Target %i ok (EventBranchRetired)", i );
}
pe.config = PERF_COUNT_HW_BRANCH_MISSES;
for( int i=0; i<numIter; i++ )
{
const int fd = perf_event_open( &pe, iter[i].pid, iter[i].cpu, -1, PERF_FLAG_FD_CLOEXEC );
if( fd != -1 )
{
new( s_ring+s_numBuffers ) RingBuffer( 64*1024, fd, EventBranchMiss );
if( s_ring[s_numBuffers].IsValid() )
{
s_numBuffers++;
TracyDebug( " Target %i ok", i );
}
}
if( OpenSampleEvent( iter[i], pe, EventBranchMiss ) ) TracyDebug( " Target %i ok (EventBranchMiss)", i );
}
}
@@ -1025,7 +1006,7 @@ bool SysTraceStart( int64_t& samplingPeriod )
if( s_ring[s_numBuffers].IsValid() )
{
s_numBuffers++;
TracyDebug( " Core %i ok", i );
TracyDebug( " Core %i ok (EventVsync)", i );
}
}
}
@@ -1067,7 +1048,7 @@ bool SysTraceStart( int64_t& samplingPeriod )
if( s_ring[s_numBuffers].IsValid() )
{
s_numBuffers++;
TracyDebug( " Core %i ok", i );
TracyDebug( " Core %i ok (EventContextSwitch)", i );
}
}
}
@@ -1101,7 +1082,7 @@ bool SysTraceStart( int64_t& samplingPeriod )
if( s_ring[s_numBuffers].IsValid() )
{
s_numBuffers++;
TracyDebug( " Core %i ok", i );
TracyDebug( " Core %i ok (EventWaking)", i );
}
}
}
@@ -1112,7 +1093,7 @@ bool SysTraceStart( int64_t& samplingPeriod )
tracy_free( iter );
if( s_numBuffers == 0 )
if( s_numBuffers == 0 || ( externalPid != 0 && s_ctxBufferIdx == 0 ) )
{
tracy_free( s_ring );
s_ring = nullptr;
@@ -1225,38 +1206,42 @@ void SysTraceWorker( void* ptr )
{
auto offset = pos + sizeof( perf_event_header );
// Layout:
// u32 pid, tid
// u64 time
// u64 cnt
// u64 ip[cnt]
// field order matches PERF_SAMPLE_IP | TID | TIME | CALLCHAIN (then buf.cnt ips)
#pragma pack( push, 1 )
struct
{
uint64_t ip;
uint32_t pid;
uint32_t tid;
uint64_t t0;
uint64_t cnt;
} buf;
#pragma pack( pop )
offset += sizeof( uint32_t );
ring.Read( &buf, offset, sizeof( buf ) );
offset += sizeof( buf );
uint64_t* trace;
if( buf.cnt > 0 )
{
#if defined TRACY_HW_TIMER && defined TRACY_HAS_RDTSC
buf.t0 = ring.ConvertTimeToTsc( buf.t0 );
#endif
auto trace = GetCallstackBlock( buf.cnt, ring, offset );
TracyLfqPrepare( QueueType::CallstackSample );
MemWrite( &item->callstackSampleFat.time, int64_t( buf.t0 ) );
MemWrite( &item->callstackSampleFat.thread, buf.tid );
MemWrite( &item->callstackSampleFat.ptr, uint64_t( trace ) );
TracyLfqCommit;
trace = GetCallstackBlock( buf.cnt, ring, offset );
}
else
{
trace = (uint64_t*)tracy_malloc_fast( 2 * sizeof( uint64_t ) );
trace[0] = 1;
trace[1] = buf.ip;
}
#if defined TRACY_HW_TIMER && defined TRACY_HAS_RDTSC
buf.t0 = ring.ConvertTimeToTsc( buf.t0 );
#endif
TracyLfqPrepare( QueueType::CallstackSample );
MemWrite( &item->callstackSampleFat.time, int64_t( buf.t0 ) );
MemWrite( &item->callstackSampleFat.thread, buf.tid );
MemWrite( &item->callstackSampleFat.ptr, uint64_t( trace ) );
TracyLfqCommit;
}
pos += hdr.size;
}
@@ -1580,10 +1565,17 @@ void SysTraceGetExternalName( uint64_t thread, const char*& threadName, const ch
f = fopen( fn, "rb" );
if( f )
{
char buf[256];
char buf[256] = {};
const auto sz = fread( buf, 1, 256, f );
if( sz > 0 && buf[sz-1] == '\n' ) buf[sz-1] = '\0';
threadName = CopyString( buf );
if( sz > 0 )
{
threadName = CopyString( buf );
}
else
{
threadName = CopyString( "???", 3 );
}
fclose( f );
}
else
@@ -1595,15 +1587,22 @@ void SysTraceGetExternalName( uint64_t thread, const char*& threadName, const ch
f = fopen( fn, "rb" );
if( f )
{
char* tmp = (char*)tracy_malloc_fast( 8*1024 );
char* tmp = (char*)tracy_malloc_fast( 8*1024 + 1 );
const auto fsz = (ptrdiff_t)fread( tmp, 1, 8*1024, f );
fclose( f );
if( fsz <= 0 )
{
tracy_free_fast( tmp );
name = CopyStringFast( "???", 3 );
return;
}
tmp[fsz] = '\0';
int pid = -1;
auto line = tmp;
for(;;)
{
if( memcmp( "Tgid:\t", line, 6 ) == 0 )
if( line - tmp + 6 <= fsz && memcmp( "Tgid:\t", line, 6 ) == 0 )
{
pid = atoi( line + 6 );
break;
@@ -1627,10 +1626,10 @@ void SysTraceGetExternalName( uint64_t thread, const char*& threadName, const ch
f = fopen( fn, "rb" );
if( f )
{
char buf[256];
char buf[256] = {};
const auto sz = fread( buf, 1, 256, f );
if( sz > 0 && buf[sz-1] == '\n' ) buf[sz-1] = '\0';
name = CopyStringFast( buf );
name = sz > 0 ? CopyStringFast( buf ) : CopyStringFast( "???", 3 );
fclose( f );
return;
}

View File

@@ -427,6 +427,21 @@ ListenSocket::~ListenSocket()
if( m_sock != -1 ) Close();
}
void ListenSocket::Adopt( int fd )
{
TRACY_ASSERT( m_sock == -1 );
m_sock = fd;
}
uint16_t ListenSocket::LocalPort() const
{
struct sockaddr_storage addr;
socklen_t len = sizeof( addr );
if( getsockname( m_sock, (sockaddr*)&addr, &len ) != 0 ) return 0;
if( addr.ss_family == AF_INET6 ) return ntohs( ((struct sockaddr_in6*)&addr)->sin6_port );
return ntohs( ((struct sockaddr_in*)&addr)->sin_port );
}
static int addrinfo_and_socket_for_family( uint16_t port, int ai_family, struct addrinfo** res )
{
struct addrinfo hints;

View File

@@ -97,6 +97,8 @@ public:
bool Listen( uint16_t port, int backlog );
Socket* Accept();
void Close();
void Adopt( int fd );
uint16_t LocalPort() const;
ListenSocket( const ListenSocket& ) = delete;
ListenSocket( ListenSocket&& ) = delete;

View File

@@ -94,7 +94,10 @@ extern struct backtrace_state *backtrace_create_state (
data to be loaded directly from the file with base_address=0.
The caller is responsible for converting runtime virtual addresses
to ELF virtual addresses before passing them to backtrace_pcinfo
or backtrace_syminfo. */
or backtrace_syminfo.
The filename is copied into state-owned memory (the file is opened
lazily on first use), so the caller's buffer need not outlive this
call. */
extern struct backtrace_state *backtrace_create_state_for_file (
const char *filename, int threaded,

View File

@@ -81,11 +81,20 @@ backtrace_create_state_for_file (const char *filename, int threaded,
backtrace_error_callback error_callback,
void *data)
{
struct backtrace_state *state;
/* The state opens the file lazily on first use (fileline_initialize),
so it must own the filename: the caller's buffer may be freed as
soon as this returns. */
const size_t len = strlen (filename) + 1;
char *copy = (char*)backtrace_alloc (NULL, len, error_callback, data);
if (copy == NULL)
return NULL;
memcpy (copy, filename, len);
state = backtrace_create_state (filename, threaded, error_callback, data);
if (state != NULL)
state->external_file = 1;
struct backtrace_state *state =
backtrace_create_state (copy, threaded, error_callback, data);
if (state == NULL)
return NULL;
state->external_file = 1;
return state;
}

View File

@@ -115,6 +115,7 @@
#define TracyParameterRegister(x,y)
#define TracyParameterSetup(x,y,z,w)
#define TracyIsConnected false
#define TracyPort 0
#define TracyIsStarted false
#define TracySetProgramName(x)
@@ -258,6 +259,7 @@
#define TracyParameterRegister( cb, data ) tracy::Profiler::ParameterRegister( cb, data )
#define TracyParameterSetup( idx, name, type, val ) tracy::Profiler::ParameterSetup( idx, name, type, val )
#define TracyIsConnected tracy::GetProfiler().IsConnected()
#define TracyPort tracy::GetProfiler().GetPort()
#define TracySetProgramName( name ) tracy::GetProfiler().SetProgramName( name )
#define TracySectionEnter( fmt, ... ) tracy::Profiler::SectionEnter( 0, fmt, ##__VA_ARGS__ )

View File

@@ -174,7 +174,7 @@ public:
WriteInitialItem( physdev, tcpu, tgpu );
m_res = (int64_t*)tracy_malloc( sizeof( int64_t ) * m_queryCount );
AllocateQueryResultBuffer();
}
#if defined VK_EXT_host_query_reset
@@ -223,9 +223,7 @@ public:
WriteInitialItem( physdev, tcpu, tgpu );
// We need the buffer to be twice as large for availability values
size_t resSize = sizeof( int64_t ) * m_queryCount * 2;
m_res = (int64_t*)tracy_malloc( resSize );
AllocateQueryResultBuffer();
}
#endif
@@ -414,6 +412,12 @@ private:
}
}
tracy_force_inline void AllocateQueryResultBuffer()
{
// We need the buffer to be twice as large for availability values
m_res = (int64_t*)tracy_malloc( sizeof( int64_t ) * m_queryCount * 2 );
}
tracy_force_inline void FindAvailableTimeDomains( VkPhysicalDevice physicalDevice, PFN_vkGetPhysicalDeviceCalibrateableTimeDomainsEXT _vkGetPhysicalDeviceCalibrateableTimeDomainsEXT )
{
uint32_t num;

View File

@@ -33,15 +33,19 @@ TaskDispatch::~TaskDispatch()
void TaskDispatch::Queue( const std::function<void(void)>& f )
{
std::lock_guard<std::mutex> lock( m_queueLock );
m_queue.emplace_back( f );
{
std::lock_guard<std::mutex> lock( m_queueLock );
m_queue.emplace_back( f );
}
m_cvWork.notify_one();
}
void TaskDispatch::Queue( std::function<void(void)>&& f )
{
std::lock_guard<std::mutex> lock( m_queueLock );
m_queue.emplace_back( std::move( f ) );
{
std::lock_guard<std::mutex> lock( m_queueLock );
m_queue.emplace_back( std::move( f ) );
}
m_cvWork.notify_one();
}