Co nowego w Instruments 27 dla responsywności aplikacji
Podczas WWDC 2026 dwóch inżynierów Apple profilowało aplikację do notatek, która wykazywała trzy odrębne zawieszenia: brak reakcji ołówka przy zapisie, szarpane przewijanie oraz zacięcie przy użyciu narzędzia lasso. Naprawili wszystkie trzy, przypisując każdemu objawowi jedno nowe narzędzie z Instruments 27, a następnie potwierdzili każdą poprawkę porównaniem wersji bazowej z zoptymalizowaną.1 Teza sesji to przepływ diagnostyczny: odczytaj CPU podczas zawieszenia, a CPU podpowie, który instrument otworzyć w następnej kolejności. Wysokie CPU oznacza, że kod jest zbyt wolny. Bezczynne CPU oznacza, że kod jest zablokowany. Instruments 27 dostarcza narzędzia do rozwiązania obu przypadków oraz nowy widok potwierdzający, że poprawka rzeczywiście zadziałała.
Ten artykuł omawia trzy narzędzia stanowiące oś tego przepływu: Top Functions do znajdowania największej wagi własnej, gdy CPU jest nasycone, nowy instrument Swift executors pozwalający zobaczyć, na którym executorze wykonało się zadanie, gdy praca rywalizuje o Main Actor, oraz nowy panel Inspector do odczytania argumentów wywołania systemowego, gdy wątek pozostaje bezczynny w oczekiwaniu na system. Run Comparisons spaja to wszystko, mierząc, czy każda zmiana poprawiła ślad. Wszystko poniżej pochodzi bezpośrednio z sesji.
TL;DR
- Instruments 27 reorganizuje przepływ pracy nad responsywnością wokół reguły diagnostycznej: najpierw otwórz Time Profiler, sprawdź CPU wątku głównego podczas zawieszenia i pozwól, by ten odczyt skierował Cię do właściwego narzędzia.1
Top Functionsto nowy tryb analizy, który odrzuca hierarchię wywołań i scala każdy rozproszony węzeł według wagi własnej, wydobywając narzut wykonania, który flame graph rozdrabnia pomiędzy gałęzie.1Run Comparisonsjest „New in Instruments” i oblicza dokładną różnicę wydajności między śladem bazowym a śladem zoptymalizowanym, dopasowując każdą funkcję między uruchomieniami oraz oznaczając regresje na czerwono, a poprawy na zielono.1- Nowy instrument
Swift executorswizualizuje Main Actor, globalny executor współbieżny oraz wszelkie executory niestandardowe, dzięki czemu można zobaczyć, na którym executorze wykonało się zadanie, i wychwycić rywalizację o Main Actor.1 - Nowy panel Inspector ujawnia dokładne argumenty wywołania systemowego (deskryptor pliku, adres bufora, rozmiar zapisu) i rozdziela czas on-core od off-core, ujawniając synchroniczne blokowanie, takie jak zapis 1,7 GB na wątku głównym.1
Przepływ diagnostyczny, wokół którego zbudowano Instruments 27
Przed jakimkolwiek nowym narzędziem sesja ustanawia regułę, która je porządkuje. Gdy aplikacja gubi klatki lub się zawiesza, pierwszym krokiem jest Time Profiler, który daje ogólny obraz na wysokim poziomie pozwalający się zorientować.1 Stamtąd jedno pytanie kieruje wszystkim: co robi CPU podczas zawieszenia?
Jeśli wykorzystanie CPU jest wysokie, wątek jest zajęty, a praca trwa zbyt długo, co wskazuje na wąskie gardło wydajności kodu. Naprawia się to na dwa sposoby: refaktoryzując algorytm tak, by działał szybciej, albo — gdy ciężka praca jest nieunikniona — przenosząc ją do zadania w tle, aby interfejs pozostał responsywny.1 Jeśli aplikacja zawiesza się, gdy procesor pozostaje bezczynny, optymalizacja algorytmów nie pomoże, ponieważ wątek główny utknął, czekając na zwolnienie zasobu: operacji wejścia/wyjścia na pliku, blokady synchronizacji lub komunikacji międzyprocesowej. Jak ujmuje to sesja: „Ponieważ Time Profiler monitoruje wyłącznie aktywne cykle CPU, nie zapewnia żadnego wglądu w te zdarzenia.”1
Inżynierowie profilowali kompilację release aplikacji do notatek, ponieważ „kompilacja debug poświęca wydajność wykonania na rzecz łatwości debugowania, więc dane profilowania z kompilacji debug mogą wprowadzać w błąd.”1 Wybrali szablon Swift Concurrency, który mimo wszystko udostępnia instrument Time Profiler, i zarejestrowali wszystkie trzy zawieszenia w jednym śladzie bazowym. Owinęli też zaznaczenie lasso w interwał os_signpost przy użyciu typu OSSignposter, ustawiając kategorię na points of interest, aby Instruments pokazał interwał na ścieżce points of interest. Ten signpost staje się punktem zaczepienia do filtrowania śladu, a później do pozbawionego szumu porównania uruchomień.1
Art i Harjas przedstawiają przepływ diagnostyczny przed demonstracją, począwszy od 1:50.
Samo okno Instruments 27 nadaje ramy przepływowi pracy. Oś czasu u góry pokazuje poziome ścieżki dla zadań, aktorów i executorów. Obszar szczegółów poniżej zmienia się w zależności od wybranej ścieżki. Po prawej stronie znajduje się „zupełnie nowy panel Inspector”, który ujawnia dodatkowe szczegóły i działania w zależności od tego, co zostanie wybrane.1 Trzy narzędzia wypełniają te ramy, a każde jest powiązane z jednym z trzech zawieszeń.
Top Functions: gdy CPU jest nasycone
Zawieszenie lassa odczytuje się jako wysokie CPU. Po przefiltrowaniu śladu do interwału os_signpost lassa instrument hangs potwierdził tam kilka zawieszeń, a rozwinięcie ścieżki procesu do wątku głównego pokazało CPU „utrzymujące się około 100% w tym okresie.”1 Wysokie CPU oznacza, że kod się wykonuje, ale trwa zbyt długo, więc Time Profiler jest właściwym narzędziem.
Sesja wyjaśnia, dlaczego sam flame graph tu nie wystarcza. Time Profiler korzysta ze sprzętowego timera, by próbkować stos wywołań z domyślną częstotliwością jednej milisekundy, rejestrując bieżący stos na każdym rdzeniu. Każda spróbkowana funkcja otrzymuje wagę, a funkcja na dole stosu otrzymuje wagę własną — czas spędzony na wykonywaniu instrukcji bezpośrednio wewnątrz niej.1 Flame graph renderuje to drzewo wywołań jako przestrzenne bloki, z wywołującymi na górze, wywoływanymi rosnącymi w dół i szerokością słupków proporcjonalną do całkowitego czasu CPU.1 Problem: „dla kodu wywoływanego z wielu miejsc, jak funkcje runtime’u Swift i rozmaite narzędzia pomocnicze”, flame graph rozkłada całkowity koszt na każdą gałąź, która je wywołuje. Czas wykonania rozpada się na drobne kawałki, co „utrudnia odpowiedź na pytanie, które konkretne funkcje spaliły łącznie najwięcej cykli.”1
Tę lukę wypełnia Top Functions. Jak sesja opisuje nowy tryb: „Ten nowy tryb odrzuca hierarchię wywołań. Zamiast tego wydobywa każdy pojedynczy rozproszony węzeł i scala je w jeden blok”, oceniany według metryki self.1 Przegląd flame graphu lassa nie wskazał żadnego pojedynczego winowajcy, jedynie „różne ścieżki kodu, które po zsumowaniu stają się na tyle kosztowne, by powodować zawieszenia.”1 Po przełączeniu na Top Functions, posortowany według wagi własnej, najwyższym wpisem był swift_project_boxed_opaque_existential — funkcja runtime’u, która rozpakowuje typ egzystencjalny, aby kod mógł na nim operować.1
Poprawka tkwiła w systemie typów, nie na osi czasu. Inżynier poprosił asystenta kodu w Xcode o przepisanie kodu rysowania tak, by używał typów konkretnych i generyków zamiast typów egzystencjalnych, ponieważ typy egzystencjalne mogą różnić się rozmiarem i wymagają dodatkowej pracy przy dostępie, co okazało się zbyt kosztowne w tym przypadku użycia.1 Wniosek dotyczący narzędzia: Top Functions istnieje po to, by wychwytywać rozproszony narzut oprogramowania, którego żadna pojedyncza gałąź flame graphu nie uwidacznia.
Run Comparisons: dowód, że poprawka zadziałała
Potwierdzenie poprawki oznaczało dawniej otwarcie dwóch śladów w osobnych oknach i porównywanie na oko danych Top Functions obok siebie. Instruments 27 zastępuje to narzędziem Run Comparisons, opisanym w sesji jako „New in Instruments.”1 „Oblicza dokładną różnicę wydajności, krzyżując wszystkie próbki ze śladu bazowego i śladu zoptymalizowanego”, oceniając każdy węzeł w stosie. Dopasowuje starą wersję funkcji z uruchomienia bazowego do nowej wersji z uruchomienia zoptymalizowanego, oblicza różnicę i sortuje według różnicy wydajności. Czerwony blok oznacza regresję, zielony blok oznacza poprawę.1
Ten przepływ pracy celowo dba o usunięcie szumu. Inżynierowie najpierw przefiltrowali oba uruchomienia do dokładnie tego samego interwału os_signpost dla zaznaczenia lasso, następnie wybrali ścieżkę wątku głównego i kliknęli przycisk porównania, by wybrać uruchomienie bazowe z listy rozwijanej.1 Dodaje to kartę porównania na pasku bocznym, a „można utworzyć wiele porównań, które są zapisywane w dokumencie, co ułatwia współpracę.”1
Porównanie opowiedziało uczciwą historię. Tekstowe drzewo wywołań pokazało, że łączny czas wykonania lassa się zmniejszył. Flame graph pokazał poprawione ścieżki na zielono, a ścieżki z regresją na czerwono, przy czym regresje stanowiły „nowe funkcje dodane przez asystenta kodu w trakcie pracy nad wyeliminowaniem użycia typów egzystencjalnych.”1 W widoku Top Functions regresje są domyślnie sortowane na górze; odwrócenie sortowania ujawniło, że swift_project_boxed_opaque_existential „został całkowicie usunięty”, a „ogólnie rzecz biorąc, poprawy przeważają (out[weigh]) nad regresjami.”1 Ta różnica to krok weryfikacji: Run Comparisons istnieje po to, by poprawka była zmierzonym wynikiem, a nie wynikiem opartym na nadziei. Stała rada sesji brzmi: „wykorzystuj os_signpost, by mieć pewność, że interwały do porównań uruchomień są wiarygodne.”1
Instrument Swift executors: gdy zadania rywalizują o Main Actor
Zawieszenie przewijania nie miało dziennika points of interest, na którym można by się oprzeć. Sesja sięga po inny rodzaj kontekstu: „jakie zadania znajdują się na Main Actor podczas tych zawieszeń.”1 To zadanie nowego instrumentu Swift executors, który „wizualizuje Main Actor, globalny executor współbieżny oraz wszelkie executory niestandardowe w procesie.”1
Przy każdym zawieszeniu przewijania ścieżka Main Actor pokazywała zadanie Swift o nazwie renderThumbnail. Wybranie ścieżki podsumowywało zadania na Main Actor i ujawniało „kilka zadań render thumbnail na Main Actor, których wykonanie trwało kilkaset ms”, co pokrywa się z szarpanym przewijaniem.1 Podążając za przepływem diagnostycznym, inżynier przefiltrował do jednego zawieszenia i użył Inspectora do przypięcia wątku głównego; Time Profiler zgłosił CPU „około 100%”, wykluczając oczekiwanie na zasób systemowy. Zadania po prostu trwały zbyt długo na Main Actor.1
Przyczyną źródłową jest subtelność dziedziczenia kontekstu, którą instrument czyni widoczną. Main Actor obsługuje wszystkie aktualizacje i interakcje interfejsu. Aplikacja renderowała miniatury asynchronicznie, ale ponieważ ten kod był wywoływany z SwiftUI, „odziedziczył kontekst Main Actor”, więc zadania miniatur rywalizowały z krytycznymi aktualizacjami interfejsu.1 Poprawka kieruje pracę do puli wątków. Inżynier dodał atrybut @concurrent do inicjalizatora zadania, co przenosi zadanie renderowania z Main Actor na executor globalny, a kompilator Swift sprawdza, że zmiana nie wprowadza żadnych sytuacji wyścigu.1 Instrument potwierdził to przeniesienie: w zaktualizowanym śladzie „zadania renderowania miniatur przeniosły się ze ścieżki Main Actor na ścieżkę executora globalnego”, a praca wykonuje się teraz równolegle.1 Istota narzędzia: instrument Swift executors istnieje po to, by identyfikować zatłoczenie aktora, pokazując dokładnie, który executor wykonał które zadanie.
Panel Inspector: gdy wątek jest bezczynny i zablokowany
Zawieszenie przy zapisie odwraca pierwsze dwa. Po przefiltrowaniu do interwału os_signpost Write to File i powiększeniu zgłoszone mikrozawieszenie pokazało CPU „oscylujące wokół 20%.”1 Niskie CPU bywa mylące: „nie oznacza ono, że kod wykonuje się wolno, lecz że wątek przestał działać.”1 Gdy interfejs zamarza przy niskim CPU, wątek główny jest zablokowany w oczekiwaniu na zasób systemowy, a optymalizacja algorytmów nic nie daje, „ponieważ nie ma żadnego działającego kodu do zoptymalizowania.”1
Dla tego objawu sesja przełącza się na szablon System Trace, „stworzony, by wizualizować dokładnie, kiedy i dlaczego system operacyjny wstrzymuje aplikację.”1 Sesja omawia model stanów wątku: działający wątek, który natrafia na niedostępny zasób, wchodzi w stan zablokowany, jądro usuwa go z procesora, i dopiero gdy zasób jest gotowy, staje się wykonywalny (runnable) i czeka, aż planista przydzieli mu wolny rdzeń. Te krótkie przebudzenia w celu skoordynowania kolejnego etapu żądania to dokładnie „to, co powoduje owe dwadzieścia procent wykorzystania CPU.”1
W System Trace pas aktywności dla zapisu pokazywał „dużą ilość pustej przestrzeni”, wskazując, że wątek był zablokowany, z fioletowymi interwałami oznaczającymi działające wywołanie systemowe.1 Wybranie jednego interwału podświetliło więcej niż kliknięty segment, wizualizując „jedno ciągłe wywołanie systemowe zapisu, które obejmuje zarówno czas on-core, jak i off-core”: segmenty nieprzezroczyste to wykonanie on-core, segmenty półprzezroczyste to blokowanie off-core.1
Nowy panel Inspector zamienia to w werdykt. Jak stwierdza sesja: „Inspector daje nam dokładne argumenty przekazane do tego wywołania systemowego. Widzimy docelowy deskryptor pliku, adres pamięci bufora i, co najważniejsze, rozmiar.”1 Rozmiar był niezbitym dowodem: aplikacja „próbowała zapisać ponad 1,7 gigabajta danych na wątku głównym.”1 Inspector pokazał też koszt: pojedyncza operacja „trwała ponad 500 milisekund, a niemal 300 z tych milisekund spędzono off-core, czekając na dysk.”1 Synchroniczne wywołanie data.write było wąskim gardłem. Owinięcie kodowania i zapisu w zadanie Swift przeniosło je do współbieżnej puli wątków, a ślad weryfikacyjny potwierdził, że wywołanie systemowe zapisu pojawia się teraz na wątku w tle, a nie na wątku głównym.1 Panel Inspector istnieje po to, by ujawniać synchroniczne zachowanie blokujące, takie jak operacje wejścia/wyjścia na pliku, którego profiler skupiony wyłącznie na CPU nie jest w stanie dostrzec.
Kluczowe wnioski
Dla inżynierów iOS i macOS:
- Otwórz najpierw Time Profiler i odczytaj CPU wątku głównego podczas zawieszenia; wysokie CPU kieruje Cię do
Top Functions, bezczynne CPU kieruje Cię do System Trace i Inspectora.1 - Używaj
Top Functions, gdy flame graph nie wskazuje pojedynczego winowajcy; scala on rozproszony narzut wykonania według wagi własnej, dzięki czemu najkosztowniejsza funkcja wychodzi na jaw.1
Dla zespołów wdrażających poprawki wydajności:
- Weryfikuj każdą zmianę za pomocą
Run Comparisons, przefiltrowaną do tego samego interwałuos_signpost, i odczytuj czerwono-zieloną różnicę, zamiast ufać porównaniu na oko obok siebie.1 - Porównania zapisują się w dokumencie i nawarstwiają wiele uruchomień, więc dowód poprawki podróżuje wraz ze śladem na potrzeby przeglądu.1
Dla pracy nad współbieżnością i responsywnością:
- Sięgnij po instrument
Swift executors, gdy praca rywalizuje o Main Actor; pokazuje on, czy zadanie wykonało się na Main Actor, na globalnym executorze współbieżnym, czy na executorze niestandardowym.1 - Zawsze profiluj kompilację release, ponieważ kompilacje debug dają mylące dane.1
Najczęściej zadawane pytania
Czym jest tryb Top Functions w Instruments 27?
Top Functions to nowy tryb analizy w Time Profiler. Odrzuca hierarchię wywołań i scala każde rozproszone wystąpienie funkcji w jeden blok, uszeregowany według wagi własnej — czasu spędzonego na wykonywaniu instrukcji bezpośrednio wewnątrz tej funkcji. Odpowiada na pytanie, z którym flame graph sobie nie radzi: które konkretne funkcje spaliły łącznie najwięcej cykli, gdy ich koszt jest rozdrobniony pomiędzy wiele gałęzi wywołujących, jak funkcje runtime’u Swift i narzędzia pomocnicze.1
Jak działają Run Comparisons w Instruments?
Run Comparisons, opisane w sesji jako nowość w Instruments, oblicza dokładną różnicę wydajności między śladem bazowym a śladem zoptymalizowanym. Dopasowuje każdą funkcję między dwoma uruchomieniami, oblicza różnicę dla każdego węzła w stosie i sortuje według różnicy wydajności, oznaczając regresje na czerwono, a poprawy na zielono. Aby uzyskać czyste porównanie, filtrujesz oba uruchomienia do tego samego interwału os_signpost, wybierasz ścieżkę i wskazujesz bazę z listy rozwijanej; porównania zapisują się w dokumencie.1
Co pokazuje instrument Swift executors?
Instrument Swift executors w Instruments 27 wizualizuje Main Actor, globalny executor współbieżny oraz wszelkie executory niestandardowe w procesie. Pozwala zobaczyć, na którym executorze wykonało się dane zadanie Swift, dzięki czemu można wychwycić rywalizację o Main Actor. W sesji ujawnił zadania renderThumbnail utknięte na Main Actor, ponieważ kod wywoływany z SwiftUI dziedziczył kontekst Main Actor; przeniesienie ich na executor globalny usunęło zawieszenie.1
Jak znaleźć zawieszenie spowodowane operacjami wejścia/wyjścia na pliku?
Gdy interfejs zamarza, a CPU wątku głównego jest niskie (około 20 procent w sesji), wątek jest zablokowany w oczekiwaniu na zasób systemowy, a nie wykonuje wolny kod. Przełącz się na szablon System Trace i wybierz interwał wywołania systemowego; nowy panel Inspector pokazuje dokładne argumenty syscalla, w tym deskryptor pliku, adres bufora i rozmiar zapisu, a także czas on-core w porównaniu z off-core. W demonstracji ujawnił on synchroniczny zapis 1,7 GB na wątku głównym.1
Przepływ diagnostyczny, którego uczy ta sesja, łączy się ze stroną SwiftUI responsywności w Wydajność i interoperacyjność SwiftUI w iOS 27 oraz z nastawieniem na pomiary w martwym polu wydajności. Ruch współbieżności, który usunął zawieszenie Main Actor, mieści się w szerszym modelu omówionym w Współbieżność Swift 6.2 w praktyce. Pełnym centrum serii jest Apple Ecosystem Series.
Bibliografia
-
Apple, WWDC 2026 session 268, Profile, fix, and verify: Improve app responsiveness with Instruments. Źródło przepływu diagnostycznego (najpierw Time Profiler, odczyt CPU wątku głównego kieruje dochodzeniem), wskazówek dotyczących kompilacji release i szablonu Swift Concurrency, interwału points of interest
os_signpost/OSSignposter, nowego panelu Inspector, modelu próbkowania drzewa wywołań i flame graphu (domyślna częstotliwość jednej milisekundy, waga własna), trybu analizyTop Functionsi odkryciaswift_project_boxed_opaque_existential,Run Comparisons(„New in Instruments”) z czerwono-zielonymi różnicami i kartami porównań zapisywanymi w dokumencie, instrumentuSwift executors(Main Actor, globalny executor współbieżny, executory niestandardowe) oraz rywalizacji o Main Actor zadaniarenderThumbnailrozwiązanej atrybutem@concurrent, a także diagnozy System Trace i Inspectora dla synchronicznego zapisu 1,7 GB (deskryptor pliku, adres bufora, rozmiar zapisu, czas on-core w porównaniu z off-core, ponad 500 ms, z czego niemal 300 ms off-core). ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩