2023 Developer Tools
WWDC23 · 43 min · Developer Tools
Analyze hangs with Instruments
User interface elements often mimic real-world interactions, including real-time responses. Apps with a noticeable delay in user interaction — a hang — can break that illusion and create frustration. We’ll show you how to use Instruments to analyze, understand, and fix hangs in your apps on all Apple platforms. Discover how you can efficiently navigate an Instruments trace document, interpret trace data, and record additional profiling data to better understand your specific hang. If you aren’t familiar with using Instruments, we recommend first watching "Getting Started with Instruments." And to learn about other tools that can help you discover hangs in your app, check out "Track down hangs with Xcode and on-device detection."
Watch at developer.apple.com ↗Chapters
- 1:56 — What is a hang?
- 3:51 — What is instant?
- 4:39 — Event handling and rendering loop
- 8:25 — Keep main thread work below 100ms
- 9:15 — Busy main thread hang
- 14:26 — Too long or too often?
- 21:46 — LazyVGrid still hangs on iPad
- 24:31 — Fix: Use task modifier to load thumbnail asynchronously
- 25:52 — Asynchronous hangs
- 32:38 — Fix: Get off of the main actor
- 35:57 — Blocked Main Thread Hang
- 39:19 — Fix: Make shared property async
- 40:35 — Blocked Thread does not imply unresponsive app
Code shown on screen · 18 snippets
BackgroundThumbnailView
struct BackgroundThumbnailView: View {
static let thumbnailSize = CGSize(width:128, height:128)
var background: BackyardBackground
var body: some View {
Image(uiImage: background.thumbnail)
}
} BackgroundSelectionView with Grid
var body: some View {
ScrollView {
Grid {
ForEach(backgroundsGrid) { row in
GridRow {
ForEach(row.items) { background in
BackgroundThumbnailView(background: background)
.onTapGesture {
selectedBackground = background
}
}
}
}
}
}
} BackgroundSelectionView with Grid (simplified)
var body: some View {
ScrollView {
Grid {
ForEach(backgroundsGrid) { row in
GridRow {
ForEach(row.items) { background in
BackgroundThumbnailView(background: background)
}
}
}
}
}
} LazyVGrid variant
var body: some View {
ScrollView {
LazyVGrid(columns: [.init(.adaptive(minimum: BackgroundThumbnailView.thumbnailSize.width))]) {
ForEach(BackyardBackground.allBackgrounds) { background in
BackgroundThumbnailView(background: background)
}
}
}
} BackgroundThumbnailView
struct BackgroundThumbnailView: View {
static let thumbnailSize = CGSize(width:128, height:128)
var background: BackyardBackground
var body: some View {
Image(uiImage: background.thumbnail)
}
} BackgroundThumbnailView with progress (but without loading)
struct BackgroundThumbnailView: View {
static let thumbnailSize = CGSize(width:128, height:128)
var background: BackyardBackground
private var image: UIImage?
var body: some View {
if let image {
Image(uiImage: image)
} else {
ProgressView()
.frame(width: Self.thumbnailSize.width, height: Self.thumbnailSize.height, alignment: .center)
}
}
} BackgroundThumbnailView with async loading on main thread
struct BackgroundThumbnailView: View {
static let thumbnailSize = CGSize(width:128, height:128)
var background: BackyardBackground
private var image: UIImage?
var body: some View {
if let image {
Image(uiImage: image)
} else {
ProgressView()
.frame(width: Self.thumbnailSize.width, height: Self.thumbnailSize.height, alignment: .center)
.task {
image = background.thumbnail
}
}
}
} BackgroundThumbnailView with async loading on main thread
struct BackgroundThumbnailView: View {
static let thumbnailSize = CGSize(width:128, height:128)
var background: BackyardBackground
private var image: UIImage?
var body: some View {
if let image {
Image(uiImage: image)
} else {
ProgressView()
.frame(width: Self.thumbnailSize.width, height: Self.thumbnailSize.height, alignment: .center)
.task {
image = background.thumbnail
}
}
}
} BackgroundThumbnailView with async loading on main thread (simplified)
struct BackgroundThumbnailView: View {
// [...]
var body: some View {
// [...]
ProgressView()
.task {
image = background.thumbnail
}
// [...]
}
} BackgroundThumbnailView with async loading on main thread
struct BackgroundThumbnailView: View {
static let thumbnailSize = CGSize(width:128, height:128)
var background: BackyardBackground
private var image: UIImage?
var body: some View {
if let image {
Image(uiImage: image)
} else {
ProgressView()
.frame(width: Self.thumbnailSize.width, height: Self.thumbnailSize.height, alignment: .center)
.task {
image = background.thumbnail
}
}
}
} synchronous thumbnail property
public var thumbnail: UIImage {
get {
// compute and cache thumbnail
}
} asynchronous thumbnail property
public var thumbnail: UIImage {
get async {
// compute and cache thumbnail
}
} BackgroundThumbnailView with async loading in background
struct BackgroundThumbnailView: View {
static let thumbnailSize = CGSize(width:128, height:128)
var background: BackyardBackground
private var image: UIImage?
var body: some View {
if let image {
Image(uiImage: image)
} else {
ProgressView()
.frame(width: Self.thumbnailSize.width, height: Self.thumbnailSize.height, alignment: .center)
.task {
image = await background.thumbnail
}
}
}
} shared property causes blocked main thread
var body: some View {
mainContent
.task(id: imageMode) {
defer {
loading = false
}
do {
var image = await background.thumbnail
if imageMode == .colorized {
let colorizer = ColorizingService.shared
image = try await colorizer.colorize(image)
}
self.image = image
} catch {
self.error = error
}
}
} shared property causes blocked main thread (simplified)
struct ImageTile: View {
// [...]
// implicit @MainActor
var body: some View {
mainContent
.task() { // inherits @MainActor isolation
// [...]
let colorizer = ColorizingService.shared
result = try await colorizer.colorize(image)
}
}
} shared property causes blocked main thread + ColorizingService (simplified)
class ColorizingService {
static let shared = ColorizingService()
// [...]
}
struct ImageTile: View {
// [...]
// implicit @MainActor
var body: some View {
mainContent
.task() { // inherits @MainActor isolation
// [...]
let colorizer = ColorizingService.shared
result = try await colorizer.colorize(image)
}
}
} shared synchronous property after await keyword still causes blocked main thread
class ColorizingService {
static let shared = ColorizingService()
// [...]
}
struct ImageTile: View {
// [...]
// implicit @MainActor
var body: some View {
mainContent
.task() { // inherits @MainActor isolation
// [...]
result = try await ColorizingService.shared.colorize(image)
}
}
} shared synchronous property after await keyword still causes blocked main thread (+colorize function)
class ColorizingService {
static let shared = ColorizingService()
func colorize(_ grayscaleImage: CGImage) async throws -> CGImage
// [...]
}
struct ImageTile: View {
// [...]
// implicit @MainActor
var body: some View {
mainContent
.task() { // inherits @MainActor isolation
// [...]
result = try await ColorizingService.shared.colorize(image)
}
}
} Resources
Related sessions
-
21 min -
22 min -
17 min -
25 min -
39 min -
24 min -
19 min -
35 min -
37 min