PowerBIDesktop_PerformanceAnalyzer000

Power BI – Performance Analyzer

Wydajność rozwiązań analitycznych w szczególności opartych o Power BI bardzo często jest czynnikiem decydującym o akceptacji raportu bądź jej braku. Tuning wydajnościowy jest o tyle trudny, że mierzenie wydajności poszczególnych składników jest zadaniem nietrywialnym i często wiąże się z dosyć złożonym procesem, którego znajomość nie jest powszechna. Na szczęście od jakiegoś czasu mamy do dyspozycji bardzo fajne narzędzie, którym jest wbudowany w Power BI Desktop Performance Analyzer, któremu chciałem poświęcić niniejszy artykuł.

Performance Analyzer jest można powiedzieć Profilerem znanym z SQL Server przeniesionym na grunt Power BI. Oczywiście ilość funkcji jest znacznie uboższa niż w przytoczonym narzędziu ale i tak daje nam dosyć spore możliwości. W założeniu ma on rejestrować działalność na raporcie i mierzyć czas potrzebny na wykonanie poszczególnych kroków. Brzmi zachęcająco prawda? Myślę, że jak opis ten zainteresował Was równie mocno jak mnie gdy się o tej funkcjonalności dowiedziałem. Myślę, że najlepszym sposobem aby przetestować funkcjonalność będzie przedstawienie jej na konkretnych przykładach. Na wstępie załadujmy dane testowe do Power BI Desktop, które będą pochodziły oczywiście z AdventureWorks (które zostało powiększone skryptem, który możecie znaleźć tutaj). Do modelu załadowałem następujące tabele (powiększone do wskazanych rozmiarów):

  • Sales Customer z 19 820 wierszy,
  • Sales Order Detail z nazwą Sales Order Detail Enlarged powiększony do 13 951 455 wierszy,
  • Sales Order Header z nazwą Sales Order Header Enlarged powięszony do 3 618 475 wierszy,
  • Sales Person z 17 wierszami.

Załadowany model w postaci diagramu wygląda w następujący sposób:

Przygotowałem również dwie bardzo proste miary, które będziemy śledzić – posiadają one następujące definicje:

Średnia cena sprzedaży:

Average Price = AVERAGE('Sales SalesOrderDetailEnlarged'[UnitPrice])

Liczba sprzedanych sztuk określonego towaru:

Sum of Order Quantity = SUM('Sales SalesOrderDetailEnlarged'[OrderQty])

Z wykorzystaniem tych właśnie miar oraz innych kolumn dostępnych w załadowanych przeze mnie tabelach stworzyłem następującą stronę raportową:

Mamy zatem 5 różnych wizualizacji:

  • dwie karty pokazujące każdą ze wspomnianych wcześniej miar,
  • wykres powierzchniowy (Area chart) pokazujący średnią cenę na przestrzeni lat,
  • Matrix pokazujący liczbę sprzedanych sztuk w podziale na numer konta klienta oraz rok sprzedaży,
  • Wykres typu TreeMap pokazujący liczbę sprzedanych sztuk towaru w zależności od określonego terytorium.

Mamy więc zestaw testowy, spróbujmy włączyć Performance Analyzer. Możemy to zrobić wybierając opcję dostępną na wstążce, a mianowicie chodzi o checkbox Performance Analyzer dostępny na zakładce View:

Po włączeniu tej opcji naszym oczom powinna ukazać się dedykowana zakładka po której będziemy się poruszać przez kilka następnych minut:

Pierwszym krokiem w użyciu opisywanego narzędzia jest kliknięcie przycisku Start recording który spowoduje uruchomienie śledzenia naszych Visuali i modelu do którego są one podpięte. Aby zebrać jakieś dane to po uruchomieniu rejestrowania musimy wykonać jakieś akcje na raporcie lub po prostu kliknąć Refresh visuals (co w rzeczywistości uczyniłem) co jak sama nazwa wskazuje odświeży wszystkie nasze wizualizacje na bieżącej stronie raportu:

Już w tym miejscu możemy spostrzec, która z naszych wizualizacji potrzebowała najwięcej czasu na wygenerowanie rezultatu. Wstępnie analizując otrzymane liczby bardzo szybko zidentyfikował, że był nią matrix. Spostrzeżenie to potwierdza naszą intuicję ponieważ visual ten wyświetla najbardziej szczegółowe dane co przy tak prostych miarach będzie się wiązało z najdłuższym czasem generowania rezultatu.

Warto dodać, że używając Performance Analyzera możemy mieć problem z identyfikacją poszczególnych obiektów – szczególnie gdy jest ich naprawdę dużo. W przedstawionym przeze mnie przykładzie mamy dwie karty i każda jest widoczna jako “Card” i żeby dociec, która jest która możemy po prostu zaznaczyć obiekt który nas interesuje i zostanie on dla nas zaznaczony:

Wyświetlana nazwa to nic innego jak tekst wpisany we właściwość Title obiektu – jeśli nie uzupełniliśmy żadnego tekstu to wyświetlana jest nazwa typu obiektu. Nazwa ta jest widoczna nie tylko w Performance Analyzer, ale również np. w oknie Selection Pane. Jeśli chcemy używać właściwości Title tylko jako identyfikatora we wspomnianych oknach (co wcale nie jest najgorszym pomysłem) to możemy dostosować nazwę do naszych potrzeb klikając dwukrotnie w oknie Selection Pane w określony obiekt i zmieniając nazwę co znacząco ułatwia sprawę:

Dobrze wiemy zatem jak to wygląda jeśli chodzi o podstawy pracy z Performance Analyzerem. Warto uzupełnić tą wiedzę o to, że każde nasze działanie takie jak selekcja wartości na wizualizacji czy też wybór filtra powoduje, że (zgodnie z logiką asocjacyjną) nie tylko wybrana wizualizacja jest odświeżana ale również wszystkie inne dostępne na bieżącej stronie raportu. Tak więc jak włączymy rejestrowanie to po jakimś czasie możemy zobaczyć pokaźnych rozmiarów “log”:

Jednakże w każdej sytuacji możemy log wyczyścić używając dostępnego w oknie przycisku Clear:

Jeśli chcemy zebrać logi dla wszystkich wizualizacji to mamy widoczną na powyższym zrzucie ekranowym opcję Refresh visuals ale oprócz tego możemy odświeżyć pojedynczą wizualizację poprzez dostępny przycisk w nagłówku każdej wizualizacji (pamiętając przy tym, że opcja ta jest dostępna wtedy gdy włączone jest okno Performance Analyzera):

No dobrze zajrzyjmy nieco w szczegóły zarejestrowanych przez nas informacji – po rozwinięciu konkretnej wizualizacji naszym oczom powinien ukazać się widok podobny do poniższego:

Zarejestrowany czas został w tym miejscu rozbity na kilka kategorii, a mianowicie:

  • DAX Query – czas potrzebny na wykonanie określonego zapytania w DAX potrzebnego do otrzymania rezultatu
  • Visual Display – czas potrzebny na wizualizację danych otrzymanych z zapytania DAX
  • Other – inny czas potrzebny na wygenerowanie wizualizacji nie związany z dwom powyższymi (według sqlbi jest to czas jaki jest spędzany na oczekiwaniu aż inna wizualizacja skończy się ładować itp i nie powinien być czymś na czym powinniśmy się skupiać w naszej analizie.

Ostatnią opcją jaką mamy tutaj do dyspozycji jest Copy query czyli możliwość zapisania w naszym schowku zapytania jakie jest używane do wygenerowania określonej wizualizacji. Muszę szczerze przyznać, że jest to moja ulubiona opcja tego narzędzia, która znacznie ułatwia pracę. Dla przykładu podam poniżej zapytanie wygenerowane przez ukazanego wyżej Matrixa:

// DAX Query
DEFINE
    VAR __DS0Core =
        SUMMARIZECOLUMNS (
            ROLLUPADDISSUBTOTAL ( 'Sales Customer'[AccountNumber], "IsGrandTotalRowTotal" ),
            ROLLUPADDISSUBTOTAL ( 'LocalDateTable_2f5930c2-7f24-4f6d-9764-1c0bf1384981'[Year],
            "IsGrandTotalColumnTotal" ),
            "Sum_of_Order_Quantity", 'Measure'[Sum of Order Quantity]
        )
    VAR __DS0Primary =
        TOPN (
            102,
            SUMMARIZE ( __DS0Core, 'Sales Customer'[AccountNumber], [IsGrandTotalRowTotal] ),
            [IsGrandTotalRowTotal], 0,
            'Sales Customer'[AccountNumber], 1
        )
    VAR __DS0Secondary =
        TOPN (
            102,
            SUMMARIZE (
                __DS0Core,
                'LocalDateTable_2f5930c2-7f24-4f6d-9764-1c0bf1384981'[Year],
                [IsGrandTotalColumnTotal]
            ),
            [IsGrandTotalColumnTotal], 1,
            'LocalDateTable_2f5930c2-7f24-4f6d-9764-1c0bf1384981'[Year], 1
        )
EVALUATE
__DS0Secondary
ORDER BY
    [IsGrandTotalColumnTotal],
    'LocalDateTable_2f5930c2-7f24-4f6d-9764-1c0bf1384981'[Year]
EVALUATE
NATURALLEFTOUTERJOIN (
    __DS0Primary,
    SUBSTITUTEWITHINDEX (
        __DS0Core,
        "ColumnIndex", __DS0Secondary,
        [IsGrandTotalColumnTotal], ASC,
        'LocalDateTable_2f5930c2-7f24-4f6d-9764-1c0bf1384981'[Year], ASC
    )
)
ORDER BY
    [IsGrandTotalRowTotal] DESC,
    'Sales Customer'[AccountNumber],
    [ColumnIndex]

Zapytanie dosyć długie ale dzięki opisywanemu narzędziu nie musimy odpalać Profilera czy chociażby DAX Studio aby je wyłapać. Jednym słowem super!

Performance Analyzer ma jeszcze jedną dosyć ciekawą opcję, a mianowicie Export:

Opcja ta powoduje wyeksportowanie zarejestrowanych przez nas danych do pliku JSON. Oczywiście plik ten możemy później sobie obrobić i przedstawić w ciekawszej formie wizualizując np. czas trwania raportu przed i po naszych zmianach. Dla przykładu ja wyeksportowany plik zaimportowałem do Power BI następującym skryptem M:

let
    Source = Json.Document(File.Contents("C:\Users\adria\Desktop\Power BI\Samples\PowerBIPerformanceData.json")),
    events = Source[events],
    #"Converted to Table" = Table.FromList(events, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
    #"Expanded Column1" = Table.ExpandRecordColumn(#"Converted to Table", "Column1", {"name", "component", "start", "id", "metrics", "end"}, {"name", "component", "start", "id", "metrics", "end"}),
    #"Expanded metrics" = Table.ExpandRecordColumn(#"Expanded Column1", "metrics", {"sourceLabel", "status", "visualTitle"}, {"sourceLabel", "status", "visualTitle"}),
    #"Removed Top Rows" = Table.Skip(#"Expanded metrics",1),
    #"Removed Columns" = Table.RemoveColumns(#"Removed Top Rows",{"component", "id", "sourceLabel", "status"}),
    #"Filled Down" = Table.FillDown(#"Removed Columns",{"visualTitle"}),
    #"Changed Type" = Table.TransformColumnTypes(#"Filled Down",{{"start", type datetime}, {"end", type datetime}}),
    #"Added Custom" = Table.AddColumn(#"Changed Type", "Duration[MS]", each Duration.Seconds([end] - [start])*1000),
    #"Removed Columns1" = Table.RemoveColumns(#"Added Custom",{"start", "end"}),
    #"Renamed Columns" = Table.RenameColumns(#"Removed Columns1",{{"visualTitle", "VisualName"}, {"name", "Event"}})
in
    #"Renamed Columns"

Dzięki czemu byłem w stanie zbudować proste zestawienie dokumentujące wydajność raportu:

 

O ile możliwość podglądu czasu i zapytań jest czymś naprawdę super to miałem jedno podejrzenie, które okazało się prawdą. Jakie? Pokażmy to sobie na osobnym przykładzie. Stworzyłem dwie miary, które nie mają żadnego znaczenia biznesowego ale dają nam pewność, że nie będą zbyt szybkie:

Filtered SUMX = SUMX(FILTER('Sales SalesOrderDetailEnlarged','Sales SalesOrderDetailEnlarged'[rowguid]<>"abc"),[OrderQty]*'Sales SalesOrderDetailEnlarged'[UnitPrice])
Distinctcount Order Detail Rowguid running total in rowguid = 
CALCULATE(
	[Distinctcount Order Detail Rowguid],
	FILTER(
		ALLSELECTED('Sales Customer'[rowguid]),
		ISONORAFTER('Sales Customer'[rowguid], MAX('Sales Customer'[rowguid]), DESC)
	)
)

Wykorzystana wizualizacja to wykres liniowo-kolumnowy gdzie użyłem następujących atrybutów:

  • Miara distincount determinująca wysokość kolumny
  • Miara Filtered SUMX odpowiadająca za linię
  • Customer Account na osi X

Graficznie zestawienie to wygląda następująco:

Zarejestrowane dane wydajnościowe przedstawiają się następująco:

Otrzymane wyniki nie są jakoś zadziwiająco wolne ponieważ sam model nie jest jakiś bardzo wymagający ani pod kątem struktury ani ilości wierszy. Wykonanie zapytania w 2256ms jest jednak trochę wyższą wartością niż wcześniej testowane elementy. I tutaj pojawi się dosyć ciekawe spostrzeżenie – wystarczy kliknąć Refresh Visuals:

Zapytanie zostało wykonane w 20ms – czy to oznacza, że w jakiś magiczny sposób Power BI sam dokonał tuningu? Oczywiście, że nie! Po prostu zapytanie nie było kręcone od nowa, a wykorzystany został cache. Wniosek z tego taki, że Refresh visuals nie powoduje wyczyszczenia cache co w moim mniemaniu powinno mieć miejsce. Jeżeli zatem chcemy tego dokonać to możemy to zrobić na kilka sposobów np. poprzez wbudowaną funkcjonalność DAX Studio:

Możemy też sami wykonać odpowiednią komendę w dowolny sposób podłączając się do Power BI Desktop (o tym jak znaleźć odpowiedni port na którym działa PBI Desktop przy pomocy Powershell pisałem tutaj).

Po wyczyszczeniu cache wyniki po odświeżeniu wróciły do pożądanego stanu:

Jaka zatem brzmi zasada, którą powinniśmy się stosować przy użyciu Performance Analyzera? Czyśćmy cache za każdym razem gdy mierzymy wydajność zapytania w DAX.

Ostatnią rzeczą na jaką chciałbym zwrócić Waszą uwagę są zsynchronizowane slicery pomiędzy stronami raportu. Stworzyłem slicery na dwóch stronach raportu i je ze sobą zsynchronizowałem:

Przy zaznaczeniu jednej wartości na slicerze zauważymy następującą aktywność:

To co zostało zaznaczone przeze mnie na powyższym zrzucie ekranowym jest według moich podejrzeń Slicerem ze strony 2 – wskazuje na to chociażby czas w sekcji Other odpowiedzialnej właśnie m.in za synchronizację. Jak przejdziemy na stronę 2 to tam po zmianie selekcji zobaczymy podobne efekty:

Niestety podobna sytuacja ma miejsce w momencie gdy dodałem na obie strony wizualizacje Matrix (z całkiem różnymi danymi) i odpowiednio je nazwałem. Poniżej rezultat moich działań:

Warto mieć to specyficzne zachowanie na uwadze szczególnie gdy mamy wielostronicowe raporty.

To by było na tyle jeśli chodzi o Performance Analyzera. Jest to jak najbardziej użyteczne narzędzie i krok w dobrym kierunku którego należy jednak używać z rozwagą ze świadomością jego dobrych i złych stron. Mi osobiście najbardziej podoba się możliwość łatwego podejrzenia zapytań wysyłanych do modelu bez konieczności odpalania dodatkowych narzędzi. Możemy się również spodziewać dodatkowych funkcjonalności i ulepszeń.

Leave a Reply