Diagnosing pre-main latency in a short-lived macOS child process while preserving its parent

I’m looking for a supported way to investigate startup latency before main() in a short-lived native macOS child process, while preserving its original parent-child relationship.

Environment

  • macOS 26.6.2, build 25G83.
  • Native arm64 executables.
  • The on-disk /usr/lib/dyld contains the identifier dyld-1387; this is a static observation, not independent verification of all runtime shared-cache components.
  • The diagnostic runs were started manually from normal macOS Terminal.
  • All inputs and target processes are synthetic test fixtures.

Execution structure Test supervisor → Watchdog → Observer → Query

Observer launches Query using posix_spawn(). Query checks a synthetic target process’s identity using Security.framework and produces a fixed 28-byte result.

Observer passes an explicit environment containing only LANG=C and LC_ALL=C. Consequently, exporting DYLD variables in the outer Terminal would not pass them to Query.

Our existing test has a 1000 ms Query deadline and requires empty Query stderr. This is an application-defined acceptance requirement, not a claimed macOS timing guarantee.

Observed results

  1. A historical full regression run timed out at approximately 1001 ms, with no Query result bytes received. That run did not have main-entry instrumentation, so we cannot determine where its time was spent.

  2. A later isolated, instrumented run measured:

    • posix_spawn return to Query main entry: 832.687 ms
    • main entry to return: 24.179 ms
    • Observer-measured Query elapsed time: 864 ms
  3. Another isolated, instrumented run measured:

    • posix_spawn call duration: 0.179 ms
    • spawn return to main entry: 634.334 ms
    • main entry to return: 6.757 ms
    • Observer-measured Query elapsed time: 647 ms

In the third run, getrusage(RUSAGE_SELF) at main entry reported cumulative CPU time of 3.014 ms user and 2.583 ms system. Before main returned, the cumulative total was 8.088 ms. Timeline measurements used CLOCK_MONOTONIC.

Interpretation limits These successful diagnostic runs do not explain or resolve the historical timeout. Instrumentation changes the executable, and cache state and scheduling may differ.

The CPU and wall-clock measurements cover different intervals. Subtracting them would not give an exact waiting-time measurement. We cannot currently attribute the delay to dyld, scheduling, security checks, IPC, or I/O.

Diagnostic constraints We want to preserve the original parent-child relationship and avoid system-wide collection, disabling security protections, or silently changing permissions.

We have not established that the Instruments App Launch configuration available to us can capture this child’s earliest startup phases while restricting collection to Query.

The historical DYLD_PRINT_STATISTICS options are not documented in the installed dyld manual. Attaching a sampling tool after obtaining the PID may miss the relevant startup interval and may perturb this short-lived process.

Questions

  1. What supported tool or API on this macOS version can distinguish startup phases before main() for this posix_spawn-created child?
  2. Can collection itself be restricted to Query, rather than collecting system-wide data and filtering the report afterward?
  3. What permissions are required, which early phases remain unobservable, and how much observer perturbation should we expect?
  4. If these constraints cannot all be met, what is the smallest necessary change to the diagnostic setup?
  5. What minimal sample project and evidence would be most useful for further investigation?

I have preserved the original failure and subsequent diagnostic evidence, but do not yet have a standalone Xcode sample that reliably reproduces the historical timeout.

I’m requesting guidance on an appropriate measurement method, not asserting that this is a confirmed macOS defect.

Answered by DTS Engineer in 909938022
The immediate goal is a reliable native process-identity check within an automated test workflow.

OK. Who is checking whose identity here? Reading your second reply it sounds like you want Observer (the parent) to be checking Query (thi child). If so, you don’t need custom machinery for that. Rather, have Observer run Query with a launch requirement that ensures its identity.

The easiest way to construct these requirements is with the LightweightCodeRequirements framework. Once you have one you can apply it using the launchRequirement property on Process.

Annoyingly, the corresponding Posix spawn attribute is SPI )-: See here. If you’d like to stick with posix_spawn, I encourage you to file an enhancement request to make that API. And if you do that, please post your bug number, just for the record.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

What’s your actual goal here? You’ve described a very complicated system that ultimately boils down to this:

Query checks a synthetic target process’s identity using Security.framework and produces a fixed 28-byte result.

You could just as easily do that without the additional processes, so I suspect that isn’t the actual goal, but rather that there’s some bigger picture.

That matters because it seems like you’re micro-benchmarking the performance of posix_spawn, which isn’t a very useful activity. macOS has never been a speed demon (daemon :-) when it comes to starting new processes, and there aren’t really any optimisations you can apply to it.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thank you, Quinn. You are right that my original post focused too heavily on the diagnostic machinery and did not explain the goal clearly.

The immediate goal is a reliable native process-identity check within an automated test workflow. If the check times out or cannot establish the identity, the workflow must not treat that as a successful verification. The target is synthetic in these tests.

The supervisor/watchdog/observer/query chain is our current test arrangement. I have not established that all of those process boundaries are necessary for the actual functionality. Preserving the chain was a constraint for comparing diagnostic runs with the original failing test, not a claim that this is the best production architecture.

Likewise, the 1000 ms deadline is an existing application/test requirement, not an expectation that macOS guarantees process startup within that time.

We are not trying to optimize posix_spawn itself. We are trying to understand whether the intermittent timeout points to a problem in our implementation or to a fragile design assumption: starting a fresh helper and completing the identity check within that deadline.

The successful instrumented runs showed substantial elapsed time before main(), but they did not establish the cause of the original failure.

Before collecting more traces, would you recommend first reconsidering the per-check helper process—for example, performing the check in an existing process or using a longer-lived helper? What functional or isolation requirements would you need us to clarify to assess that choice?

We have not changed the implementation or relaxed the existing acceptance criteria.

One important clarification after reviewing our original design and implementation: my previous reply omitted why the separate query process was introduced.

This is a controlled validation harness for a fixed native host, not a general-purpose process-identity service. The intended sequence is:

  1. Observer creates the target using POSIX_SPAWN_START_SUSPENDED.
  2. Before resuming it, Query checks dynamic code validity, the expected build identity, and the required runtime/library-validation status bits.
  3. Observer resumes the target only after validating the complete result, observing stdout/stderr EOF, and confirming that Query exited successfully and was reaped.
  4. Observer then checks the target’s fixed synthetic output and termination.

The goal is to observe these properties without modifying the host being validated. This is a limited diagnostic observation, not a claim of continuous protection or production authentication.

The separate Query process was introduced because the Security calls are synchronous: if a call blocks, Observer must remain able to handle cancellation, terminate Query and the still-suspended target, and perform bounded cleanup. Observer owns both children. An outer watchdog addresses Observer stalls, but does not guarantee descendant cleanup after a forced termination.

Therefore, moving the synchronous check directly into Observer’s supervision loop would lose an intended failure-isolation property unless another supported mechanism replaces it.

The existing 1000 ms query deadline is an application/test budget, not an OS guarantee. We have not established that this particular value is appropriate.

Given this more specific goal, is a disposable helper a reasonable isolation boundary, or would you recommend another supported design that preserves cancellation and bounded cleanup without relying on fast per-check process startup?

The immediate goal is a reliable native process-identity check within an automated test workflow.

OK. Who is checking whose identity here? Reading your second reply it sounds like you want Observer (the parent) to be checking Query (thi child). If so, you don’t need custom machinery for that. Rather, have Observer run Query with a launch requirement that ensures its identity.

The easiest way to construct these requirements is with the LightweightCodeRequirements framework. Once you have one you can apply it using the launchRequirement property on Process.

Annoyingly, the corresponding Posix spawn attribute is SPI )-: See here. If you’d like to stick with posix_spawn, I encourage you to file an enhancement request to make that API. And if you do that, please post your bug number, just for the record.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"