Memory Visualizer wymaga udostępnienia języka zapytań odnośnie obiektów i struktur w pamięci. Jak pisałem w części Cypher, co to jest?! , język ten nazywam MQL - Memory Query Language. W istocie jest to jednak po prostu Cypher, który rozszerzę o elementy kontrolujące sposób wyświetlania.
Aby sobie przypomnieć, co chcę osiągnąć, wróćmy do przykładowego rysunku z opisu projektu:
Projektując język MQL spróbuję wymyśleć zapytania, którymi dałoby się je narysować. To mi pozwoli określić w jak dużym stopniu przyda się sam Cypher, a na ile będę potrzebował dokonać w nim jakiś rozszerzeń. Liczę, że uda się wszystko bez modyfikacji, co pozwoliło by mi użyć z pudełka działające rozwiązania oparte o Cypher i neo4j.
Pierwszą zasadą rysowania (a nie zapytań jako takich), którą muszę przyjąć jest super ważne założenie - jeśli polecenie zawiera kilka zapytań, wyniki tych zapytań są rysowane w "nakładający się" sposób pod kątem przestrzeni adresowej. Nazwijmy to semantyką nakładania:) O co chodzi, wyjaśni się za chwil parę.
Przejdźmy zatem do rozważań. Uwaga: przyjmuję w przykładach pewne uproszczenia, by nie pogubić się w złożoności problemu. M.in. zakładam na razie analizę procesu w trybie Workstation, gdzie jest tylko jeden Managed Heap.
Pierwszy przykład. Zwrócić możemy same segmenty - dostaniemy niczym nie oznaczone klocki:
MATCH (seg:Segment)
RETURN seg
|
|
Tutaj pojawia się potrzeba definiowania jak mają być wyświetlane poszczególne zwrócone wyniki - czegoś czego Cypher nie ma. Muszę to zrobić tak, by dało się to wyraźnie oddzielić od samych zapytań, które prześlę do silnika neo4j. Sekcję zaś odpowiedzialną za sposób wyświetlania przekażę ViewModelowi. Na chwilę obecną wymyśliłem użycie operatora AS (coś jak w SQL), który chyba dość dobrze sam się tłumaczy:
MATCH (seg:Segment)
RETURN seg AS BOX(Label = seg.Address, LabelPosition = OuterLeft)
|
|
W podobny sposób osobno wyświetlimy generacje:
MATCH (gen:Generation)
RETURN gen AS BOX(Label = seg.Name, LabelPosition = InnerCenter, Background = Grey)
|
|
Ale jak w ramach jednego zapytania zwrócimy razem segmenty i generacje, silnik rysowania musi być mądry i nałożyć jedno na drugie zgodnie z adresami - to jest właśnie wspomniana powyżej semantyka nakładania:
MATCH (seg:Segment) RETURN seg
MATCH (gen:Generation) RETURN gen AS BOX(Label = seg.Name, LabelPosition = InnerCenter, Background = hash(seg.Generation))
|
|
I tak doszliśmy do pierwszego z przykładowych rysunków.
Idąc dalej, w Cypher bardzo fajnie zdefiniujemy zapytanie pokazujące graf obiektów mających referencje do obiektu pod danym adresem:
MATCH (parent:Object) -[ref]-> (obj:Object)
WHERE obj.@Address = 0xDDE51018
RETURN parent, ref, obj
|
|
I za pomocą operatora AS możemy nałożyć na to dodatkowo sposób rysowania:
MATCH (parent:Object) -[ref]-> (obj:Object)
WHERE obj.@Address = 0xDDE51018
RETURN parent AS CIRCLE(Radius = parent.Size),
ref,
obj AS CIRCLE(Label = obj.Address + "\r\n" + obj.Type, Radius = obj.Size)
|
|
Chcemy sprawdzić jakie Rooty trzymają referencje do obiektu? Nic prostszego:
MATCH p = (root:Object) -[*]-> (obj:Object)
WHERE obj.@Address = 0xDDE51018
RETURN root AS DOT(Label = root.Type),
relationships(p),
obj AS DOT(Label = obj.Size)
Jak widać, jedyne co było potrzebne to odrobina znajomości składni Cypher. Ma on potężne możliwości, więc nie obawiam się szczególnie, że czegoś nie da się zrobić. A lepiej użyć języka, który zdobywa popularność i ma coraz więcej tutoriali i innych materiałów w sieci, niż pisać coś własnego!
Ponadto, zakładam pewną strukturę zależności pomiędzy różnymi bytami w pamięci. Np. Segment będzie miał relacje do wchodzących w jego skład Generacji, a te do zawartych w nich obiektach. Dzięki temu, oraz składni Cypher, możemy więc łatwo narysować tylko segmenty i zawarte w nich generacje 2:
MATCH (seg:Segment) --> (gen:Generation)
WHERE gen.Generation = 2
RETURN seg, gen AS BOX(Label = seg.Name,
LabelPosition = InnerCenter, Background = Yellow)
|
|
Możemy to rozszerzać dalej, np. rysując na nich dodatkowo obiekty typu Pinned:
MATCH (seg:Segment) --> (gen:Generation) --> (obj:Object)
WHERE gen.Generation = 2 AND obj.Handle = Pinned
RETURN seg,
gen AS BOX(Label = seg.Name, LabelPosition = InnerCenter),
obj AS PIN
|
|
Mogę też łączyć (dzięki sprytnemu rysowanium czyli semantyki nakładania) segmenty, generacje i grafy obiektów:
MATCH (seg:Segment) RETURN seg
MATCH (gen:Generation) RETURN gen AS BOX(Label = seg.Name, LabelPosition = InnerCenter)
MATCH (obj:Object) -[ref]-> (child:Object)
WHERE obj.@Address = 0xDDE51018
RETURN obj, ref, child
|
|
Największy problem mam na razie z rysunkami przykładowymi 2 oraz 6 czyli rozkład segmentów na obszarze pamięci. Potrzebuję tutaj móc narysować symbolicznie obszar pamięci, w tym celu chyba zaimplementuję nową komendę, która być może przyda się też w przyszłości - DRAW. Może ona przyjmować różne wejściowe funkcje, na początku niech to będzie funkcja Memory, rysująca symbolicznie zadany blok pamięci:
DRAW Memory(0xDDE51000, 0xDFE51000, Width = 1M)
|
|
Następnie można by ją wykorzystywać z pozostałymi powyższymi przykładami - dzięki "semantyce nakładania" elementy typu BOX, wiedząc że w wyniku znajduje się Memory, zmieniałyby trochę sposób rysowania, by się w nią wpasowywać:
MATCH (gen:Generation) RETURN gen
RETURN gen AS BOX(Background = hash(gen.Generation))
DRAW Memory(0xDDE51000, 0xDFE51000, Width = 1M)
|
|
Mówi się łatwo, ale nie mam jeszcze pomysłu jak to zaimplementować.
Idąc tym tropem również obiekty moglibyśmy rysować jako BOX na tle przestrzeni adresowej, np. pokazując tylko wolną przestrzeń (fragmentację):
MATCH (obj:Object)
WHERE obj.Type = Free
RETURN obj AS BOX
DRAW Memory(0xDDE51000, 0xDFE51000, Width = 1M)
|
|
Powyższe przykłady pokazują moim zdaniem, że składnia Cypher w połączeniu z operatorem AS będą na tyle silne, że narysować da się wszystko. Czy to do analizy problemów, czy na potrzeby prezentacji/wykładów, czy też po prostu dla frajdy.