Skip to content

Modernize signposts and os_log: decodable strings, dynamic tracing, Instruments package - #87

Merged
maleadt merged 6 commits into
mainfrom
tb/signposts
Oct 18, 2026
Merged

maleadt merged 6 commits into
mainfrom
tb/signposts

Conversation

@maleadt

@maleadt maleadt commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

The signposts and os_log messages from ObjectiveC.OS never showed up in Instruments or log show. Unified logging doesn't copy a message's name and format strings into the log. It records their offsets in the Mach-O image passed as dso, plus that image's UUID, and the decoder reads the strings back from the image's __TEXT segment on disk. We passed a heap copy of libjulia's file contents as dso, so libtrace dropped every message.

This PR makes those strings static, and makes signposts cheap enough for hot paths like kernel launches in Metal.jl.

Static strings. The name and format strings are emitted as private unnamed_addr LLVM constants (via llvmcall) in a @noinline function specialized on them. When that specialization is compiled into a package image, the constants end up in the image's __TEXT,__cstring section, which the decoder reads like Apple's own __oslogstring (pkgimages drop custom sections, so we can't use that one). A weak hidden __dso_handle reference, which the linker resolves to the image's Mach-O header, provides the matching dso. @signpost_interval and @signpost_event precompile that specialization when they are expanded during precompilation, so call sites that are only compiled at run time still find their strings in the image of the package that contains them. When there is no image (a name that isn't a string literal, or code outside a package image), signposts are named "Julia" with the actual name prepended to the message. If ObjectiveC.jl itself isn't precompiled, they borrow a %s from libobjc, whose strings are always decodable.

Dynamic tracing. OS.CATEGORY_DYNAMIC_TRACING (and CATEGORY_DYNAMIC_STACK_TRACING, CATEGORY_POINTS_OF_INTEREST) are Apple's categories for signposts that stay disabled until a tool asks for them. A disabled @signpost_interval then costs one os_signpost_enabled check and builds no message: about 16 ns on an M1, the same as Apple's C macros. Enabled intervals cost about 250 ns, also the same as C, and allocate nothing. Instruments only enables these categories for subsystems that an instrument names, so OS.instruments_package(subsystems) builds such an instrument with Xcode's instrumentbuilder. It plots the intervals and events of those subsystems on a timeline, lists them, and summarizes the intervals by subsystem and name. Use it with xctrace record --package $path --instrument "Julia Signposts", or open the package in Instruments.app.

Alignment with Apple's macros. Events now use OS_SIGNPOST_ID_EXCLUSIVE instead of the null ID, and signposts with a null or invalid ID are dropped, as os_signpost_emit_with_type does. The enabled checks are declared as returning Bool, matching the C prototypes. The docs now say that OSLog construction (os_log_create, 40–60 ns) should be hoisted out of hot paths, and fix the documented default stop message.

API changes are additive. interval_begin/interval_end/os_signpost_emit_with_type gained methods that take the static strings, and their name and message arguments are no longer restricted to String. The now-unused Libdl dependency is dropped, and the version is bumped to 7.1.0 so that Metal.jl can depend on the release.

Verified with xctrace record on macOS 27 (M1): without this PR the os-signpost table contains no Julia signposts at all. With it, a precompiled package emitting DynamicTracing intervals and events in three subsystems, recorded with a package built for two of them, shows exactly the intervals and events of those two, with their names and messages. The tests check that the strings of a call site in another package end up in that package's image. The mechanism also works in juliac executables and libraries (with and without --trim=safe): the strings land in the binary's __cstring, and log show decodes them.

@maleadt maleadt changed the title Make signposts and os_log messages visible in Instruments Modernize signposts and os_log: decodable strings, dynamic tracing, Instruments package Oct 18, 2026
maleadt added a commit to JuliaGPU/Metal.jl that referenced this pull request Oct 18, 2026
Temporary, until ObjectiveC.jl 7.1 is released (JuliaInterop/ObjectiveC.jl#87).
maleadt added a commit to JuliaGPU/Metal.jl that referenced this pull request Oct 18, 2026
Temporary, until ObjectiveC.jl 7.1 is released (JuliaInterop/ObjectiveC.jl#87).
Unified logging stores the name and format strings of a message as offsets
into the image passed as dso, and resolves them from that image's on-disk
__TEXT segment when decoding. We passed a heap copy of libjulia, so libtrace
dropped every message. Instead, emit the strings as llvmcall constants in a
non-inlined function specialized on them, so that they end up in the
__cstring section of the package image that precompiles the call site, and
use the linker's __dso_handle as the image handle. The signpost macros
precompile these specializations while generating output, so that call
sites that are only compiled at run time still find their strings in an
image. Non-literal names, and call sites outside of package images, fall
back to a generic "Julia" name with the name prepended to the message.

This removes the last use of Libdl.
Signposts on a regular category are always enabled and end up in the
system log, which is too expensive for hot paths like kernel launches.
The DynamicTracing categories keep them disabled until a tool asks for
them, but Instruments only does so for subsystems that an instrument
names, so add instruments_package to build such an instrument. It plots
the intervals and events of the given subsystems on a timeline, lists
them, and summarizes the intervals by subsystem and name.
`@signpost_event` emitted events with OS_SIGNPOST_ID_NULL, which Apple's
os_signpost_event_emit never does: Instruments records such events without
an identifier. Use OS_SIGNPOST_ID_EXCLUSIVE instead, and drop signposts
with a null or invalid ID, as the macros do. os_signpost_id_generate
returns the null ID when signposts get disabled between the
`is_signpost_enabled` check and the start of an interval.
os_log_type_enabled and os_signpost_enabled return a C bool, which we read
as a Cint and converted with `Bool`. That relied on the callee extending
the result, and added an InexactError path to every signpost call site.
The default stop message of `@signpost_interval` is "stop", not "end".
Creating a logger calls os_log_create, which takes 40-60 ns: cheap, but
several times the cost of a disabled signpost, so recommend hoisting it on
hot paths. Also mention that the system truncates long messages.
maleadt added a commit to JuliaGPU/Metal.jl that referenced this pull request Oct 18, 2026
Temporary, until ObjectiveC.jl 7.1 is released (JuliaInterop/ObjectiveC.jl#87).
@maleadt
maleadt merged commit 1672812 into main Oct 18, 2026
12 checks passed
@maleadt
maleadt deleted the tb/signposts branch October 18, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant