<title>
<![CDATA[ Krótka historia o optymalizacji ]]>
...</title>
<description>
<![CDATA[ Jakiś czas temu na Twitterze zamieściłem zrzut ekranu pokazujący flame-chart z narzędzia Profiler. Pracowałem wtedy nad poprawą wydajności aplikacji, którą rozwijamy w Egnyte. Pewna funkcjonalność… ]]>
...</description>
<link>https://przemuh.dev/blog/tree-performance-improvement-case-study</link>
<guid isPermaLink="false">https://przemuh.dev/blog/tree-performance-improvement-case-study</guid>
<pubDate>Sun, 08 Nov 2020 00:00:00 GMT</pubDate>
<content:encoded><p>Jakiś czas temu na <a href="https://twitter.com/przemuh/status/1319595759935852544" target="_blank" rel="nofollow noopener noreferrer">Twitterze zamieściłem zrzut ekranu</a> pokazujący flame-chart z narzędzia Profiler. Pracowałem wtedy nad poprawą wydajności aplikacji, którą rozwijamy w Egnyte. Pewna funkcjonalność, dla dużej ilości danych, zajmowała strasznie dużo czasu - 3,5 minuty! Przez ten czas w aplikacji pokazywany był "kręciołek", a użytkownik nie wiedział, czy coś się dzieje, czy może coś się zawiesiło. Po kilku dniach pracy z Profilerem udało mi się zaimplementować poprawki, które zredukowały czas potrzebny do obliczeń z 3,5 minuty do 35 sekund. W tym wpisie chciałbym opisać jak do tego doszedłem.</p><h2>Opis funkcjonalności</h2><p>Zacznijmy od opisu funkcjonalności, która krótko mówiąc kulała pod względem wydajności. Jednym z głównych widoków w naszej apce jest tzw. "Sensitive Content". Pokazujemy tu listę folderów z plikami, które zawierają wrażliwe dane. To mogą być numery kart kredytowych, dane medyczne, personalne i nie tylko. Dane te mogą podchodzić pod jedną z kilkunastu wbudowanych polityk np. HIPA, GDPR, ale również dajemy możliwość użytkownikom zdefiniowana własnej polityki np. w oparciu o utworzony wcześniej słownik. Początkowo widok "Sensitive Content" pokazywał tylko płaską listę folderów. W zeszłym roku nasz Product Owner wraz z zespołem UX doszli do wniosku, że praca z płaską listą folderów może być nieefektywna. Zamiast tego dużo lepszym, i w zasadzie bardziej naturalnym sposobem reprezentacji danych, będzie drzewko folderów.</p><p><figure class="gatsby-resp-image-figure">
<span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:944px">
<a class="gatsby-resp-image-link" href="/static/f622f9b398ed8964cce4a32ffc9df3fb/966a0/sc-view.png" style="display:block" target="_blank" rel="noopener">
<span class="gatsby-resp-image-background-image" style="padding-bottom:37.81779661016949%;position:relative;bottom:0;left:0;background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsSAAALEgHS3X78AAABVElEQVQoz01R2U7EMBDr//8fLwiEuLTskftq0rTGk3YXIllJp45jz0yXm8ZVGdy0HZDz9aidQ8YpFqg0o7WOMjcEflufYGyAdRGGcCGN3biAyVqHmBJiTCQTIaKUGb2vqEtH6x25LThbD2Pc4NXaBjfngllQK7wPRMSktIY2li9aPM6EDwELBWVF7s907J0b/9uyjHrnYygVKWUopZFLwfR+0vg8G3xdLD5+NL4vvBRm2NTgckcsK66x4Ymcb3KEa2JFYN2wHnzBxSS8nxSuNmO6PRxSSBu6NIwTUVvD0neHiQ5f2V9HjrhvbMG2YSTYeh7RJZ20ao98ILGXYjvnPCI3isrKh6B30g5Dob/I21oHXymFeaagECx70w83/9d27OLwhYLBezhy772VBH3dhhER3B2q3Z0QJY5MeWOeO4ZDXnzjhJ3dB3Yfijjt64pEh/qI/AvbE2lTetVs3wAAAABJRU5ErkJggg==');background-size:cover;display:block"></span>
<img class="gatsby-resp-image-image" alt="Widok Sensitive Content List" title="Widok Sensitive Content List" src="/static/f622f9b398ed8964cce4a32ffc9df3fb/966a0/sc-view.png" srcSet="/static/f622f9b398ed8964cce4a32ffc9df3fb/3cf3e/sc-view.png 293w,/static/f622f9b398ed8964cce4a32ffc9df3fb/78a22/sc-view.png 585w,/static/f622f9b398ed8964cce4a32ffc9df3fb/966a0/sc-view.png 944w" sizes="(max-width: 944px) 100vw, 944px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/>
</a>
</span>
<figcaption class="gatsby-resp-image-figcaption">Widok Sensitive Content List</figcaption>
</figure></p><h2>Algorytm budowania drzewa</h2><p>Podejść do drzewek było już u nas w projekcie kilka. Głównie opierały się one na własności folderu jakim było unikalne <code>folderId</code>. Niestety, w przypadku "Sensitive Content" nie mogliśmy tego użyć, ponieważ nie wszystkie foldery mogły zawierać wrażliwe dane, a tylko dla takich folderów dostawaliśmy <code>folderId</code>.</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">/Shared/A/B/
W tym wypadku mamy 3 foldery (Shared, A, B), z czego tylko dla B dostajemy folderId</code></pre></div><p>Takich lokacji z SC (Sensitive Content) może być multum. Problem wydajności pojawiał się już przy 250 tysiącach lokacji. A że nie jest to wyjątek utwierdził nas klient, u którego znaleźliśmy prawie <strong>milion</strong> folderów. To właśnie dla 1M elementów listy, czas budowania drzewka wynosił 3,5 minuty. Dlatego też, moje zadanie polegało na tym, że drzewko dla 1M elementów ma się budować w mniej niż 60s.</p><p>Wracając do algorytmu. Prosty - jak budowa cepa - tak by się wydawało :)</p><ol><li>Weź całą ścieżkę i podziel ją na fragmenty wg. separatora np. <code>/</code></li><li>Każdy folder wsadź do dwóch struktur: "drzewiastej" i "płaskiej"</li><li>Jeśli folder nie ma <code>folderId</code> traktuj go jako meta-folder</li></ol><p>Dla uproszczenia pomijam kwestię tego, że wspieramy różne źródła danych i te separatory mogą się mocno różnić :) Co więcej, jak się później okazało, niektóre źródła danych mogą mieć dwa foldery o tej samej nazwie, na tym samym poziomie zagnieżdżenia 😱. I jak je rozróżnić? Pominę też kwestię tego, że wynikowe drzewo mieliśmy przedstawić w formie "sparse-tree". W skrócie - chodzi o to, że jeśli folder zawiera tylko jeden sub-folder, to ścieżka rodzica powinna być zwinięta/scalona.</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">Lista:
/Shared/A/B/C
/Shared/A/B/D
-->
Drzewo:
/Shared/A/B
/C
/D</code></pre></div><h2>Pierwsza, a w zasadzie druga implementacja</h2><p>Jak już wspomniałem wcześniej, nie było to pierwsze drzewo, jakie mieliśmy wyświetlić w aplikacji. W zupełnie innym widoku, też mieliśmy zrobić sparse-tree i nie chcieliśmy mieć kilku różnych implementacji. Dlatego napisaliśmy prosty moduł do budowania i zarządzania drzewkiem. Oparty został o dwa małe komponenty:</p><ul><li>funkcję <code>buildTree</code>, która przyjmowała płaską tablicę węzłów i miejsce (ścieżkę w drzewie) od którego miała te węzły wstawiać</li><li>"plasterek" (slice) z <code>redux-toolkit</code>, który zarządzał strukturą drzewa (rozwijanie, zwijanie węzłów itp.)</li></ul><p>Całe drzewo, a w zasadzie te dwie struktury "drzewiasta" i "płaska", trzymane były w reduxie w następujący sposób:</p><div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
tree<span class="token operator">:</span> <span class="token punctuation">{</span>
path<span class="token operator">:</span> <span class="token string">""</span><span class="token punctuation">,</span> <span class="token comment">// root</span>
children<span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"Shared"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token comment">// path-part or folder name as a key</span>
path<span class="token operator">:</span> <span class="token string">"/Shared"</span><span class="token punctuation">,</span>
children<span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"A"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
path<span class="token operator">:</span> <span class="token string">"/Shared/A"</span><span class="token punctuation">,</span>
children<span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
paths<span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"/Shared"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
meta<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
...nodeProps
<span class="token punctuation">}</span>
<span class="token property">"/Shared/A"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
meta<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
folderId<span class="token operator">:</span> <span class="token string">"some-unique-id"</span><span class="token punctuation">,</span>
...nodeProps
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>Taką strukturę otrzymujemy z pomocniczej funkcji <code>buildTree</code>.</p><p>Dzięki zastosowaniu <a href="https://redux-toolkit.js.org/" target="_blank" rel="nofollow noopener noreferrer">redux-toolkit</a>, a co za tym idzie biblioteki <a href="https://github.com/immerjs/immer" target="_blank" rel="nofollow noopener noreferrer">immer</a>, mogliśmy w bardzo prosty sposób wykonywać operacje na drzewie:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> initialTreeState <span class="token operator">=</span> <span class="token punctuation">{</span>
initialized<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
tree<span class="token operator">:</span> <span class="token punctuation">{</span>
path<span class="token operator">:</span> <span class="token string">""</span><span class="token punctuation">,</span>
children<span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
paths<span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
<span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">createTreeSlice</span> <span class="token operator">=</span> <span class="token parameter">treeName</span> <span class="token operator">=></span>
<span class="token function">createSlice</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
name<span class="token operator">:</span> treeName<span class="token punctuation">,</span>
initialState<span class="token operator">:</span> initialTreeState<span class="token punctuation">,</span>
reducers<span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token function-variable function">insertTree</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> tree<span class="token punctuation">,</span> paths <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> payload <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
paths <span class="token operator">=</span> <span class="token punctuation">{</span>
<span class="token operator">...</span>paths<span class="token punctuation">,</span>
<span class="token operator">...</span>payload<span class="token punctuation">.</span>paths<span class="token punctuation">,</span>
<span class="token punctuation">}</span>
<span class="token keyword">const</span> node <span class="token operator">=</span> <span class="token function">getNodeByPath</span><span class="token punctuation">(</span>payload<span class="token punctuation">.</span>parentPath <span class="token operator">||</span> <span class="token string">""</span><span class="token punctuation">,</span> tree<span class="token punctuation">)</span>
node<span class="token punctuation">.</span>children <span class="token operator">=</span> payload<span class="token punctuation">.</span>tree<span class="token punctuation">.</span>children
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token function-variable function">toggleNode</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> tree <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> payload<span class="token operator">:</span> path <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> node <span class="token operator">=</span> <span class="token function">getNodeByPath</span><span class="token punctuation">(</span>path<span class="token punctuation">,</span> tree<span class="token punctuation">)</span>
node<span class="token punctuation">.</span>expanded <span class="token operator">=</span> <span class="token operator">!</span>node<span class="token punctuation">.</span>expanded
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>Pomocnicza funkcja <code>getNodeByPath</code> służy do wyszukiwania węzła po ścieżce. Potrafi też wyszukać węzeł w sparse-tree.</p><h2>Pierwsze podejścia do optymalizacji i pierwsze błędy</h2><p>No i wszystko pięknie-ładnie, ale przyszedł klient, 1 milion folderów i jebs... Product Owner zakłada Epic w Jirze pt. "Support 1M folders on SC tree view". Szybka burza mózgów i od razu kosz pełen pomysłów:</p><ul><li>a może by tak budować drzewo w locie, jak parsujemy JSONa?</li><li>a może by tak budować drzewo w web-workerze, przynajmniej nie zablokujemy głównego wątku na 3,5 minuty?</li><li>a może by tak, pizgnąć to wszystko i wyjechać w Bieszczady? ⛰ 🐑</li></ul><p><img src="https://media.giphy.com/media/kPtv3UIPrv36cjxqLs/giphy.gif" alt="A może by tak..."/></p><p>Pierwszy błąd - nikt nawet nie odpalił Profilera, żeby zobaczyć co zajmuje tyle czasu. Każdy założył, że obecna implementacja drzewka wymiata i lepiej być nie może. Sam Profiler na pierwszy rzut oka nie jest prostym narzędziem i może to było powodem tego, że rzuciliśmy się wtedy na tego typu pomysły jak budowanie drzewa "w locie" czy przeniesienie tego do web-workera. Warto też podkreślić, że sam Epic w Jirze powstał już jakiś czas temu, a do samej implementacji usiadłem w połowie października.</p><p>Pewnie zastanawiacie się - ale jak to w locie? Przecież jak idzie request to dopiero jak przyjdzie odpowiedź to przeglądarka parsuje JSONa i daje odpowiedź. Tak, ale...w tym przypadku nasi backendowcy też musieli podziałać trochę w kwestii optymalizacji i zamiast zwracać pełne dane to zaczęli tę naszą listę SC zwracać na zasadzie stream'u. Dzięki temu mogliśmy np. wykorzystać bibliotekę <code>oboe.js</code> to parsowania JSONa w locie.</p><p>Oczywiście spróbowałem tego podejścia, no bo w końcu ktoś to wpisał do zadania w Jirze, trzeba było sprawdzić co nie? 😜 Fajnie, JSON parsował się "w locie", ale stream zamiast 10s trwał 30s, a jeszcze nie zacząłem nawet budować drzewa. Dlatego odpuściłem i postanowiłem poszukać gdzie indziej.</p><h2>Web-worker</h2><p>Podejście z web-workerem też przetestowałem. Ale tu napotkałem zupełnie inny problem. Ok - mogę sobie pobrać 1M elementów i zbudować na tej podstawie drzewo, ale muszę je później przesłać z wątku web-workera do wątku głównego. Struktura drzewa jest dość obszerna, razem z danymi, które zapisywaliśmy w tej płaskiej strukturze <code>paths</code>. Jeśli chcemy przesłać tak duże dane z jednego wątku do drugiego, przeglądarka musi te dane zserializować, przesłać, a następnie ponownie sparsować. To też powodowało "zamrożenie" przeglądarki na czas przesyłania z jednego miejsca pamięci do drugiego. Oczywiście są sposoby na przesłanie "bezpośrednie" (bez kopiowania) poprzez tzw. Transferable Objects np. ArrayBuffer, ale uznałem, że na chwilę obecną to może być gra nie warta świeczki i postanowiłem sprawdzić, czy faktycznie ta nasza implementacja drzewka była tak zajebista jak myśleliśmy 😜</p><h2>Profiler podejście pierwsze</h2><p>Usiadłem przed ekranem komputera, odpaliłem dev-toolsy i wcisnąłem przycisk "record" w Profilerze. Po chwili dostałem taki kolorowy wykresik, który przypomniał mi czasy defregmentatora dysków z Windowsa 98 🤣</p><p><figure class="gatsby-resp-image-figure">
<span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px">
<a class="gatsby-resp-image-link" href="/static/08d444b93bebf165ed87c320556ad7b0/d9ed5/flame-graph-violet.png" style="display:block" target="_blank" rel="noopener">
<span class="gatsby-resp-image-background-image" style="padding-bottom:62.5%;position:relative;bottom:0;left:0;background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAACOklEQVQ4y42TeW/aQBDF/ZGrtiSUw6Fqv1YVVYqapkkgKeEU4JgjGBx87YkPXscODUhVpP7x05ud3X07s14blVoNpZMyTkunqFNcr5+h8qmKWtWEaZoUn1BcRqV8gtL7d4V+bpiom1V8+dpA46yGRr0Ks1ZB6cNHGH7oYRMQvgsmQ3AtIWIFGWtwFUGLGRK5QKZWhINMOkjEEimR8KeXmHIsWMB1HRhaawjBCQHORaFZtiMAEiT4f6TSMISkipR6hdPY91YIwhW8pQ1/0odvDY7owytyR9A4sIeIgmcy1Cm1loEVpGS6g+V00N58w8PwHL2L75i2bmC3rmE3fxHXmDZvMGs1C83npve3sK+usB6Nc8MMx8jtDjxMEW1iOP0N2pcPGA8tPPanGC8eMVpN0Jn30G33MLq3MGiO0G8OYd8uEFn6X8MCqlbSBfJRDP/So5MdrAdLMKapiy08yWB3uljcD2HddTG9HsJtrhAtGQz5hqFIdoieNKJbutt2DP5bQ/Jd0YGKgfWFB+88gPfTQ/SDgbVo3Uq9YZgTU4XPCuGYI5qRzvIPlu7nUoQ9ynUkVaURWqQTCeVu32g5p7jLLdgsAnMUGJ0uXg2JeQLxRJXzhA6mTuYcMoipwu3hY+QcDAmZgW+24B498g291/wV/F0TUByRKRkKMmKugqKxkS/KnwuTdJJKD9CmosWQNjAah8khn2ueEy8IRnNkKsncWLvP8IMQYcgKGP0tjMu9EmxPJA654zw/mif9A0zFwtIb7Iv6AAAAAElFTkSuQmCC');background-size:cover;display:block"></span>
<img class="gatsby-resp-image-image" alt="Flame graph" title="Flame graph" src="/static/08d444b93bebf165ed87c320556ad7b0/105d8/flame-graph-violet.png" srcSet="/static/08d444b93bebf165ed87c320556ad7b0/3cf3e/flame-graph-violet.png 293w,/static/08d444b93bebf165ed87c320556ad7b0/78a22/flame-graph-violet.png 585w,/static/08d444b93bebf165ed87c320556ad7b0/105d8/flame-graph-violet.png 1170w,/static/08d444b93bebf165ed87c320556ad7b0/28884/flame-graph-violet.png 1755w,/static/08d444b93bebf165ed87c320556ad7b0/92bee/flame-graph-violet.png 2340w,/static/08d444b93bebf165ed87c320556ad7b0/d9ed5/flame-graph-violet.png 2880w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/>
</a>
</span>
<figcaption class="gatsby-resp-image-figcaption">Flame graph</figcaption>
</figure></p><p>Pierwsze co rzuciło mi się w oczy to ten fioletowy kolor, który zanurkował bardzo, bardzo głęboko. Po bliższym spojrzeniu okazało się, że bardzo dużo tych fioletowych elementów to sprawka <code>immer.js</code>. Szybki rzut oka w dokumentację i boom! strzał w dziesiątkę. Okazuje się, że przy "wkładaniu" dużej ilości danych poprzez <code>immer</code> możemy przyspieszyć ten proces poprzez <code>Object.freeze</code> <a href="https://immerjs.github.io/immer/docs/performance#performance-tips" target="_blank" rel="nofollow noopener noreferrer">tutaj więcej info</a>. Zabieg ten pozwolił mi na zejście z 12,54s na 11,24s dla 54K elementów. Dla 1M skok był oczywiście proporcjonalnie większy. No ale to nadal nie było to...</p><h2>Od profilera, do źródeł</h2><p>Wiedzieliście, że jak klikniecie na dany blok w Profilerze, a następnie przeniesiecie się do pliku, to dostaniecie czasy dla poszczególnych bloków kodu? Nie!? 😎 To teraz już wiecie ;)</p><p><figure class="gatsby-resp-image-figure">
<span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px">
<a class="gatsby-resp-image-link" href="/static/1ba2423b674f0cab158baa993f4c7cfd/0d0e4/source-before.png" style="display:block" target="_blank" rel="noopener">
<span class="gatsby-resp-image-background-image" style="padding-bottom:90.73170731707317%;position:relative;bottom:0;left:0;background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAABYlAAAWJQFJUiTwAAACUElEQVQ4y4VUa5eaMBT0//+39kPrqa26qy4LKpDwSMgbpje40kWtyzEnYJLJ3Llz76IVEnnJwOsGQjokSYbt5oDdrsLbW4PDniHLzhBCw1mLEAL6Pozz59H3Pbz3WKiug2pPMLJEfAJtbniH/a7BdluMF7CyhlIe3nkMw4D/PRF0IaWEqFICPeO6d9CeQC04N7DWwxqLrnOQ0tDcEaN+BP48JkClFDr+AtMmuN7t6PCv5RHr9ZHY8ZHZeNENwO08AlrS5fqMCx+LjRBYbzZYrTLSMicN238AGGahzwCN0RDnn+jYetqgRYdjXiIrK7SyRtM044gHHjGbAWqtwZJvYOmPWVh1XaEoKuz3pLGwDwEeMlRKozkt0RZXhgN62pBmDWW6wOnIiZ2C1u4S7ICZjneAMWuyyaElnwDjjzMxejDPFYwOs8MDnoRsjCHDasqkmRYMvdddBWko3M7AeLJPuIxn2Z5sI8s/ZJ3dxMJqg/2hwPJ3gn1yQHpieKHvnNfwwd/pNwMUZA+tFZxzc23CgNMrozIssXvleE84+bKhyqnHJFkbHjOMgN65O00CMbFUbm3r0IpYKZYyb+j7UjVPASWFq9tsCsO7AEtAog3ExoymjsBKhbv6vQs5ZrlKv0MWq2kTq6g5pGTs/EQGrynTsYYVot7Rt0+TErMcG4ORbGoOhiQoOEdZMZRMwhjqND7qdhm3zO4qxRlB1ukuf9KiI6vwQiF9F6h59CKjqiGtffi6UiJDbxUCeS82zlgRmkTvKcvOXZqm9z0x6z9qeXga8l+P/3vSoyCcKQAAAABJRU5ErkJggg==');background-size:cover;display:block"></span>
<img class="gatsby-resp-image-image" alt="Czasy przed optymalizacją dla buildTree" title="Czasy przed optymalizacją dla buildTree" src="/static/1ba2423b674f0cab158baa993f4c7cfd/105d8/source-before.png" srcSet="/static/1ba2423b674f0cab158baa993f4c7cfd/3cf3e/source-before.png 293w,/static/1ba2423b674f0cab158baa993f4c7cfd/78a22/source-before.png 585w,/static/1ba2423b674f0cab158baa993f4c7cfd/105d8/source-before.png 1170w,/static/1ba2423b674f0cab158baa993f4c7cfd/0d0e4/source-before.png 1230w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/>
</a>
</span>
<figcaption class="gatsby-resp-image-figcaption">Czasy przed optymalizacją dla buildTree</figcaption>
</figure></p><p>To co się rzuca w oczy to 229ms dla zbudowania prostego ciągu znaków 🤯 jakim jest aktualna ścieżka. Okazało się, że to zwykłe niedopatrzenie można było zastąpić krótszym kawałkiem kodu, który koniec końców zajmuje 1.7ms.</p><p><figure class="gatsby-resp-image-figure">
<span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px">
<a class="gatsby-resp-image-link" href="/static/831c9d5d2fd63e39d71b3e58d9daf5e6/7be33/source-after.png" style="display:block" target="_blank" rel="noopener">
<span class="gatsby-resp-image-background-image" style="padding-bottom:100.38510911424905%;position:relative;bottom:0;left:0;background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABYlAAAWJQFJUiTwAAACtElEQVQ4y41UCXKjMBDM/3+XrdojTuIb29ygCx1AbyMn2HFc3hWeMqURrZ6ZnnnSWsO0B1h5hDYeSZJi8bbG66rELlFIU4X1ukR6KuCsRfAeIQT0fX/XnrrOwtQbiGKJcQSc86gKgdWyxvNzjsVii90uRVlpdMZhHHo8WgQ06HQTbQKMqx/gtEfdOGjdEcjC8yJjRkZh4L2Lx0Z+cGtPrdT8uESnigg4YY6uR5tbbLcSmn4hFNqmRVFUmFJU14zKhBn0C0OhOgSnyEDPzuAcDmmOZZLhVOaRVSs6hq2glCPDAT709wGnkEWxQnV6PR+YnmGEIZP9IcPLosHxKBmBh0wNLFPxuW7BPgA7aBalyd6/HFLM3W6bsCAK253k/hAvus3ZdS4joGN4482NPWXRNBZt+7iid0M2ZmK4onTWc8jTLy8r/HxfY01dJnmJQ3HE+pjwP0clKxjPVFnxnaHWBrp8h66WFycfpRWrKVgIi4omBN9LhaqkHqnda3Y3gDwgc/S2mTedt8iqCvuiQNHmaFSDjLJpuhqs7wcIvuXxHDKL0qa/UCY/Lg6abh3eXjL8+bPBZpNhuSxYHAJnilq0AMa7eYwMvVUsRDeDffp1aZBS3JudZi/XrHpLYEFgEYs2ibtnV31hqJRi/9qzLK5CmAaA8z3zFWCtp5gD33u+c5+dZMzZNww3gHHaNAk6ccCllQcETQAZIFoKWkpKSMfedi48lE7Moch+M7yXedN0HqeixiGrkbI4U5Xr2pGRj5FMYU52zW5mOElAlwvI/ALoOF0K9vJ6V2GbNMxvT5Zky36WUpCtiwMiBP8d0HJoOttF+9wcOCjrusN+ryGFY96mEWbIKvxPp5gI1gd/aT3myZDF6WQ4sjpOGDtP6X/28sQwHvxwTNVU0sZBEMIQbRjGhxPmev0FrkwWn/TxJDQAAAAASUVORK5CYII=');background-size:cover;display:block"></span>
<img class="gatsby-resp-image-image" alt="Czasy po optymalizacji dla buildTree" title="Czasy po optymalizacji dla buildTree" src="/static/831c9d5d2fd63e39d71b3e58d9daf5e6/105d8/source-after.png" srcSet="/static/831c9d5d2fd63e39d71b3e58d9daf5e6/3cf3e/source-after.png 293w,/static/831c9d5d2fd63e39d71b3e58d9daf5e6/78a22/source-after.png 585w,/static/831c9d5d2fd63e39d71b3e58d9daf5e6/105d8/source-after.png 1170w,/static/831c9d5d2fd63e39d71b3e58d9daf5e6/7be33/source-after.png 1558w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/>
</a>
</span>
<figcaption class="gatsby-resp-image-figcaption">Czasy po optymalizacji dla buildTree</figcaption>
</figure></p><p>Pomyślicie - (ironicznie) wow... 227ms... "brawo Ty" 👏. Czym jest 227ms? Jeśli spojrzymy na to jako pojedynczą wartość - to fakt...mikro-optymalizacja. Ale pamiętajcie, że celem było obsłużenie 1M elementów, a operacja sklejania ścieżki dotyczyła każdego sub-folderu.</p><h2>Mój spread operator AKA Object.assign taki piękny</h2><p>Jak zrobić płytką kopię obiektu, bądź rozszerzyć inny obiekt - nic prostszego - spread operator <code>...</code>. Jeśli musicie wspierać przeglądarki takie jak IE11, to pewnie korzystacie z babel.js - tak jak my...no i taki spread operator, koniec końców jest tłumaczony na <code>Object.assign</code> (w wielkim uproszczeniu).</p><p><code>Object.assign</code> jest <a href="https://twitter.com/dan_abramov/status/980436488860196864" target="_blank" rel="nofollow noopener noreferrer">stosunkowo wolny</a> i przy większej skali może sprawiać problem. W tym przypadku postawiłem na zwykłe kopiowanie per klucz. Dzięki temu prostemu zabiegowi zbiłem 154ms do 44ms. I znowu, dla pojedynczych elementów to nie ma absolutnie żadnego znaczenia, ale kiedy iterujemy po dużym zbiorze danych takie optymalizacje mogą zdziałać cuda.</p><p><figure class="gatsby-resp-image-figure">
<span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px">
<a class="gatsby-resp-image-link" href="/static/7bed4f76daac0b292144d25c8a68996a/35252/dan.png" style="display:block" target="_blank" rel="noopener">
<span class="gatsby-resp-image-background-image" style="padding-bottom:93.85382059800665%;position:relative;bottom:0;left:0;background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAABYlAAAWJQFJUiTwAAACuklEQVQ4y51U23LaQAz1//9EX/sTfehDp5k2MwkJ2MQ2EDBgx1eM7xc41dnYCcn0kunOyNrVylrp6OxqztLC9MdX7F0Pi+UKy9UKZVXhfD6jP51w+qDQv2k7aPrNNb58/oQkKxBGMYIwRtf3+J/BwFpelNh7T1ivN9hsHGy3O+x2e9FbZXOcLWzbhuu6UoUrPuLnOFg9Pirf9XoNPwhUQGapeZ6HyWQCXdcxm82UXi6XsCwLhmFgOp2KnuPu7h6mKbb5XPx03Iv99naCB9NE0zTPGTKg++Tj+maCqW7AeLCgmzbm1gKTqRzAtfEASzLkj7a9gCkH2YuFCsr1TBKwLBur1SOOxwzaNm3wc5djHpSY+hWMoIIZVrBEzKiGn7c4CaYj+P0wH3XXdUrGtVa2PZL6jLwDDlWPtutl8zTI+3n/zt4rJnB+Op2V1jg5RD6i4AlVWaJt25dT/yVt+zov5d+6bqARzOvvV7j6doVDekSapiiKAnleoKrqD1MmyzJUdQ2NnCvLSoKlCtRIuBgnCZLkoORwoKRIqOXA+FghFc7SHsWx0v3A20aq04gBjfu9C0/4mOe54NgpKlAIAWWcN4IxS73cJ//ko+ZaO3RoHGEYIhiIykEnHkSMuHeUShKpgBVdjvMYkCVXcnd931c/UFgym0WnWrIIo0gBH4mmL/dZyRjoTYYKQwHf9Xx4QvIgiGTdCMCtEnU1XYGiqGQ/ECyPyieKkz8EFEzYbt5HPwjVA0FnNoANyrJc2ZlhEEZqzQypLwM+vzZsipCRFNk4O3kI5LLLA7GTBq03W8FKKFTKbfFDybZRL1GWC5ZRglQY8X6wWRo/zLBu2mc9zJtB3tjry3nz4vPiS2Kzy4XwkDgym1HTlg/2egz8N5HD1APLUpI0QyE/UmdS/pHElZJiaUA53JZLrH4rg88vUpezjjrk5acAAAAASUVORK5CYII=');background-size:cover;display:block"></span>
<img class="gatsby-resp-image-image" alt="Dan Abramov o Object.assign" title="Dan Abramov o Object.assign" src="/static/7bed4f76daac0b292144d25c8a68996a/105d8/dan.png" srcSet="/static/7bed4f76daac0b292144d25c8a68996a/3cf3e/dan.png 293w,/static/7bed4f76daac0b292144d25c8a68996a/78a22/dan.png 585w,/static/7bed4f76daac0b292144d25c8a68996a/105d8/dan.png 1170w,/static/7bed4f76daac0b292144d25c8a68996a/35252/dan.png 1204w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/>
</a>
</span>
<figcaption class="gatsby-resp-image-figcaption">Dan Abramov o Object.assign</figcaption>
</figure></p><h2>Agregacja wartości</h2><p>Po "podkręceniu" immera i wyrzuceniu kliku Object.assign, bądź przepisaniu ich na prostą pętlę, skończyły mi się pomysły na "proste" optymalizacje. Trzeba było podkręcić sam sposób budowania drzewa.</p><p>Poprzednia implementacja, dzieliła sobie SC lokacje wg. źródła i dla każdego z nich budowała pod-drzewo. Dla każdego pod-drzewa liczone były zagregowane wartości (np. jeśli folder sam w sobie zawierał 10 SC, ale miał dodatkowo 50 sub-folderów, chcieliśmy pokazać zsumowane wartości). Dla każdego takiego pod-drzewa, wykonywane były sumowania, a następnie węzeł źródła był aktualizowany wg. zsumowanych wartości dla wszystkich folderów.</p><p>Każda taka operacja wrzucała coś do stanu w redux'ie. Pomyślałem - a na co to komu? A komu to potrzebne? Przecież nie pokazujemy drzewa dopóki wszystko nie jest policzone i zaktualizowane. Dlatego, zmieniłem kod tak, aby najpierw zbudować w pamięci całe drzewo wraz z policzonymi zagregowanymi wartościami, a następnie za pomocą jednej operacji, wsadzić zbudowane drzewo do reduxa.</p><p>Co więcej - w wymaganiach drzewka, było napisane, że pewne węzły miały być domyślnie rozwinięte np. pierwszy poziom + potencjalnie wcześniej zaznaczony element na liście (z listy do drzewka można przejść za pomocą prostego przycisku). Wcześniej operacje rozwijania były wyzwalane za pomocą akcji <code>toggleNode</code>. To też zmieniłem - zamiast odpalać reduxową akcję, po prostu zmieniam wartość <code>expanded</code> na <code>true</code> bezpośrednio w obiekcie węzła.</p><p>Można zapytać - co Ci to dało drogi Panie?</p><p>Dla 54K elementów zjechałem z czasu 12,25s na 2,4s 🚀</p><p>Łogień w szopie :) Product Owner cały w skowronkach.</p><p><img src="https://media.giphy.com/media/ciwIz38tlvDFH08Yuu/giphy.gif" alt="Wow"/></p><h2>Testy dla 1M</h2><p>Poprosiłem backendowców, żeby przygotowali mi środowisko do testów dla 1M elementów. Chciałem sprawdzić czy moje optymalizacje dla 54K znajdą uzasadnienie. No i uśmiech nie uciekł z mej twarzy :)</p><p>Przed optymalizacją czas budowania drzewa wynosił ~3,5 minuty. Po zaaplikowaniu wyżej wymienionych zmian udało się zjechać do 59s.</p><p>Mniej więcej ~70% oszczędności. W sumie można by powiedzieć - job done - miało się budować w mniej niż 60s... 59 to mniej niż 60 😅 Jest git...</p><p>Trochę byłem już zmęczony tym grzebaniem w de-facto w nie swoim kodzie...ale kumpel z zespołu słusznie stwierdził:</p><blockquote><p>No fajnie, fajnie, ale dla mnie nadal to jest wolno.</p></blockquote><p>Dodał też potem, że nic mi nie ujmuje i wg. niego wykonałem kawał dobrej roboty...ale trudno było się z nim nie zgodzić. Od momentu kliknięcia w element nawigacji, do czasu wyświetlenia widoku, użytkownik musiał poczekać łącznie 90s:</p><ul><li>25s pobieranie danych (streaming)</li><li>6s przeglądarka parsuje JSONa</li><li>59s budowanie drzewa</li></ul><p>Jako użytkownik, gdybym przez 90s widział tylko spinner ("kręciołek") to by mnie szlag trafił :) Nie chcę myśleć, co czuli nasi użytkownicy jak musieli czekać 3,5 minuty...pewnie żaden nie wytrzymał 😅</p><p>...no...ale wracając...kumpel mówi: "wolno"...no to ja od razu: "co!? wolno!? ja Ci pokażę!" 🤣</p><h2>Generowanie ID</h2><p>Znowu zanurkowałem w Profiler. Dla meta-folderów generowny był <code>folderId</code>. Było to spowodowane tym, że inne miejsce w kodzie tego <code>id</code> potrzebowało (mniejsza o to). Koniec końców to generowane id nic nie znaczyło (nigdy nie było wysyłane do backendu). Ktoś jednak wymyślił, że to meta-folder-id ma być haszem ze ścieżki...</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">createUniqueIdForLocation</span> <span class="token operator">=</span> <span class="token parameter">path</span> <span class="token operator">=></span> <span class="token function">btoa</span><span class="token punctuation">(</span><span class="token function">encodeURIComponent</span><span class="token punctuation">(</span>path<span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre></div><p>Funkcja <code>btoa</code> koduje ciąg znaków jako base64. Sama w sobie trwa średnio 0,25ms...czyli ułamek milisekund. Ale gdy mocniej się zastanowimy - a na co komu ten hasz? a na co komu to potrzebne?</p><p><img src="https://media.giphy.com/media/s239QJIh56sRW/giphy.gif" alt="Ale po co?"/></p><p>No właśnie! Jeśli meta-folder-id to tylko base64 ze ścieżki, która de facto zawierała też w sobie <code>id</code> źródła, więc była unikatowa względem całej listy, to po co to w ogóle ten cały hash?</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">- id: createUniqueIdForLocation(path),
+ id: path,
name: getLocationName(path),</code></pre></div><p>Ten jeden diff sprawił, że dla 1M elementów zszedłem z 59s na 35s, co dało ~40% zysku 🤯</p><p>Czyli teraz klient nie czekał już 90s a 66s - łącznie z pobieraniem i parsowaniem danych! Biorąc pod uwagę, że wymagania mówiły o budowaniu drzewka w czasie mniejszym niż 60s, to chyba Product Owner oraz klienci powinni być zadowoleni 😅</p><h2>Dalsze kroki</h2><p>Oczywiście nie spoczywamy na laurach. Blokowanie użytkownika na 60s nadal jest kiepskim pomysłem, dlatego dalej myślimy o ulepszeniu implementacji. Być może wyrzucimy to w końcu do web-workera. Kto wie? Może uda mi się dzięki temu zebrać materiał na następnego posta 😉.</p><h2>Podsumowanie</h2><p>Lekcja pierwsza - zamiast gdybać i sypać pomysłami z kapelusza o web-workerach, lepiej odpalić Profiler.</p><p>Lekcja druga - jeśli operujesz w dużej skali, iterujesz po dużym zbiorze danych, to optymalizacje na poziomie <code>ms</code> dla jednej iteracji potrafią czynić cuda 🚀</p><p>Lekcja trzecia - do reduxa wrzucaj dopiero wtedy, kiedy jesteś gotów 💪</p><p>Lekcja czwarta - jeśli nie ma potrzeby, to nie komplikuj sytuacji 😉 (patrz ID & btoa).</p><p>Mam nadzieję, że dzięki tej historii sięgniecie wcześniej do Profilera i uda się Wam poprawić wydajność nie jednej aplikacji.</p></content:encoded>