What's New in Instruments 27 for App Responsiveness

At WWDC 2026, two Apple engineers profiled a note-taking app that exhibited three separate hangs: an unresponsive pencil on save, choppy scrolling, and a stall when using the lasso tool. They fixed all three by matching one new Instruments 27 tool to each symptom, then proved each fix with a baseline-versus-optimized comparison.1 The session’s thesis is a diagnostic flow: read the CPU during the hang, and the CPU tells you which instrument to open next. High CPU means your code is too slow. Idle CPU means your code is blocked. Instruments 27 ships the tooling to resolve both, and a new view to confirm the fix actually worked.

This post walks the three tools that anchor that flow: Top Functions to find the hot self-weight when the CPU is saturated, the new Swift executors instrument to see which executor a task ran on when work fights for the Main Actor, and the new Inspector panel to read syscall arguments when a thread sits idle waiting on the system. Run Comparisons ties them together by measuring whether each change improved the trace. Everything below comes from the session directly.

TL;DR

  • Instruments 27 reorganizes the responsiveness workflow around a diagnostic rule: open the Time Profiler first, check main-thread CPU during the hang, and let that reading route you to the right tool.1
  • Top Functions is a new analysis mode that discards the call hierarchy and merges every scattered node by self weight, surfacing the runtime overhead a flame graph fractures across branches.1
  • Run Comparisons is “New in Instruments” and computes the exact performance delta between a baseline trace and an optimized trace, matching each function across runs and coloring regressions red and improvements green.1
  • The new Swift executors instrument visualizes the Main Actor, the global concurrent executor, and any custom executors, so you can see which executor a task ran on and catch Main Actor contention.1
  • The new Inspector panel surfaces a system call’s exact arguments (file descriptor, buffer address, write size) and splits on-core from off-core time, exposing synchronous blocking like a 1.7 GB write on the main thread.1

The diagnostic flow Instruments 27 is built around

Before any new tool, the session establishes the rule that organizes them. When an app drops frames or hangs, the first step is the Time Profiler, which gives the high-level overview to orient yourself.1 From there, one question routes everything: what is the CPU doing during the hang?

If CPU usage is high, the thread is busy and the work is taking too long, which points to a code performance bottleneck. You fix that two ways: refactor the algorithm to run faster, or, when the heavy workload is unavoidable, offload it to a background task so the interface stays responsive.1 If the app hangs while the processor sits idle, optimizing algorithms will not help, because the main thread is stuck waiting for a resource to free up: file I/O, a synchronization lock, or inter-process communication. As the session puts it, “Because Time Profiler only monitors active CPU cycles, it provides no visibility into these events.”1

The engineers profiled a release build of the note app, because “a debug build trades off runtime performance for debug ability, so profiling data from debug builds can be misleading.”1 They chose the Swift Concurrency template, which still exposes the Time Profiler instrument, and recorded all three hangs into a single baseline trace. They also wrapped the lasso selection in an os_signpost interval using the OSSignposter type, setting the category to points of interest so Instruments surfaces the interval in the points of interest track. That signpost becomes the anchor for filtering the trace and, later, for a noise-free run comparison.1

Watch on Apple Developer ↗

Art and Harjas lay out the diagnostic flow before the demo, starting at 1:50.

The Instruments 27 window itself frames the workflow. The timeline at the top shows horizontal tracks for tasks, actors, and executors. The detail area below changes based on the selected track. On the right is “a brand new Inspector panel” that surfaces additional details and actions based on what you select.1 Three tools fill that frame, each tied to one of the three hangs.

Top Functions: when the CPU is saturated

The lasso hang reads as high CPU. After filtering the trace to the lasso os_signpost interval, the hangs instrument confirmed several hangs there, and expanding the process track to the main thread showed CPU “staying around a 100% during this time period.”1 High CPU means the code is executing but takes too long, so the Time Profiler is the right tool.

The session explains why a flame graph alone is not enough here. The Time Profiler uses a hardware timer to sample the call stack at a default rate of one millisecond, recording the current stack on every core. Each sampled function gets a weight, and the function at the bottom of the stack gets a self-weight, the time spent executing instructions directly inside it.1 A flame graph renders that call tree into spatial blocks, with callers on top, callees growing downward, and bar width proportional to total CPU time.1 The problem: “for code that is called from a lot of places, like Swift runtime functions and various helper utilities,” the flame graph distributes the total cost across every branch that calls them. The execution time fractures into small pieces, which “makes it difficult to answer which specific functions burned the most overall cycles.”1

That gap is what Top Functions fills. As the session describes the new mode, “This new mode discards the call hierarchy. Instead, it extracts every single scattered node and merges them together to form one block,” evaluated by the self metric.1 Scanning the lasso flame graph showed no single offender, just “different codepaths sum together to be costly enough to cause hangs.”1 Switching to Top Functions, sorted by self weight, the top entry was swift_project_boxed_opaque_existential, the runtime function that unwraps an existential so the code can operate over it.1

The fix lived in the type system, not the timeline. The engineer asked the Xcode coding assistant to rewrite the drawing code to use concrete types and generics instead of existentials, since existentials can vary in size and require extra work to access, which proved too expensive for this use case.1 The lesson for the tool: Top Functions exists to catch scattered software overhead that no single flame-graph branch makes obvious.

Run Comparisons: proving the fix worked

Confirming the fix used to mean opening two traces in separate windows and eyeballing the Top Functions data side by side. Instruments 27 replaces that with Run Comparisons, described in the session as “New in Instruments.”1 It “computes the exact performance delta by cross-referencing all of samples from the baseline trace and optimized trace,” evaluating every node in the stack. It matches the old version of a function from the baseline run to the new version in the optimized run, calculates the delta, and sorts by performance difference. A red block marks a regression; a green block marks an improvement.1

The workflow is deliberate about removing noise. The engineers first filtered both runs to the exact same os_signpost interval for the lasso selection, then selected the main thread track and clicked the compare button to pick the baseline run from a dropdown.1 That adds a comparison tab to the sidebar, and “you can create multiple comparisons, and they are saved to the document to make collaboration easier.”1

The comparison told an honest story. The textual call tree showed overall lasso execution time decreased. The flame graph showed improved paths in green and regressed paths in red, and the regressions were “new functions added by the coding assistant as it worked to eliminate the usage of existentials.”1 In Top Functions view, regressions sort to the top by default; flipping the sort revealed that swift_project_boxed_opaque_existential “has been removed completely,” and “overall, the improvements out[weigh] the regressions.”1 That delta is the verification step: Run Comparisons exists so a fix is a measured outcome, not a hopeful one. The session’s standing advice is to “leverage os_signpost to make sure your intervals for run comparisons are reliable.”1

The Swift executors instrument: when tasks fight for the Main Actor

The scrolling hang had no points-of-interest log to lean on. The session reaches for a different kind of context: “what tasks are on the Main Actor during these hangs.”1 That is the job of the new Swift executors instrument, which “visualizes the Main Actor, the global concurrent executor, and any custom executors in your process.”1

For each scrolling hang, the Main Actor track showed a Swift task named renderThumbnail. Selecting the track summarized the tasks on the Main Actor and revealed “several render thumbnail tasks on the Main Actor taking a few hundred ms to run,” which lines up with the choppy scrolling.1 Following the diagnostic flow, the engineer filtered to one hang and used the Inspector to pin the main thread; the Time Profiler reported CPU “around a 100%,” ruling out a system-resource wait. The tasks were simply taking too long on the Main Actor.1

The root cause is a context-inheritance subtlety the instrument makes visible. The Main Actor handles all UI updates and interactions. The app rendered thumbnails asynchronously, but because that code was called from SwiftUI, “it inherited the Main Actor context,” so the thumbnail tasks competed with critical UI updates.1 The fix routes the work to the thread pool. The engineer added the @concurrent attribute to the task initializer, which moves the rendering task off the Main Actor and onto the global executor, and the Swift compiler checks that the change introduces no race conditions.1 The instrument confirmed the move: in the updated trace, “the thumbnail rendering tasks have moved from the Main Actor track to the global executor track,” and the work now runs in parallel.1 The point for the tool: the Swift executors instrument exists to identify actor congestion by showing you exactly which executor ran which task.

The Inspector panel: when the thread is idle and blocked

The save hang inverts the first two. After filtering to the Write to File os_signpost interval and zooming in, the reported micro hang showed CPU “hovering around 20%.”1 Low CPU is deceptive: “it doesn’t mean your code is executing slowly, it means the thread has stopped running.”1 When the interface freezes under low CPU, the main thread is blocked waiting on a system resource, and optimizing algorithms yields nothing “because there is no code running to optimize.”1

For that symptom the session switches to the System Trace template, “built to visualize exactly when and why the operating system pauses your application.”1 The session walks the thread-state model: a running thread that hits an unavailable resource enters the blocked state, the kernel evicts it from the processor, and only when the resource is ready does it become runnable and wait for the scheduler to assign a free core. Those brief wake-ups to coordinate the next stage of the request are exactly “what cause that twenty percent of CPU utilization.”1

In the System Trace, the activity lane for the save showed “a large amount of blank space,” indicating the thread was blocked, with purple intervals marking a running system call.1 Selecting one interval highlighted more than the clicked segment, visualizing “one continuous write system call that spans both on and off-core time”: opaque segments are on-core execution, translucent segments are off-core blocking.1

The new Inspector panel turns that into a verdict. As the session states, “The Inspector gives us the exact arguments passed to this system call. We can see the target file descriptor, the memory address of the buffer, and most importantly, the size.”1 The size was the smoking gun: the app was “trying to write over 1.7 gigabytes of data on the main thread.”1 The Inspector also showed the cost: the single operation “took over 500 milliseconds, and almost 300 of those milliseconds were spent off-core waiting for the disk.”1 The synchronous data.write call was the bottleneck. Wrapping the encode-and-write in a Swift task pushed it to the concurrent thread pool, and a verification trace confirmed the write system call now appears on a background thread, not the main thread.1 The Inspector panel exists to expose synchronous blocking behavior, like file I/O, that a CPU-only profiler cannot see.

Key Takeaways

For iOS and macOS engineers:

  • Open the Time Profiler first and read main-thread CPU during the hang; high CPU sends you to Top Functions, idle CPU sends you to System Trace and the Inspector.1
  • Use Top Functions when a flame graph shows no single offender; it merges scattered runtime overhead by self weight so the costliest function surfaces.1

For teams shipping performance fixes:

  • Verify every change with Run Comparisons, filtered to the same os_signpost interval, and read the red/green delta instead of trusting a side-by-side eyeball.1
  • Comparisons save to the document and stack multiple runs, so a fix’s evidence travels with the trace for review.1

For concurrency and responsiveness work:

  • Reach for the Swift executors instrument when work fights for the Main Actor; it shows whether a task ran on the Main Actor, the global concurrent executor, or a custom executor.1
  • Always profile a release build, since debug builds produce misleading data.1

FAQ

What is the Top Functions mode in Instruments 27?

Top Functions is a new analysis mode in the Time Profiler. It discards the call hierarchy and merges every scattered instance of a function into one block, ranked by self weight, the time spent executing instructions directly inside that function. It answers a question a flame graph struggles with: which specific functions burned the most overall cycles when their cost is fractured across many calling branches, such as Swift runtime functions and helper utilities.1

How do Run Comparisons work in Instruments?

Run Comparisons, described in the session as new to Instruments, computes the exact performance delta between a baseline trace and an optimized trace. It matches each function across the two runs, calculates the delta for every node in the stack, and sorts by performance difference, coloring regressions red and improvements green. For a clean comparison you filter both runs to the same os_signpost interval, select a track, and pick the baseline from a dropdown; comparisons save to the document.1

What does the Swift executors instrument show?

The Swift executors instrument in Instruments 27 visualizes the Main Actor, the global concurrent executor, and any custom executors in your process. It lets you see which executor a given Swift task ran on, so you can catch Main Actor contention. In the session it revealed renderThumbnail tasks stuck on the Main Actor because SwiftUI-called code inherited the Main Actor context; moving them to the global executor cleared the hang.1

How do you find a hang caused by file I/O?

When the interface freezes but main-thread CPU is low (around 20 percent in the session), the thread is blocked waiting on a system resource rather than running slow code. Switch to the System Trace template and select the system-call interval; the new Inspector panel shows the syscall’s exact arguments, including the file descriptor, the buffer address, and the write size, plus on-core versus off-core time. In the demo it exposed a 1.7 GB synchronous write on the main thread.1


The diagnostic flow this session teaches pairs with the SwiftUI side of responsiveness in SwiftUI performance and interop in iOS 27 and the measurement mindset in the performance blind spot. The concurrency move that cleared the Main Actor hang sits inside the broader model covered in Swift 6.2 concurrency in practice. The full series hub is the Apple Ecosystem Series.

References


  1. Apple, WWDC 2026 session 268, Profile, fix, and verify: Improve app responsiveness with Instruments. Source for the diagnostic flow (Time Profiler first, main-thread CPU reading routes the investigation), the release-build and Swift Concurrency template guidance, the os_signpost / OSSignposter points-of-interest interval, the new Inspector panel, the call-tree and flame-graph sampling model (one-millisecond default rate, self weight), the Top Functions analysis mode and the swift_project_boxed_opaque_existential finding, Run Comparisons (“New in Instruments”) with red/green deltas and document-saved comparison tabs, the Swift executors instrument (Main Actor, global concurrent executor, custom executors) and the renderThumbnail Main Actor contention resolved with the @concurrent attribute, and the System Trace plus Inspector diagnosis of a 1.7 GB synchronous write (file descriptor, buffer address, write size, on-core versus off-core time, over 500 ms with almost 300 ms off-core). 

Artículos relacionados

SwiftUI Performance and Interop in iOS 27

How iOS 27 SwiftUI handles lazy-stack scrolling, GPU shader effects, and AppKit/UIKit interop, drawn from three official…

17 min de lectura

Meet Music Understanding: On-Device Audio Analysis

Music Understanding is Apple's on-device framework for analyzing a song's key, rhythm, structure, pace, instrument activ…

12 min de lectura

From 76 to 100: Achieving a Perfect Lighthouse Score

A FastAPI site went from Lighthouse 76 with 0.493 CLS to perfect 100/100/100/100. The fix: critical CSS extraction, a CS…

10 min de lectura