Studia przypadków

Jak inżynierowie Instagram Direct stworzyli architekturę interfejsu opartą na AI za pomocą Jetpack Compose i zmniejszyli koszt tokenów na sesję agenta o 33%

Czas czytania: 11 minut

Ten post na blogu został napisany we współpracy z zespołem Meta

Instagram Direct to jedna z głównych usług na Instagramie, która codziennie obsługuje miliardy wiadomości użytkowników. Przez lata iteracji zespół wycisnął z dotychczasowego systemu widoków Androida każdą możliwą mikrooptymalizację. Jednak utrzymywanie i rozwijanie wysoce zoptymalizowanej starszej platformy generuje znaczne zadłużenie techniczne i koszty inżynieryjne, zwłaszcza że zespoły coraz częściej korzystają z deklaratywnego interfejsu i asystentów kodowania AI. 

Wprowadzenie Jetpack Compose w Instagram Direct to coś więcej niż tylko modernizacja interfejsu. Zespół stworzył bazę kodu interfejsu AI, która jest o 50% mniejsza niż pierwotna implementacja, a jednocześnie pozwala skrócić czas wykonywania zadań przez agenta AI o 35% , zmniejszyć liczbę wymian informacji między inżynierem a agentem o 32% i obniżyć koszt tokenów o 33% . Zespół ściśle współpracował z Google i wdrożył Jetpack Compose, zachowując przy tym wysoki poziom wydajności. Dzięki optymalizacji wydajności Meta i Google ulepszyły Compose nie tylko na potrzeby Instagrama, ale także dla szerszego ekosystemu deweloperów Androida.

Modernizacja bazy kodu na dużą skalę

AI szybko stała się codziennym towarzyszem inżynierów w branży, a jej zastosowanie w przypadku dużego kodu, takiego jak Instagram, już przynosi realne korzyści w zakresie produktywności. Zespół Instagram Direct wyznaczył sobie bardziej ambitny cel. Zamiast po prostu wykorzystać narzędzia AI do istniejącego kodu, zespół przeprojektował bazę kodu i jej architekturę, aby były od początku dostosowane do AI. Dzięki temu wpływ AI był znacznie większy niż w przypadku samego dopasowania.

Zespół Instagram Direct wybrał Jetpack Compose jako kluczowy komponent do tworzenia architektury interfejsu użytkownika opartej na AI. Deklaratywny charakter sprawia, że kod jest zwięzły, przewidywalny i strukturalnie łatwiejszy do analizowania przez modele AI, ma mniej efektów ubocznych, mniej stanu niejawnego i wyraźniejsze granice komponentów.

Migracja do Jetpack Compose wymagała starannego planowania. Codziennie setki milionów osób wysyłają wiadomości na Instagramie, więc migracja musiała być stopniowa i płynna, bez zakłóceń w działaniu aplikacji, podczas gdy zespół przeprojektowywał jej podstawy. Aby zilustrować skalę tego wyzwania: poszczególne komponenty interfejsu mogą być renderowane w ponad 160 różnych wariantach stanu, a sam ekran rozmowy obsługuje ponad 200 różnych typów wiadomości.

Product Design 1.png

Podczas przenoszenia tak dużej bazy kodu do Compose kuszące jest pójście na łatwiznę i osadzenie komponentów interfejsu Compose w istniejącej hierarchii widoków. W ramach stopniowej migracji jest to w porządku. Na dłuższą metę jednak integracja Compose w bazie kodu opartej na widokach stanowi wyzwanie. Narzędzia AI często wybierają najłatwiejszą drogę. Jeśli połączysz deklaratywny i imperatywny kod interfejsu, AI prawdopodobnie nieprawidłowo je zmiesza, co spowoduje drobne błędy, dług techniczny i regresję wydajności. 

Tworzenie architektury interfejsu opartej na AI

W przypadku Instagrama pewien stopień abstrakcji architektury jest nieunikniony i to właśnie dzięki niemu aplikacja jest łatwa w utrzymaniu w miarę rozwoju. Rozważ typowy wzorzec, w którym każdy RecyclerView typ produktu jest modelowany jako element podrzędny niestandardowej klasy bazowej RecyclerViewItem, która udostępnia typowe punkty zaczepienia cyklu życia, takie jak onBind.

Przykład 1

class ChatItem(
  val features: FeatureFlagProvider
) : RecyclerViewItem<ComposeViewHolder, ChatUiState> {

  // Imperative context:
  // AI could often take the path of least resistance and generate a mutable
  // state here, dispatched outside the ChatUiState. This class survives
  // re-bindings and is shared across multiple items, ultimately leading to
  // unexpected, hard-to-reproduce bugs.
  var isPinned: Boolean = false

  override fun onBind(holder: ComposeViewHolder, uiState: ChatUiState) {
      // Imperative context
      val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature")

      // Declarative context
      holder.composeView.setContent {

        // Blending imperative and declarative contexts
        if (isPinnedChatsEnabled) {
          Button(onClick = { isPinned = !isPinned }) {
            Text(if (isPinned) "Unpin" else "Pin")
          }
        }
        
        ...
      }
  }
}


W powyższym fragmencie pojawiają się 2 problemy. Najpierw flaga isPinnedChatsEnabled jest odczytywana w kodzie imperatywnym, a następnie przechwytywana w funkcji lambda Compose, co stanowi subtelne powiązanie między paradygmatami. Po drugie, isPinned jest polem modyfikowalnym w samym elemencie, a nie w ChatUiState, więc przetrwa ponowne powiązanie i ponowne wykorzystanie RecyclerView w wierszach, co powoduje wycieki pamięci i błędy, które trudno odtworzyć.

Nawet po uporządkowaniu kodu przez przypisanie elementowi funkcji @Composable problemy pozostają te same.

Przykład 2

class ChatItem(
  val features: FeatureFlagProvider
) : ComposeRecyclerViewItem<ChatUiState> {

  // Imperative context
  val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature")
  var isPinned: Boolean = false

  // Declarative context
  @Composable
  override fun Content(uiState: ChatUiState) {

 
    // Blending imperative and declarative contexts
    if (isPinnedChatsEnabled) {
      Button(onClick = { isPinned = !isPinned }) {
        Text(if (isPinned) "Unpin" else "Pin")
      }
    }

    ...
  }
}

To celowo uproszczony przykład, ale ilustruje on szerszy problem – im mniej ograniczeń nałożysz na AI, tym niższa będzie jakość generowanego przez nią kodu. Ograniczenia i umiejętności pomagają, ale same w sobie nie wystarczą, ponieważ gdy AI napotka trudności, często je omija, aby się odblokować.

Aby baza kodu była przyjazna dla AI, musi spełniać 2 praktyczne zasady: 

  • Zminimalizuj zależność od kontekstu niestandardowego. Im bardziej specjalistyczna wiedza o bazie kodu jest potrzebna agentowi AI do wprowadzenia prawidłowej zmiany, tym niższa jest jakość jego danych wyjściowych. Im bardziej baza kodu jest zgodna ze znanymi sprawdzonymi metodami, tym lepsze będą wyniki AI.
  • Baza kodu oparta na AI musi mieć własne granice. Łatanie luk w projektowaniu za pomocą umiejętności AI nie jest skalowalne, ponieważ każda umiejętność załadowana do kontekstu kosztuje tokeny i może obniżyć wydajność agenta. Zamiast tego powinna to robić sama architektura. Agenci AI naturalnie wybierają ścieżkę najmniejszego oporu, więc projekt powinien sprawić, że ta ścieżka prowadzi do prawidłowego kodu wysokiej jakości, a podejmowanie złych decyzji projektowych jest trudne i kosztowne.

Element listy może być nadal reprezentowany przez własną abstrakcję, ale w tym przypadku cały kod Compose znajduje się w konstruktorze, więc nie ma dostępu do elementów ani stanu klasy, a jego jedynym źródłem argumentów jest konstruktor. Dzięki temu jest ona równoważna zwykłej funkcji @Composable, a jednocześnie jest zgodna z dotychczasową architekturą.

Przykład 3

class ChatItem(
  val features: FeatureFlagProvider,
  val onPin: (Boolean) -> Unit,
) : ComposeItem<ChatUiState>(

 
   // Compose UI
   content = { uiState: ChatUiState ->
    val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature")

    if (isPinnedChatsEnabled) {
      Button(onClick = { onPin(!uiState.isPinned) }) {
        Text(if (uiState.isPinned) "Unpin" else "Pin")
      }
    }
    
    ...
  },
)

Migracja tak dużej bazy kodu to ogromne przedsięwzięcie. Przez długi czas setki komponentów interfejsu, które stanowią większość interfejsu Direct UI, musiały współistnieć ze starszymi odpowiednikami, a oba te interfejsy były utrzymywane równolegle. Przepływy pracy AI umożliwiły tę równoległą migrację, przyspieszając proces pisania ogromnych ilości kodu. Dzięki temu zespół Direct przeprowadził migrację w rekordowym czasie, nie przeszkadzając przy tym reszcie zespołu, która nadal wprowadzała funkcje ulepszające codzienne korzystanie z usług przez miliony osób.

Wielu inżynierów uruchomiło własne agenty AI w oparciu o wspólną bazę wiedzy zawierającą umiejętności i konwencje wielokrotnego użytku, która została utworzona podczas migracji. Dzięki temu przepływy pracy i sprawdzone metody były zsynchronizowane w całym zespole, a nie odkrywane na nowo przez każdego inżyniera. W przypadku każdej platformy zespół przeprowadził migrację w tych etapach:

  • Pisz cały kod Compose z pomocą AI.
  • Dopracowywaliśmy go, uwzględniając przypadki brzegowe i usuwając luki w wydajności, aż interfejs został udostępniony prawdziwym użytkownikom w ramach testu publicznego.

Podzielenie pracy na 2 etapy na ekran pozwala jednemu inżynierowi szybko przejść przez całą powierzchnię, ustalając architekturę i trudne przypadki brzegowe z góry. Dzięki temu inni mogą skupić się na przygotowaniu interfejsu do wdrożenia bez konieczności podejmowania decyzji technicznych, co przyspiesza ogólną migrację.

Wyniki migracji potwierdziły skuteczność tego podejścia. W przypadku przeniesionych powierzchni Instagram Direct Jetpack Compose pozwolił zespołowi zmniejszyć łączną ilość kodu interfejsu o 50%. Mniejsza ilość kodu do wygenerowania przez AI wiąże się z wyższą jakością danych wyjściowych i niższym kosztem tokenów na zadanie. 

Quote-Pavlo-New.jpg

Wewnętrzna analiza danych bazy kodu Androida dla Instagrama Direct porównała sesje agentów AI pracujących nad interfejsem Compose z tymi samymi zadaniami wykonywanymi za pomocą widoków Androida. Wzrost wydajności był widoczny w 2 obszarach:

  • Na znak kodu: funkcja Compose wymaga o 32% mniej wymian informacji między inżynierem a agentem i o 35% krótszego czasu wykonywania zadań przez agenta(czasu, który upływa od momentu rozpoczęcia pracy agenta nad żądaniem inżyniera do momentu zwrócenia przez niego odpowiedzi).
  • Na sesję agenta: ogólny koszt tokenów spadł o 33%  w przypadku Compose w porównaniu z widokami.

Podajemy zarówno wydajność wyjściową, jak i typową liczbę sesji, ponieważ są to niezależnie od siebie przydatne wyniki. Dane dotyczące wymiany i czasu wykonania przez agenta-inżyniera porównują wykorzystanie zasobów na jednostkę wygenerowanego wyniku, a dane dotyczące tokenów porównują całkowity koszt typowej sesji agenta.

Dane ujawniły też stałą różnicę w sposobie, w jaki te 2 metodologie radzą sobie ze złożonym lub niestabilnym kodem. Meta śledzi to za pomocą oceny ryzyka zmian w kodzie, która ocenia ogólną jakość kodu i prawdopodobieństwo, że zmiana spowoduje incydenty w środowisku produkcyjnym. Analiza mierzyła efektywność wykorzystania zasobów przez agenta za pomocą złożonego wskaźnika obejmującego zużycie tokenów, czas wykonania agenta i interakcje inżyniera z agentem. W miarę jak pliki uzyskują wyższą ocenę ryzyka, sesje agenta AI stają się mniej wydajne pod względem wykorzystania zasobów. 

Gdy skumulowany wynik ryzyka pliku się podwoi, interfejs użytkownika zaimplementowany za pomocą Android Views zmniejsza wydajność zasobów agenta o 30% (na znak). W tych samych okolicznościach zmniejszenie przez interfejs Jetpack Compose wynosi tylko 9%.

Dzięki współpracy Google i Mety zespół Instagram Direct wniósł świeże spojrzenie na wdrożenie Compose – podchodząc do niego z perspektywy gotowości bazy kodu na potrzeby AI, a nie tylko przepisywania interfejsu. Te prace pokazały, że Compose jest doskonałą podstawą do tworzenia baz kodu i architektur opartych na AI, zwłaszcza w przypadku aplikacji takich jak Instagram.

Optymalizacje skuteczności 

Instagram Direct to jedna z najważniejszych funkcji aplikacji, dlatego użytkownicy oczekują, że będzie działać szybko i sprawnie. Wprowadzenie Jetpack Compose oznaczało konieczność znacznego przepisania interfejsu, a głównym celem było zachowanie wysokiej jakości działania aplikacji bez pogorszenia jej wydajności.

Wieloletnie iteracje doprowadziły już starszą implementację opartą na widokach w Instagramie do wyjątkowo wysokiego poziomu wydajności, a zespół musiał osiągnąć ten sam standard, przechodząc na zupełnie nowe ramy interfejsu.

Instagram mierzy setki, a nawet tysiące danych o skuteczności. W przypadku Compose najważniejsze były te 3 czynniki:

  • Czas do interakcji – czas od otwarcia ekranu do momentu, w którym można zacząć z niego korzystać.
  • Czas pełnego wczytania  – czas od otwarcia ekranu do pełnego wczytania wszystkich treści (np. obrazów).
  • Płynność przewijania – jak płynnie przewija się ekran, bez utraty klatek.

Te dane są śledzone w czasie działania w środowisku produkcyjnym, co umożliwia przeprowadzanie testów A/B porównujących przeniesiony interfejs Compose z dotychczasowym interfejsem i ocenianie wpływu tych działań na wydajność.

Często stosowanym podejściem do takiej migracji jest rozpoczęcie od niewielkiej liczby komponentów interfejsu, zebranie danych i zbadanie ich działania. Chociaż te wstępne wyniki są przydatne, dają tylko częściowy obraz sytuacji i dają fałszywe negatywne wyniki w odniesieniu do wdrożenia Compose, ponieważ:

  • Nie jest reprezentatywny – jeden przeniesiony komponent interfejsu może dostarczać przydatnych danych o ogólnej skuteczności na danym ekranie. Jednak różne komponenty zachowują się inaczej z powodów, których nie można uogólnić, więc nie zawsze można na tej podstawie dokonać ekstrapolacji.
  • Koszt współdziałania – mały fragment kodu Compose w dużej bazie kodu View wiąże się z nieprzewidywalnym kosztem połączenia między tymi dwoma systemami. Ten narzut zniekształca pomiary, więc wczesne wyniki na małą skalę nie odzwierciedlają tego, jak wyglądałaby pełna migracja.

W rezultacie małe migracje, choć przydatne, nie zawsze odzwierciedlają pełny wpływ Compose. Im większa część platformy zostanie przeniesiona w całości bez przerw, tym wyraźniejszy i lepszy będzie obraz pod względem wydajności.

Główne ekrany w Instagram Direct są oparte na długich listach różnych typów elementów, które pierwotnie były implementowane za pomocą RecyclerView. Architektura opiera się na niestandardowych abstrakcjach zapewniających skalowalność, ale pozostaje związana z cyklem życia systemu opartego na widokach.

Diagram 1.png

Głównym zadaniem zespołu była stopniowa migracja kilkuset pojedynczych elementów listy do Compose w ramach istniejącej architektury opartej na RecyclerView. Wprowadzano je w środowisku produkcyjnym w małych, niezależnych grupach w ramach testów A/B – wszystko to bez widocznych zmian w sposobie wysyłania wiadomości do użytkowników.

Największą wadą takiego rozwiązania jest znaczna zależność od starszego systemu View za pomocą podstawowej architektury RecyclerView, nawet po pełnej migracji wszystkich elementów listy do Compose. Naturalnym kolejnym krokiem było podjęcie decyzji o zastąpieniu podstawowej architektury opartej na RecyclerView alternatywnym rozwiązaniem natywnym dla Compose, czyli LazyColumn.

Oznacza to, że komponenty interfejsu Compose powinny być odseparowane od platformy, w której są umieszczone, a jednocześnie być kompatybilne z RecyclerView i LazyColumn. Równie ważna jest możliwość przełączania się między nimi w czasie działania programu za pomocą flag funkcji, aby umożliwić testowanie A/B.

Diagram 2.png

Nowe elementy Compose są natywnie zgodne z LazyColumn i można je wstawiać do nieprzerwanego drzewa kompozycji. Utworzyliśmy jednak interfejs API do współpracy, aby można było je wstawiać również do RecyclerView. Dzięki temu mogliśmy wdrożyć konfigurację LazyColumn w ramach testu A/B równolegle z RecyclerView, ponownie wykorzystując te same elementy Compose i dopracowując wydajność bez zakłócania pracy reszty zespołu, która tworzyła i ulepszała funkcje.

Skala, złożoność i wrażliwość Instagrama na nawet najmniejsze regresje stanowiły wyjątkowe wyzwanie dla Jetpack Compose. Rozwiązanie tych problemów wymagało iteracyjnego, praktycznego partnerstwa. Inżynierowie Google i Mety ściśle ze sobą współpracowali, analizując dane, aby określić i zaprojektować nowe funkcje Compose, które spełniałyby lub przekraczałyby testy porównawcze oparte na widokach. W ramach tego partnerstwa do Jetpack Compose dodano m.in. te funkcje: kompozycję z możliwością wstrzymania za pomocą LazyLayoutCacheWindows i śledzenie widoczności.

Kompozycja z możliwością wstrzymania z LazyLayoutCacheWindows

Kompozycja z możliwością wstrzymania (domyślnie włączona w Compose 1.10) umożliwia stopniowe tworzenie kosztownych elementów leniwej listy w ramach klatek, aby zapobiec zacinaniu się. W połączeniu z LazyLayoutCacheWindow (dodanym w Compose 1.9) znacznie poprawia płynność przewijania. W niedawnych testach wewnętrznych w firmie Meta połączenie kompozycji z możliwością wstrzymania z jednym widokiem LazyLayoutCacheWindow zmniejszyło liczbę dużych spadków liczby klatek na minutę (LFD/m) o około 13% w porównaniu z czystym Compose. Samo okno pamięci podręcznej zmniejszyło ją o około 8% w porównaniu z tą samą wartością podstawową. LFDs/m to wewnętrzny wskaźnik używany przez firmę Meta do śledzenia zauważalnych zacięć podczas przewijania.

Quote-Fabio.jpg


Użycie LazyLayoutCacheWindow w aplikacji przygotowuje i zachowuje elementy poza ekranem w pasmie pikseli wokół obszaru widocznego, aby umożliwić szybkie przesuwanie. Aby korzystać z LazyLayoutCacheWindows w aplikacji, możesz użyć najnowszej wersji Compose 1.13.0-alpha03 i skonfigurować ją tak, jak pokazano w przykładzie poniżej:

val cacheWindow = LazyLayoutCacheWindow(ahead = 150.dp, behind = 100.dp)
// OR
val cacheWindow = LazyLayoutCacheWindow(aheadFraction = 0.5f, behindFraction = 0.3f)

LazyColumn(state = state, cacheWindow = cacheWindow) {
    ...
}

Okno pamięci podręcznej można skonfigurować na 2 sposoby. Oba parametry opisują to samo: ile treści poza ekranem ma być zachowanych, ale w różnych jednostkach.

  • Dp: stała długość bezwzględna. ahead = 150.dp zachowuje 150 dp treści poza widoczną krawędzią niezależnie od urządzenia.
  • Liczba zmiennoprzecinkowa: ułamek widocznego obszaru.aheadFraction = 0.5f utrzymuje połowę ekranu w stanie gotowości, więc bezwzględna ilość skaluje się wraz z wysokością ekranu, obsługując różne formaty: więcej na tablecie lub rozłożonym urządzeniu składanym, mniej na kompaktowym telefonie. 

Zespół Instagrama dostosował ułamki zmiennoprzecinkowe okna pamięci podręcznej do struktury treści i rozmiarów elementów w Direct. Idealne wartości różnią się w zależności od konkretnych parametrów interfejsu, więc znalezienie odpowiedniej równowagi wymaga eksperymentowania.

Rejestrowanie wyświetleń za pomocą funkcji onVisibilityChanged


Interfejs onVisibilityChanged (dodany w Compose 1.9.0) API był kolejnym kluczowym wynikiem współpracy technicznej między Google a Metą. Zapewnia on spójny sposób określania, kiedy komponent jest faktycznie widoczny na ekranie, zastępując niestandardowe implementacje używane w przeszłości. W samym Instagramie Direct te sygnały widoczności są używane w setkach plików do obsługi wskaźników jakości produktu, które zależą od tego, czy elementy interfejsu były faktycznie wyświetlane użytkownikom.

Wydajność uruchamiania

Wprowadzenie Jetpack Compose w Instagram Direct przyniosło nieoczekiwane zwiększenie wydajności w innych obszarach aplikacji. Środowisko wykonawcze Jetpack Compose wiąże się z kosztem rozruchu, który ponosisz tylko raz. Ponieważ wiadomości to obszar o dużym natężeniu ruchu, często odwiedzany na początku sesji użytkownika, inne obszary Instagrama, które korzystają z Compose, odnotowały zauważalny wzrost wydajności.

Wydajność uruchamiania interfejsu Compose w Instagramie Direct została zoptymalizowana dzięki zastosowaniu profili bazowych, które wstępnie kompilują często używane ścieżki kodu w momencie instalacji, dzięki czemu interfejs Compose renderuje się szybko od pierwszego uruchomienia.

Wnioski z migracji Instagram Direct do Jetpack Compose 

  • Jetpack Compose zapewnia natychmiastowy zwrot z inwestycji: aby korzystać z zalet Compose, nie musisz używać zaawansowanych przepływów pracy AI. Dzięki zmniejszeniu ilości kodu o ok. 50% jest on łatwiejszy w utrzymaniu i zmniejsza się obszar, w którym mogą występować błędy.
  • Zaprojektowanie architektury opartej na AI przyniosło znaczące korzyści, w tym skrócenie czasu wykonywania zadań przez agenta AI o 35%, zmniejszenie liczby wymian informacji między inżynierem a agentem o 32% i obniżenie kosztu tokenów o 33%.
  • Chociaż istnieje wiele interfejsów API do interoperacyjności i obsługa łączenia widoków i Compose, staraj się migrować większe obszary zamiast poszczególnych małych komponentów. Dzięki temu interfejs użytkownika pozostaje w jednej, nieprzerwanej hierarchii kompozycji i można w nim stosować wszystkie najlepsze optymalizacje wydajnościowe dostępne w Compose.
  • Połącz kompozycję z możliwością wstrzymania z LazyLayoutCacheWindow: połączenie tych 2 elementów daje lepsze wyniki niż same okna pamięci podręcznej. Jeśli jest tylko okno pamięci podręcznej, element o dużej wadze może nadal próbować utworzyć kompozycję w jednym przebiegu, co może spowodować przekroczenie budżetu klatek.
  • Współtwórz funkcję Pisanie Meta współpracuje z zespołem Jetpack Compose, aby wprowadzać w Compose ich opinie i pomysły. Praca nad zestawem narzędzi open source oznacza, że wszyscy korzystamy z poprawek błędów i ulepszeń wydajności wprowadzanych centralnie. Podziel się swoją opinią.

Wprowadzenie Jetpack Compose przyniosło znaczne korzyści w zakresie rozwoju wspomaganego przez AI, a jednocześnie uprościło codzienną pracę inżynierów zajmujących się interfejsem użytkownika w Instagramie. Podejście deklaratywne ogranicza kod standardowy, ułatwia rozumienie stanu i zwiększa ogólną produktywność programistów. Zespół inżynierów Instagrama planuje wprowadzić Compose na więcej platform w aplikacji. Google i Meta będą kontynuować współpracę, aby wprowadzać kolejne ulepszenia dla użytkowników Instagrama i Jetpack Compose.

Jeśli nie masz jeszcze doświadczenia z Compose, teraz z pomocą AI przejście na Jetpack Compose jest łatwiejsze niż kiedykolwiek.

Potwierdzenia. Dziękujemy za współpracę przy ulepszaniu wydajności Compose: Michalowi Zielinskiemu i Matthew Du z firmy Meta oraz Andrejowi Szikowowi i George’owi Mountowi z Google. Dziękujemy też Gary’emu Ye z Mety za pomoc we wdrożeniu Compose w Instagram Direct oraz Gopalowi Juneja z Mety za wsparcie tych działań w zakresie analizy danych.

Autor:
Czytaj dalej