{"$schema": "https://raw.githubusercontent.com/guitaripod/wwdc-sessions/master/schema/transcript.schema.json", "id": "wwdc2026-268", "language": "eng", "source": "https://developer.apple.com/videos/play/wwdc2026/268", "wordCount": 3825, "segments": [{"start": 7.0, "text": "Hi, I'm Art, I work on Xcode."}, {"start": 9.0, "text": "And I am Harjas from the Instruments team."}, {"start": 12.0, "text": "Delivering a fluid user interface is the baseline for a great app."}, {"start": 16.0, "text": "But there is a lot happening behind the scenes."}, {"start": 18.0, "text": "Navigating the layers of the software stack can be confusing."}, {"start": 22.0, "text": "Instruments 27 makes it easier than ever to understand"}, {"start": 25.0, "text": "how to make your apps feel fast and responsive."}, {"start": 29.0, "text": "It starts with app foundation."}, {"start": 31.0, "text": "The Swift code you write is fast and expressive."}, {"start": 35.0, "text": "But this code does not execute in isolation."}, {"start": 38.0, "text": "Even simple code is hiding a lot of complexity."}, {"start": 42.0, "text": "The compiler and runtime inject dynamic dispatch, safety checks,"}, {"start": 46.0, "text": "and reference counting behind the scenes."}, {"start": 49.0, "text": "Then, it relies on the Operating System for memory allocation,"}, {"start": 52.0, "text": "process scheduling, and I/O routing."}, {"start": 56.0, "text": "After that, OS delegates to the underlaying platform."}, {"start": 60.0, "text": "This finally routes execution directly to the physical hardware."}, {"start": 64.0, "text": "It is, also, worth noticing while I've highlighted"}, {"start": 67.0, "text": "a few common components here,"}, {"start": 68.0, "text": "there are many more compiler and system features at play."}, {"start": 72.0, "text": "To help you triage performance issues, we will share a mental model to understand"}, {"start": 75.0, "text": "and optimize your app responsiveness."}, {"start": 78.0, "text": "First, we will look at CPU saturation,"}, {"start": 80.0, "text": "and show you how to investigate periods of high system utilization."}, {"start": 85.0, "text": "Next, we will discuss sampling data visualization,"}, {"start": 88.0, "text": "exploring how to interpret your profiling data."}, {"start": 92.0, "text": "Then, we will look at execution contention,"}, {"start": 94.0, "text": "and examine what happens when tasks are starved for resources."}, {"start": 99.0, "text": "And finally, we analyze system blocking,"}, {"start": 102.0, "text": "uncovering the root causes behind why your application stops processing."}, {"start": 106.0, "text": "But before we dive in, let's establish the diagnostic flow."}, {"start": 111.0, "text": "When an application drops frames or hangs."}, {"start": 113.0, "text": "The first step is the Time Profiler."}, {"start": 116.0, "text": "It provides high-level overview needed to orient yourself."}, {"start": 120.0, "text": "From there, the question is:"}, {"start": 122.0, "text": "what is the CPU doing during the hang?"}, {"start": 125.0, "text": "If CPU usage is high, then the thread is busy and the work is taking too long."}, {"start": 131.0, "text": "This points to a code performance bottleneck."}, {"start": 134.0, "text": "When you hit a performance issue on the main thread,"}, {"start": 136.0, "text": "there are two ways to fix it."}, {"start": 138.0, "text": "The first is code optimization:"}, {"start": 141.0, "text": "refactoring algorithms to execute faster."}, {"start": 144.0, "text": "But, if the heavy workload is unavoidable,"}, {"start": 146.0, "text": "resolve it by offloading the work to a background task"}, {"start": 149.0, "text": "so the user interface remains responsive."}, {"start": 153.0, "text": "If the application hangs but the processor is idle,"}, {"start": 156.0, "text": "optimizing algorithms won't be helpful."}, {"start": 159.0, "text": "This, usually, suggest that the main thread is stuck"}, {"start": 161.0, "text": "waiting for a resource to free up."}, {"start": 163.0, "text": "This block can fall into many categories,"}, {"start": 166.0, "text": "but some common ones are:"}, {"start": 167.0, "text": "waiting for the File I/O,"}, {"start": 169.0, "text": "waiting on a synchronization lock,"}, {"start": 171.0, "text": "or waiting for Inter-Process Communication."}, {"start": 174.0, "text": "Because Time Profiler only monitors active CPU cycles,"}, {"start": 177.0, "text": "it provides no visibility into these events."}, {"start": 181.0, "text": "We will demonstrate this workflow using a note-taking application,"}, {"start": 184.0, "text": "that we have been prototyping."}, {"start": 186.0, "text": "This app allows me to draw,"}, {"start": 189.0, "text": "add images,"}, {"start": 192.0, "text": "and use a lasso tool to move elements around."}, {"start": 194.0, "text": "But we've noticed three issues while testing the app."}, {"start": 198.0, "text": "Let's capture a profile to pin these issues down."}, {"start": 201.0, "text": "We start right here in Xcode."}, {"start": 204.0, "text": "To begin profiling, I will open the Product menu and select Profile."}, {"start": 209.0, "text": "This will create a release build of my application."}, {"start": 212.0, "text": "A debug build trades off runtime performance for debug ability,"}, {"start": 215.0, "text": "so profiling data from debug builds can be misleading."}, {"start": 219.0, "text": "Profiling a release build is crucial to get the most actionable data."}, {"start": 223.0, "text": "Because my codebase utilizes Swift Concurrency,"}, {"start": 226.0, "text": "I will choose the Swift Concurrency template from the picker."}, {"start": 233.0, "text": "Within this template, we still have access to the Time Profiler instrument."}, {"start": 239.0, "text": "I'll start the recording,"}, {"start": 242.0, "text": "and switch over to iPad to record the workflows"}, {"start": 244.0, "text": "that were exhibiting performance issues during my testing."}, {"start": 248.0, "text": "When I hit save on a note, the pencil is not instantly responsive."}, {"start": 253.0, "text": "When I scroll through my notes, the UI is not smooth."}, {"start": 259.0, "text": "And when use the lasso tool, I noticed that it hangs."}, {"start": 269.0, "text": "Now, I stop the recording at the top left corner."}, {"start": 276.0, "text": "And we have a trace containing all the information about these three hangs."}, {"start": 282.0, "text": "Hey Harjas, the trace is ready."}, {"start": 283.0, "text": "I'll AirDrop you the file, so we can start the investigation."}, {"start": 286.0, "text": "Thanks, Art."}, {"start": 289.0, "text": "Instruments 27 makes it easy to read and interpret your data."}, {"start": 294.0, "text": "The timeline at the top displays horizontal tracks"}, {"start": 297.0, "text": "for your tasks, actors, and executors."}, {"start": 300.0, "text": "These tracks provide a high-level view of resource usage and events over time."}, {"start": 306.0, "text": "Below the timeline is the detail area."}, {"start": 308.0, "text": "This detail view is based on the selected track in the timeline,"}, {"start": 312.0, "text": "and each track provides its own set of details."}, {"start": 316.0, "text": "You can switch between them by changing the selection"}, {"start": 318.0, "text": "with the pop-up button in the middle bar."}, {"start": 321.0, "text": "On the right-hand side is a brand new Inspector panel."}, {"start": 324.0, "text": "It surfaces additional details"}, {"start": 327.0, "text": "and actions based on selection in the timeline or the detail view."}, {"start": 331.0, "text": "First, let's investigate why the lasso tool"}, {"start": 334.0, "text": "wasn't able to keep up with the pencil."}, {"start": 337.0, "text": "To make this easier I am going to use a tool called OSSignpost."}, {"start": 341.0, "text": "OSLog and OSSignpost offer a set of APIs to enable logging and tracing."}, {"start": 346.0, "text": "For our use case, I added an os.signpost interval around lasso selection."}, {"start": 352.0, "text": "I achieved this by using the OSSignposter type"}, {"start": 354.0, "text": "which has APIs to start and stop tracing an interval."}, {"start": 358.0, "text": "When creating this signposter,"}, {"start": 360.0, "text": "I set the subsystem to \"Demo App\" so that I know it's related to my App."}, {"start": 364.0, "text": "And, I set the category to points of interest,"}, {"start": 367.0, "text": "so that instruments will automatically surface"}, {"start": 369.0, "text": "this data in the points of interest track."}, {"start": 372.0, "text": "Now, we can find the lasso selection interval in the timeline."}, {"start": 381.0, "text": "And use the context menu on the interval to filter the trace to this time period."}, {"start": 390.0, "text": "The hangs instrument shows that there were several hangs during lasso selection,"}, {"start": 394.0, "text": "which lines up with what Art was experiencing."}, {"start": 397.0, "text": "As Art said earlier,"}, {"start": 398.0, "text": "we should check the CPU usage on the main thread during this hang."}, {"start": 403.0, "text": "We can find that by expanding the process track,"}, {"start": 406.0, "text": "which reveals all the threads for this process."}, {"start": 409.0, "text": "The main thread shows that CPU usage is high,"}, {"start": 413.0, "text": "staying around a 100% during this time period."}, {"start": 416.0, "text": "Which suggests our code is executing, but it takes too long."}, {"start": 420.0, "text": "So we should investigate using Time Profiler."}, {"start": 423.0, "text": "Hey Art, can you walk through the ways"}, {"start": 426.0, "text": "we can visualize sampling data in Instruments."}, {"start": 428.0, "text": "Absolutely."}, {"start": 430.0, "text": "When your application is running,"}, {"start": 432.0, "text": "the CPU is generating thousands of samples every single second."}, {"start": 436.0, "text": "Instruments is built to handle that scale,"}, {"start": 438.0, "text": "providing visualizations for different categories of performance analysis."}, {"start": 443.0, "text": "Let's break down how the Time Profiler translates raw execution into a call tree."}, {"start": 449.0, "text": "It uses a hardware timer to sample the state of your app's execution"}, {"start": 453.0, "text": "at regular intervals."}, {"start": 455.0, "text": "The default sampling rate is one millisecond."}, {"start": 459.0, "text": "When the timer fires, it records the current call stack on every core."}, {"start": 464.0, "text": "In this first sample, main calls saveNote."}, {"start": 468.0, "text": "In the call tree, each of these functions receives a weight of 1."}, {"start": 472.0, "text": "Because saveNote is the function actively executing at the bottom of the stack,"}, {"start": 476.0, "text": "it receives a self-weight of 1."}, {"start": 481.0, "text": "A millisecond later, the second sample fires."}, {"start": 484.0, "text": "This time, we sample main calling renderCanvas,"}, {"start": 487.0, "text": "which calls drawStroke function."}, {"start": 490.0, "text": "Their weights are, also, added to the tree."}, {"start": 494.0, "text": "On the third sample, drawStroke has finished,"}, {"start": 497.0, "text": "so we sample main and renderCanvas."}, {"start": 500.0, "text": "Main increments to a total weight of 3,"}, {"start": 502.0, "text": "and renderCanvas increments to a weight of 2."}, {"start": 507.0, "text": "Notice the fast swift_retain call."}, {"start": 509.0, "text": "Because it started and finished entirely between the samples, it is never recorded,"}, {"start": 514.0, "text": "but it is important to note that the more frequent it called"}, {"start": 517.0, "text": "the more likely it will be sampled."}, {"start": 521.0, "text": "This call tree is the raw data driving your investigation."}, {"start": 525.0, "text": "But, reading it can be difficult to parse at a glance."}, {"start": 529.0, "text": "To make this data even more intuitive,"}, {"start": 531.0, "text": "Instruments can render it visually as a flame graph."}, {"start": 535.0, "text": "The flame graph maps the all tree structure into spacial blocks."}, {"start": 540.0, "text": "Main, with a weight of 3, forms the top level."}, {"start": 544.0, "text": "The functions it called, renderCanvas and saveNote,"}, {"start": 547.0, "text": "cascade downwards proportionally to their weights,"}, {"start": 551.0, "text": "with drawStroke node sitting at the very bottom."}, {"start": 555.0, "text": "In this visualization, the vertical axis represents the call stack,"}, {"start": 560.0, "text": "with callers on top and callees growing downward."}, {"start": 564.0, "text": "The horizontal axis represents total CPU time,"}, {"start": 567.0, "text": "but instead of a chronological timeline, it's an aggregated view."}, {"start": 571.0, "text": "The wider the bar, the more samples that function appeared in,"}, {"start": 574.0, "text": "allowing you to instantly spot expensive code paths."}, {"start": 579.0, "text": "However, for code that is called from a lot of places,"}, {"start": 582.0, "text": "like Swift runtime functions and various helper utilities."}, {"start": 586.0, "text": "The structural view of a flame graph distributes their total cost."}, {"start": 591.0, "text": "The execution time is fractured into small pieces"}, {"start": 594.0, "text": "across every distinct branch that calls them."}, {"start": 597.0, "text": "This distribution makes it difficult to answer"}, {"start": 599.0, "text": "which specific functions burned the most overall cycles."}, {"start": 604.0, "text": "To answer that question,"}, {"start": 605.0, "text": "Instruments introduces new analysis mode Top Functions."}, {"start": 609.0, "text": "This new mode discards the call hierarchy."}, {"start": 613.0, "text": "Instead, it extracts every single scattered node"}, {"start": 617.0, "text": "and merges them together to form one block."}, {"start": 621.0, "text": "This is evaluated using the self metric,"}, {"start": 623.0, "text": "which calculates the amount of time spent executing instructions"}, {"start": 626.0, "text": "directly inside that specific function."}, {"start": 630.0, "text": "Harjas, I think we are ready look at the data, take it away."}, {"start": 632.0, "text": "Thanks Art."}, {"start": 634.0, "text": "When viewing a profile in Instruments,"}, {"start": 636.0, "text": "the default view is an outline call tree display,"}, {"start": 638.0, "text": "this is great when looking at sample counts."}, {"start": 641.0, "text": "But, as Art mentioned a flame graph can be easier to scan through"}, {"start": 644.0, "text": "and visually spot issues."}, {"start": 647.0, "text": "Using the segmented control in the bar above the call tree,"}, {"start": 650.0, "text": "we can switch the detail view to display the flame graph."}, {"start": 653.0, "text": "Doing a quick scan through it reveals that the time is split across"}, {"start": 656.0, "text": "different codepaths in the rendering code for our canvas."}, {"start": 660.0, "text": "There is no single obvious issue to address."}, {"start": 664.0, "text": "Rather, these different codepaths sum together"}, {"start": 667.0, "text": "to be costly enough to cause hangs."}, {"start": 670.0, "text": "I am going to try the new top functions mode in Instruments,"}, {"start": 673.0, "text": "as it might surface something"}, {"start": 675.0, "text": "that would be harder to find in the call tree or the flame graph."}, {"start": 678.0, "text": "This can be found in the same control we used before."}, {"start": 685.0, "text": "On the left hand side is a list of all the Top Functions sorted by self weight."}, {"start": 690.0, "text": "The right hand side displays a flame graph of all the code paths"}, {"start": 694.0, "text": "that called into the selected function."}, {"start": 697.0, "text": "The Top Function during lasso selection is swift_project_boxed_opaque_existential."}, {"start": 704.0, "text": "This runtime function is responsible for unwrapping an existential"}, {"start": 708.0, "text": "so our code can operate over it."}, {"start": 710.0, "text": "I will ask the coding assistant in Xcode to rewrite our drawing code"}, {"start": 714.0, "text": "to use concrete types and generics instead of existentials."}, {"start": 723.0, "text": "While this is running, I want to explain what is an existential"}, {"start": 727.0, "text": "and why I asked the coding assistant to make this change."}, {"start": 731.0, "text": "In Swift, we often want to have a variable"}, {"start": 733.0, "text": "that can hold any type conforming to a protocol"}, {"start": 736.0, "text": "without having to know the specific type at compile time."}, {"start": 740.0, "text": "One way of achieving this is by using the any keyword before the protocol name,"}, {"start": 744.0, "text": "this is called an existential."}, {"start": 746.0, "text": "Because the possible types can vary in size,"}, {"start": 750.0, "text": "existentials may require additional work to access"}, {"start": 752.0, "text": "and operate over the underlying value."}, {"start": 755.0, "text": "This is proving to be too expensive for our use case."}, {"start": 759.0, "text": "There are several alternatives to existentials"}, {"start": 761.0, "text": "when performance is paramount"}, {"start": 764.0, "text": "including concrete types,"}, {"start": 766.0, "text": "generics,"}, {"start": 767.0, "text": "and in some cases, enums."}, {"start": 769.0, "text": "These approaches give the compiler more information,"}, {"start": 772.0, "text": "allowing for better optimizations."}, {"start": 775.0, "text": "To learn more about generics in Swift,"}, {"start": 777.0, "text": "watch \"Embrace Swift generics\" from WWDC22."}, {"start": 781.0, "text": "Let's check back in with the assistant."}, {"start": 785.0, "text": "It appears that the coding assistant has finished making those changes."}, {"start": 789.0, "text": "I will send an updated trace over to you, Art."}, {"start": 792.0, "text": "Thank you Harjas."}, {"start": 793.0, "text": "To verify the fix we made,"}, {"start": 795.0, "text": "I could open them up side-by-side in different windows"}, {"start": 798.0, "text": "and compare the Top Functions data."}, {"start": 800.0, "text": "However, Instruments now allows you to directly compare profiling data"}, {"start": 804.0, "text": "across runs in a single document,"}, {"start": 806.0, "text": "to help speedup the work of confirming your changes."}, {"start": 810.0, "text": "New in Instruments we're excited to introduce Run Comparisons."}, {"start": 814.0, "text": "It computes the exact performance delta"}, {"start": 816.0, "text": "by cross-referencing all of samples from the baseline trace and optimized trace."}, {"start": 821.0, "text": "It evaluates every node in the stack."}, {"start": 824.0, "text": "To do this,"}, {"start": 825.0, "text": "Instruments matches the old version of a function from your baseline run"}, {"start": 828.0, "text": "directly to the new version in your optimized one."}, {"start": 832.0, "text": "Once matched, it calculates the delta"}, {"start": 835.0, "text": "and sorts them based on their performance difference."}, {"start": 838.0, "text": "A red block indicates a performance regression."}, {"start": 842.0, "text": "A green block indicates a performance improvement, or run time optimization."}, {"start": 846.0, "text": "So, let's try it out."}, {"start": 848.0, "text": "To ensure an accurate comparison without noise,"}, {"start": 850.0, "text": "we first filter both runs"}, {"start": 851.0, "text": "to the exact same os_signpost interval for the lasso selection."}, {"start": 865.0, "text": "Then, to compare sampling data on the main thread,"}, {"start": 868.0, "text": "I will select the main thread track."}, {"start": 875.0, "text": "And, by clicking the compare button in the middle bar,"}, {"start": 878.0, "text": "I can select my baseline run from the dropdown menu."}, {"start": 883.0, "text": "This will add a comparison tab in the sidebar."}, {"start": 886.0, "text": "You can create multiple comparisons,"}, {"start": 888.0, "text": "and they are saved to the document to make collaboration easier."}, {"start": 891.0, "text": "A comparison tree can be visualized as a textual call tree."}, {"start": 897.0, "text": "Here we can see that overall execution time"}, {"start": 899.0, "text": "for lasso selection has decreased."}, {"start": 902.0, "text": "Switching to a flame graph,"}, {"start": 904.0, "text": "we can see code paths that improved in green"}, {"start": 907.0, "text": "and the ones that regressed in red."}, {"start": 909.0, "text": "When the coding assistant adopted generics it introduced new functions,"}, {"start": 914.0, "text": "which Run Comparisons will mark as regressions."}, {"start": 917.0, "text": "And in Top Functions view we can now see what improved the most"}, {"start": 921.0, "text": "and what regressed the most."}, {"start": 923.0, "text": "By default, regressions in Run Comparisons are sorted to the top."}, {"start": 927.0, "text": "These regressions are new functions added by the coding assistant"}, {"start": 930.0, "text": "as it worked to eliminate the usage of existentials."}, {"start": 933.0, "text": "We can flip the sort order to see the improvements."}, {"start": 937.0, "text": "Here, the swift_project_boxed_opaque_existential"}, {"start": 940.0, "text": "call has been removed completely."}, {"start": 942.0, "text": "And overall, the improvements out weight the regressions."}, {"start": 945.0, "text": "This confirms that adopting concrete types and generics"}, {"start": 948.0, "text": "successfully eliminated this specific runtime overhead."}, {"start": 952.0, "text": "To learn more about optimizing CPU work,"}, {"start": 954.0, "text": "watch the \"Optimize CPU performance with Instruments\" session from WWDC25."}, {"start": 961.0, "text": "Harjas, 1 hang down, 2 to go."}, {"start": 963.0, "text": "That's right."}, {"start": 965.0, "text": "Now that we've optimized the drawing code,"}, {"start": 967.0, "text": "we can return to our baseline run and keep optimizing other aspects of the app."}, {"start": 972.0, "text": "There are still several hangs that need to be investigated."}, {"start": 979.0, "text": "Unfortunately, these don't have any logs in the points of interest track"}, {"start": 983.0, "text": "to help contextualize what was happening at the time."}, {"start": 986.0, "text": "Another way to contextualize this"}, {"start": 988.0, "text": "would be to see what tasks are on the Main Actor during these hangs."}, {"start": 992.0, "text": "Instruments 27 has a new Swift executors instrument."}, {"start": 1000.0, "text": "This instrument visualizes the Main Actor, the global concurrent executor,"}, {"start": 1004.0, "text": "and any custom executors in your process."}, {"start": 1007.0, "text": "For each of these hangs,"}, {"start": 1009.0, "text": "the Main Actor track shows a corresponding Swift task called renderThumbnail."}, {"start": 1013.0, "text": "We can select this track"}, {"start": 1014.0, "text": "and a get a summary of all the tasks running on the Main Actor."}, {"start": 1020.0, "text": "It appears that we have several render thumbnail tasks"}, {"start": 1023.0, "text": "on the Main Actor taking a few hundred ms to run."}, {"start": 1027.0, "text": "This could help explain why scrolling the list of notes wasn't smooth."}, {"start": 1032.0, "text": "Let's follow our diagnostic flow, and check the CPU usage."}, {"start": 1035.0, "text": "I will filter the trace to one of these hangs."}, {"start": 1042.0, "text": "And use the inspector to pin the main thread."}, {"start": 1050.0, "text": "In this period, time profiler reports"}, {"start": 1052.0, "text": "that CPU usage of the main thread is around a 100%."}, {"start": 1056.0, "text": "So we aren't waiting on other system resources,"}, {"start": 1059.0, "text": "these tasks are just taking too long to run on the Main Actor."}, {"start": 1063.0, "text": "So why is this a problem?"}, {"start": 1065.0, "text": "Well, first let's review what the Main Actor is"}, {"start": 1067.0, "text": "and then discuss how we can resolve this hang."}, {"start": 1071.0, "text": "The Main Actor is responsible for handling"}, {"start": 1073.0, "text": "all user interface updates and interactions."}, {"start": 1077.0, "text": "The application renders all the thumbnails asynchronously."}, {"start": 1080.0, "text": "But because this code was called from SwiftUI,"}, {"start": 1083.0, "text": "it inherited the Main Actor context."}, {"start": 1086.0, "text": "These tasks will compete with critical UI updates for the Main Actor,"}, {"start": 1090.0, "text": "preventing the app from delivering a smooth experience."}, {"start": 1094.0, "text": "To resolve this, we must route"}, {"start": 1095.0, "text": "the thumbnail rendering to the thread pool."}, {"start": 1098.0, "text": "This frees up the Main Actor, allowing the pending UI events to execute smoothly."}, {"start": 1104.0, "text": "This is the code responsible for generating our thumbnails."}, {"start": 1109.0, "text": "We can refactor it by adding the @concurrent attribute"}, {"start": 1112.0, "text": "to the task initializer,"}, {"start": 1113.0, "text": "this will move the thumbnail rendering task off the Main Actor,"}, {"start": 1117.0, "text": "and onto the global executor."}, {"start": 1120.0, "text": "The swift compiler will check that this code change"}, {"start": 1123.0, "text": "does't introduce any race conditions."}, {"start": 1126.0, "text": "In the updated trace,"}, {"start": 1127.0, "text": "the Swift executors instrument shows"}, {"start": 1130.0, "text": "that the thumbnail rendering tasks have moved"}, {"start": 1132.0, "text": "from the Main Actor track to the global executor track."}, {"start": 1137.0, "text": "Not only did moving this to the global concurrent executor"}, {"start": 1140.0, "text": "prevent hangs in the UI,"}, {"start": 1142.0, "text": "but it also allowed us to render these thumbnails in parallel."}, {"start": 1147.0, "text": "To learn more about using Swift Concurrency,"}, {"start": 1150.0, "text": "watch \"Embracing Swift Concurrency\" from WWDC25."}, {"start": 1155.0, "text": "Alright Art, one more hang left!"}, {"start": 1157.0, "text": "Let's bring it home."}, {"start": 1159.0, "text": "Now that the concurrency contention is resolved,"}, {"start": 1162.0, "text": "let's return to our baseline run for the final hang."}, {"start": 1165.0, "text": "When we hit save, the user interface hangs for a short time."}, {"start": 1168.0, "text": "I am going to use the Write to File interval"}, {"start": 1170.0, "text": "in the points of interest track to find this hang."}, {"start": 1181.0, "text": "And I am going to set the inspector range"}, {"start": 1182.0, "text": "and zoom in by clicking on appropriate option in the contextual menu."}, {"start": 1193.0, "text": "There is a reported micro hang during this step."}, {"start": 1195.0, "text": "Again, let's check the CPU usage."}, {"start": 1206.0, "text": "In this case, it's actually quite low, hovering around 20%."}, {"start": 1212.0, "text": "The diagnostic points us to low CPU usage."}, {"start": 1215.0, "text": "When the interface freezes under these conditions,"}, {"start": 1218.0, "text": "it tells us the main thread is blocked waiting on a system resource."}, {"start": 1223.0, "text": "To understand exactly why that happens,"}, {"start": 1225.0, "text": "let's look at how threads transition between states."}, {"start": 1229.0, "text": "A low CPU utilization can be deceptive,"}, {"start": 1231.0, "text": "because it doesn't mean your code is executing slowly,"}, {"start": 1234.0, "text": "it means the thread has stopped running."}, {"start": 1237.0, "text": "System Trace template is built to visualize exactly"}, {"start": 1240.0, "text": "when and why the operating system pauses your application."}, {"start": 1244.0, "text": "Here, the main thread is actively running on a CPU core."}, {"start": 1249.0, "text": "The operating system provides whole host of system calls to do work on your behalf"}, {"start": 1253.0, "text": "and control the hardware."}, {"start": 1256.0, "text": "But when a resource isn't immediately available"}, {"start": 1259.0, "text": "a thread enters the blocked state."}, {"start": 1261.0, "text": "When this occurs, the kernel evicts the thread from the processor."}, {"start": 1266.0, "text": "Only when the resource is finally ready, the thread becomes runnable,"}, {"start": 1269.0, "text": "and returns to the core."}, {"start": 1271.0, "text": "To be more specific, when the hardware eventually finishes the job,"}, {"start": 1275.0, "text": "the thread doesn't immediately start executing."}, {"start": 1278.0, "text": "It first enters the runnable state,"}, {"start": 1280.0, "text": "which means the resource is ready,"}, {"start": 1283.0, "text": "but the thread has to wait in line for the OS scheduler"}, {"start": 1286.0, "text": "to assign it a free CPU core."}, {"start": 1289.0, "text": "Now, zooming back."}, {"start": 1291.0, "text": "Notice this blocked state highlighted here."}, {"start": 1293.0, "text": "It spends the vast majority of its time"}, {"start": 1295.0, "text": "blocked waiting for that external dependency to resolve."}, {"start": 1299.0, "text": "When it is resolved the thread wakes up briefly"}, {"start": 1302.0, "text": "to coordinate the next stage of the request."}, {"start": 1304.0, "text": "These brief moments of execution are exactly what cause"}, {"start": 1308.0, "text": "that twenty percent of CPU utilization."}, {"start": 1311.0, "text": "During this phase, the thread is completely idle."}, {"start": 1314.0, "text": "A single system call like this"}, {"start": 1317.0, "text": "often relies on multiple underlying dependencies,"}, {"start": 1320.0, "text": "forcing the main thread to wait until the OS resolves every single one of them."}, {"start": 1325.0, "text": "It finally returns to the core to finish the work."}, {"start": 1329.0, "text": "Optimizing algorithms yields no improvement here,"}, {"start": 1332.0, "text": "because there is no code running to optimize."}, {"start": 1335.0, "text": "Let's find out together what is happening with the Write to File interval."}, {"start": 1340.0, "text": "Here is a profile of this hang using the System Trace template."}, {"start": 1343.0, "text": "With System Trace, we can see exactly what the thread was doing,"}, {"start": 1347.0, "text": "including key OS concepts like system calls."}, {"start": 1350.0, "text": "We can pin the main thread using the inspector, and zoom in."}, {"start": 1362.0, "text": "It reveals that while saving the file,"}, {"start": 1364.0, "text": "the activity lane shows a large amount of blank space."}, {"start": 1368.0, "text": "This indicates that the thread was blocked,"}, {"start": 1369.0, "text": "preventing the UI from updating."}, {"start": 1372.0, "text": "The purple intervals you see indicate that a system call is running."}, {"start": 1375.0, "text": "However, just because a syscall is active"}, {"start": 1378.0, "text": "does not mean the thread is actually executing the application code."}, {"start": 1381.0, "text": "I am going to select one of these intervals."}, {"start": 1386.0, "text": "Notice, when I do so,"}, {"start": 1387.0, "text": "more than just the segment I clicked is highlighted."}, {"start": 1390.0, "text": "This visualizes one continuous write system call"}, {"start": 1393.0, "text": "that spans both on and off-core time."}, {"start": 1396.0, "text": "The opaque segments represent the time spent actively running on-core,"}, {"start": 1400.0, "text": "while the translucent segments are the periods it was blocked off-core."}, {"start": 1405.0, "text": "To understand why it was blocked for so long,"}, {"start": 1408.0, "text": "we can look at the Inspector."}, {"start": 1410.0, "text": "The Inspector gives us the exact arguments passed to this system call."}, {"start": 1414.0, "text": "We can see the target file descriptor,"}, {"start": 1417.0, "text": "the memory address of the buffer,"}, {"start": 1420.0, "text": "and most importantly, the size."}, {"start": 1424.0, "text": "It is trying to write over 1.7 gigabytes of data on the main thread."}, {"start": 1429.0, "text": "We can also see the performance cost."}, {"start": 1431.0, "text": "This single operation took over 500 milliseconds,"}, {"start": 1434.0, "text": "and almost 300 of those milliseconds were spent off-core waiting for the disk."}, {"start": 1440.0, "text": "Because the file write was initiated synchronously on the main thread,"}, {"start": 1443.0, "text": "the application freezes waiting for the storage to respond."}, {"start": 1447.0, "text": "To fix this, I will refactor the code to move the file I/O in the background."}, {"start": 1452.0, "text": "This is the snippet responsible for that workflow."}, {"start": 1455.0, "text": "We're using the PropertyListEncoder class to serialize our data,"}, {"start": 1459.0, "text": "and the bottleneck is this line right here."}, {"start": 1462.0, "text": "We are calling the data.write method synchronously."}, {"start": 1465.0, "text": "Because this atomic write is executing directly on the main thread,"}, {"start": 1469.0, "text": "it's exactly what caused that block."}, {"start": 1472.0, "text": "We, again, can wrap this code inside a Swift task."}, {"start": 1475.0, "text": "By doing this, we push the encoding"}, {"start": 1476.0, "text": "and file writing onto concurrent thread pool"}, {"start": 1479.0, "text": "and unblock the Main Actor."}, {"start": 1481.0, "text": "Here's another profile to verify"}, {"start": 1483.0, "text": "that we no longer block the main thread during file saving."}, {"start": 1486.0, "text": "I have already navigated myself"}, {"start": 1488.0, "text": "using the Writing to File signpost interval."}, {"start": 1490.0, "text": "The main thread no longer shows the write system call."}, {"start": 1497.0, "text": "We can now find the same syscall on a background thread."}, {"start": 1504.0, "text": "Which mean using the Apple Pencil should now feel fluid."}, {"start": 1507.0, "text": "Let's open the app and confirm the wins."}, {"start": 1510.0, "text": "With the file I/O properly routed,"}, {"start": 1512.0, "text": "saving the document is now happening in the background,"}, {"start": 1514.0, "text": "allowing interacting with the application."}, {"start": 1518.0, "text": "And because we also optimized the CPU saturation"}, {"start": 1520.0, "text": "and resource contention,"}, {"start": 1522.0, "text": "using the lasso tool and scrolling my notes both remain responsive."}, {"start": 1529.0, "text": "By following the data, we have successfully eliminated"}, {"start": 1531.0, "text": "all three types of hangs from our baseline run."}, {"start": 1534.0, "text": "Consistently engineering responsive applications"}, {"start": 1537.0, "text": "requires matching your profiling tool"}, {"start": 1539.0, "text": "to the specific performance symptom."}, {"start": 1542.0, "text": "When the CPU is overloaded,"}, {"start": 1543.0, "text": "utilize Top Functions to isolate scattered software overhead,"}, {"start": 1547.0, "text": "and use Run Comparison to verify your improvements."}, {"start": 1551.0, "text": "When the tasks fight for resources, use the Swift Concurrency instrument"}, {"start": 1554.0, "text": "to identify actor congestion."}, {"start": 1558.0, "text": "And when a thread is idle,"}, {"start": 1559.0, "text": "leverage System Trace and the Inspector panel"}, {"start": 1562.0, "text": "to uncover synchronous blocking behaviors like file I/O."}, {"start": 1567.0, "text": "As you apply these workflows to your own codebases,"}, {"start": 1570.0, "text": "ensure your profiling is accurate."}, {"start": 1572.0, "text": "Always profile a release build."}, {"start": 1574.0, "text": "And leverage os_signpost"}, {"start": 1576.0, "text": "to make sure your intervals for run comparisons are reliable."}, {"start": 1579.0, "text": "To go even deeper into these topics, we highly recommend checking out"}, {"start": 1583.0, "text": "\"Analyze hangs with Instruments\" session from WWDC 2023."}, {"start": 1587.0, "text": "With Instruments you never have to guess where to look."}, {"start": 1591.0, "text": "Profile often, and let the data tell the story."}, {"start": 1594.0, "text": "Thanks for watching!"}]}