You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2021/12/03 15:44:49 UTC

[isis] branch master updated: ISIS-2903: adds CallStack Visualization to _Xray

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/master by this push:
     new 65aab3d  ISIS-2903: adds CallStack Visualization to _Xray
65aab3d is described below

commit 65aab3dda13acd5b4b1d57d557249da6d0eb8da7
Author: Andi Huber <ah...@apache.org>
AuthorDate: Fri Dec 3 16:43:42 2021 +0100

    ISIS-2903: adds CallStack Visualization to _Xray
---
 .../apache/isis/commons/internal/debug/_Debug.java |  25 ++-
 .../commons/internal/debug/xray/XrayDataModel.java |  27 +---
 .../isis/commons/internal/debug/xray/XrayUi.java   |  76 ++++++++-
 .../internal/debug/xray/_CallStackMerger.java      | 178 +++++++++++++++++++++
 .../commons/internal/debug/xray/_SwingUtil.java    |  41 +++--
 .../debug/xray/graphics/CallStackDiagram.java      |  37 +++++
 .../{sequence => graphics}/SequenceDiagram.java    |  39 +++--
 .../xray/{sequence => graphics}/_Graphics.java     |   2 +-
 .../commons/internal/base/debug/XrayUiTest.java    |  29 +++-
 .../internal/debug/xray/CallStackMergerTest.java   |  76 +++++++++
 .../apache/isis/core/security/util/XrayUtil.java   |   2 +-
 11 files changed, 466 insertions(+), 66 deletions(-)

diff --git a/commons/src/main/java/org/apache/isis/commons/internal/debug/_Debug.java b/commons/src/main/java/org/apache/isis/commons/internal/debug/_Debug.java
index b0ef600..225c7cf 100644
--- a/commons/src/main/java/org/apache/isis/commons/internal/debug/_Debug.java
+++ b/commons/src/main/java/org/apache/isis/commons/internal/debug/_Debug.java
@@ -94,18 +94,29 @@ public class _Debug {
 
     // -- HELPER
 
-    private void dump(final Object x, final int indent) {
+    private void dump(Object x, final int indent) {
         if(x instanceof Iterable) {
             _NullSafe.streamAutodetect(x)
             .forEach(element->dump(element, indent+1));
+            return;
+        }
+        if(x!=null
+                && x.getClass().isArray()) {
+
+            val array = _NullSafe.streamAutodetect(x)
+            .map(e->""+e)
+            .collect(Collectors.joining(", "));
+
+            x = String.format("[%s]", array);
+        }
+
+        if(indent==0) {
+            System.err.printf("%s%n", x);
         } else {
-            if(indent==0) {
-                System.err.printf("%s%n", x);
-            } else {
-                val suffix = _Strings.padEnd("", indent, '-');
-                System.err.printf("%s %s%n", suffix, x);
-            }
+            val suffix = _Strings.padEnd("", indent, '-');
+            System.err.printf("%s %s%n", suffix, x);
         }
+
     }
 
     private boolean accept(final StackTraceElement se) {
diff --git a/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/XrayDataModel.java b/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/XrayDataModel.java
index 0e7fb80..4176829 100644
--- a/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/XrayDataModel.java
+++ b/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/XrayDataModel.java
@@ -20,7 +20,6 @@ package org.apache.isis.commons.internal.debug.xray;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
-import java.awt.Graphics;
 import java.awt.Graphics2D;
 import java.util.ArrayList;
 import java.util.List;
@@ -36,7 +35,7 @@ import javax.swing.JScrollPane;
 import org.apache.isis.commons.functional.IndexedConsumer;
 import org.apache.isis.commons.internal.base._Refs;
 import org.apache.isis.commons.internal.debug.xray.XrayModel.HasIdAndLabel;
-import org.apache.isis.commons.internal.debug.xray.sequence.SequenceDiagram;
+import org.apache.isis.commons.internal.debug.xray.graphics.SequenceDiagram;
 
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
@@ -140,6 +139,7 @@ public abstract class XrayDataModel extends HasIdAndLabel {
 
             panel.setViewportView(panel2);
         }
+
     }
 
 
@@ -168,22 +168,13 @@ public abstract class XrayDataModel extends HasIdAndLabel {
         @Override
         public void render(final JScrollPane panel) {
 
-            val dim = data.layout((Graphics2D)panel.getGraphics());
-
-            val canvas = new JPanel() {
-                private static final long serialVersionUID = 1L;
-
-                @Override
-                public void paintComponent(final Graphics _g) {
-
-                    val g = (Graphics2D)_g;
-
-                    g.setColor(BACKGROUND_COLOR);
-                    g.fillRect(0, 0, getWidth(), getHeight());
+            val canvas = _SwingUtil.canvas(g->{
+                g.setColor(BACKGROUND_COLOR);
+                g.fill(g.getClip());
+                data.render(g);
+            });
 
-                    data.render(g);
-                  }
-            };
+            val dim = data.layout((Graphics2D)panel.getGraphics());
 
             if(BORDER_COLOR!=null) {
                 canvas.setBorder(BorderFactory.createLineBorder(BORDER_COLOR));
@@ -193,8 +184,6 @@ public abstract class XrayDataModel extends HasIdAndLabel {
             panel.setViewportView(canvas);
 
         }
-
-
     }
 
 }
diff --git a/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/XrayUi.java b/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/XrayUi.java
index 3c0c078..ed0d1fd 100644
--- a/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/XrayUi.java
+++ b/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/XrayUi.java
@@ -18,8 +18,10 @@
  */
 package org.apache.isis.commons.internal.debug.xray;
 
+import java.awt.BorderLayout;
 import java.awt.Component;
 import java.awt.Dimension;
+import java.awt.Font;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.KeyEvent;
@@ -32,7 +34,9 @@ import java.net.URL;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
+import java.util.stream.Stream;
 
+import javax.swing.BoxLayout;
 import javax.swing.ImageIcon;
 import javax.swing.JFrame;
 import javax.swing.JLabel;
@@ -41,13 +45,14 @@ import javax.swing.JPanel;
 import javax.swing.JPopupMenu;
 import javax.swing.JScrollPane;
 import javax.swing.JSplitPane;
+import javax.swing.JTextArea;
 import javax.swing.JTree;
+import javax.swing.ScrollPaneConstants;
 import javax.swing.SwingUtilities;
 import javax.swing.event.TreeSelectionEvent;
 import javax.swing.tree.DefaultMutableTreeNode;
 import javax.swing.tree.DefaultTreeCellRenderer;
 import javax.swing.tree.DefaultTreeModel;
-import javax.swing.tree.MutableTreeNode;
 import javax.swing.tree.TreeCellRenderer;
 
 import org.apache.isis.commons.collections.Can;
@@ -142,6 +147,15 @@ public class XrayUi extends JFrame {
         });
 
         val popupMenu = new JPopupMenu();
+
+        val callStackMergeAction = popupMenu.add(new JMenuItem("Merge Logged Call-Stack"));
+        callStackMergeAction.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(final ActionEvent e) {
+                mergeCallStacksOnSelectedNodes();
+            }
+        });
+
         val deleteAction = popupMenu.add(new JMenuItem("Delete"));
         deleteAction.addActionListener(new ActionListener() {
             @Override
@@ -199,10 +213,15 @@ public class XrayUi extends JFrame {
         });
     }
 
+    private Stream<DefaultMutableTreeNode> streamSelectedNodes() {
+        return Can.ofArray(tree.getSelectionModel().getSelectionPaths())
+                .stream()
+                .map(path->(DefaultMutableTreeNode)path.getLastPathComponent());
+    }
+
     private void removeSelectedNodes() {
-        Can.ofArray(tree.getSelectionModel().getSelectionPaths())
-        .forEach(path->{
-            val nodeToBeRemoved = (MutableTreeNode)path.getLastPathComponent();
+        streamSelectedNodes()
+        .forEach(nodeToBeRemoved->{
             if(nodeToBeRemoved.getParent()!=null) {
                 ((DefaultTreeModel)tree.getModel()).removeNodeFromParent(nodeToBeRemoved);
                 xrayModel.remove(nodeToBeRemoved);
@@ -210,6 +229,55 @@ public class XrayUi extends JFrame {
         });
     }
 
+    private void mergeCallStacksOnSelectedNodes() {
+        val logEntries = streamSelectedNodes()
+        .filter(node->node.getUserObject() instanceof XrayDataModel.LogEntry)
+        .map(node->(XrayDataModel.LogEntry)node.getUserObject())
+        .collect(Can.toCan());
+
+        if(!logEntries.getCardinality().isMultiple()) {
+            System.err.println("must select at least 2 logs for merging");
+            return;
+        }
+
+        val callStackMerger = new _CallStackMerger(logEntries);
+
+        JFrame frame = new JFrame("Merged Log View");
+        JPanel panel = new JPanel();
+        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
+        panel.setOpaque(true);
+//        val canvas = _SwingUtil.canvas(g->{
+//            g.setColor(Color.GRAY);
+//            g.fill(g.getClip());
+//            callStackMerger.render(g);
+//        });
+//        JScrollPane scroller = new JScrollPane(canvas);
+
+        //Create a text area.
+        JTextArea textArea = new JTextArea(
+                "This is an editable JTextArea. " +
+                "A text area is a \"plain\" text component, " +
+                "which means that although it can display text " +
+                "in any font, all of the text is in the same font."
+        );
+        textArea.setFont(new Font("Serif", Font.PLAIN, 16));
+        textArea.setLineWrap(true);
+        textArea.setWrapStyleWord(true);
+        JScrollPane scroller = new JScrollPane(textArea);
+        callStackMerger.render(textArea);
+
+        scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
+        scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+        panel.add(scroller);
+        frame.getContentPane().add(BorderLayout.CENTER, panel);
+        frame.setPreferredSize(new Dimension(800, 600));
+        frame.pack();
+        frame.setLocationByPlatform(true);
+        frame.setVisible(true);
+        frame.setResizable(true);
+        frame.setVisible(true);
+    }
+
     private JScrollPane layoutUIAndGetDetailPanel(final JTree masterTree) {
 
         JScrollPane masterScrollPane = new JScrollPane(masterTree);
diff --git a/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/_CallStackMerger.java b/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/_CallStackMerger.java
new file mode 100644
index 0000000..2d90585
--- /dev/null
+++ b/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/_CallStackMerger.java
@@ -0,0 +1,178 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.commons.internal.debug.xray;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.IntFunction;
+
+import javax.swing.JTextArea;
+
+import org.apache.isis.commons.collections.Can;
+import org.apache.isis.commons.functional.IndexedConsumer;
+import org.apache.isis.commons.internal.collections._Maps;
+import org.apache.isis.commons.internal.collections._Sets;
+import org.apache.isis.commons.internal.debug.xray.XrayDataModel.LogEntry;
+import org.apache.isis.commons.internal.debug.xray.graphics.CallStackDiagram;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@RequiredArgsConstructor
+final class _CallStackMerger {
+
+    private final Can<LogEntry> logEntries;
+    private final AtomicBoolean initialized = new AtomicBoolean(false);
+    private CallStackDiagram callStackDiagram;
+
+    void render(final JTextArea textArea) {
+        if(!initialized.get()) {
+            initialize();
+            initialized.set(true);
+        }
+        callStackDiagram.render(textArea);
+    }
+
+    static interface IntTreeVisitor {
+        void accept(int level, int value);
+    }
+
+    @RequiredArgsConstructor
+    static class IntTreeNode {
+        final int value;
+        final int level;
+        final IntTreeNode parent;
+        final List<IntTreeNode> children = new ArrayList<>();
+        IntTreeNode addChild(final int value) {
+            IntTreeNode child;
+            children.add(child = new IntTreeNode(value, level+1, this));
+            return child;
+        }
+        static IntTreeNode newRoot(final int value) {
+            return new IntTreeNode(value, 0, null);
+        }
+        void visitDepthFirst(final IntTreeVisitor visitor) {
+            visitor.accept(level, value);
+            for(val child : children) {
+                child.visitDepthFirst(visitor);
+            }
+        }
+        void visitBreadthFirst(final IntTreeVisitor visitor) {
+            val queue = new ArrayDeque<IntTreeNode>();
+            queue.add(this);
+            IntTreeNode currentNode;
+            while (!queue.isEmpty()) {
+                currentNode = queue.remove();
+                visitor.accept(currentNode.level, currentNode.value);
+                queue.addAll(currentNode.children);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return print(i->""+i).toString();
+        }
+
+        private StringBuilder print(final IntFunction<String> valueMapper) {
+            val sb = new StringBuilder();
+            print(valueMapper, sb, "", "");
+            return sb;
+        }
+
+        private void print(final IntFunction<String> valueMapper,
+                final StringBuilder buffer, final String prefix, final String childrenPrefix) {
+            buffer.append(prefix);
+            buffer.append(valueMapper.apply(value));
+            buffer.append('\n');
+            for (Iterator<IntTreeNode> it = children.iterator(); it.hasNext();) {
+                IntTreeNode next = it.next();
+                if (it.hasNext()) {
+                    next.print(valueMapper, buffer, childrenPrefix + "├── ", childrenPrefix + "│   ");
+                } else {
+                    next.print(valueMapper, buffer, childrenPrefix + "└── ", childrenPrefix + "    ");
+                }
+            }
+        }
+
+    }
+
+    private void initialize() {
+
+        val executionNodeSet = _Sets.<String>newHashSet(); // temporary helper
+        val executionNodeMap = _Maps.<Integer, String>newHashMap(); // StackStraceElement by unique id
+
+        val executionLanes = new ArrayList<int[]>();
+
+        logEntries.forEach(logEntry->{
+            //System.err.printf("joining %s%n", logEntry.getLabel());
+
+            val executionLane = new int[logEntry.getData().size()];
+            executionLanes.add(executionLane);
+
+            Can.ofCollection(logEntry.getData()).reverse().stream()
+            .map(StackTraceElement::toString).forEach(IndexedConsumer.zeroBased((index, se)->{
+                val isNew = executionNodeSet.add(se);
+                if(isNew) {
+                    final int id = executionNodeSet.size();
+                    executionNodeMap.put(id, se);
+                    executionLane[index] = id;
+                } else {
+                    final int id = executionNodeMap.entrySet().stream()
+                    .filter(entry->entry.getValue().equals(se))
+                    .mapToInt(entry->(int)entry.getKey())
+                    .findAny()
+                    .orElseThrow();
+                    executionLane[index] = id;
+                }
+            }));
+        });
+
+        val root = merge(executionLanes);
+        callStackDiagram = new CallStackDiagram(root.print(id->{
+            return executionNodeMap.getOrDefault(id, "root");
+        }).toString());
+    }
+
+    /**
+     * executionLanes look like
+     * [1, 2, 3, 4, 5, 6]
+     * [1, 2, 3, 7, 8, 6, 9]
+     * [1, 2, 3, 4, 8]
+     * ...
+     */
+    static IntTreeNode merge(final List<int[]> executionLanes) {
+        val root = IntTreeNode.newRoot(-1);
+        executionLanes.forEach(lane->{
+            var node = root;
+            for(int id : lane) {
+                val equalNode = node.children.stream().filter(child->child.value==id).findAny();
+                if(!equalNode.isPresent()) {
+                    node = node.addChild(id);
+                } else {
+                    node = equalNode.get();
+                }
+            }
+        });
+        return root;
+    }
+
+}
diff --git a/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/_SwingUtil.java b/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/_SwingUtil.java
index 634f879..5670991 100644
--- a/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/_SwingUtil.java
+++ b/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/_SwingUtil.java
@@ -19,8 +19,12 @@
 package org.apache.isis.commons.internal.debug.xray;
 
 import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
 import java.util.Collections;
+import java.util.function.Consumer;
 
+import javax.swing.JPanel;
 import javax.swing.JTable;
 import javax.swing.JTree;
 import javax.swing.table.TableCellRenderer;
@@ -29,31 +33,33 @@ import javax.swing.tree.DefaultMutableTreeNode;
 import javax.swing.tree.TreePath;
 
 import lombok.val;
+import lombok.experimental.UtilityClass;
 
+@UtilityClass
 final class _SwingUtil {
 
-    static JTable newTable(final Object[][] tableData, final String[] columnNames) {
+    JTable newTable(final Object[][] tableData, final String[] columnNames) {
         val table = new JTable(tableData, columnNames) {
             private static final long serialVersionUID = 1L;
             @Override
-               public Component prepareRenderer(final TableCellRenderer renderer, final int row, final int column) {
-                   Component component = super.prepareRenderer(renderer, row, column);
-                   int rendererWidth = component.getPreferredSize().width;
-                   TableColumn tableColumn = getColumnModel().getColumn(column);
-                   tableColumn.setPreferredWidth(Math.max(rendererWidth + getIntercellSpacing().width, tableColumn.getPreferredWidth()));
-                   return component;
-                }
-            };
+            public Component prepareRenderer(final TableCellRenderer renderer, final int row, final int column) {
+                Component component = super.prepareRenderer(renderer, row, column);
+                int rendererWidth = component.getPreferredSize().width;
+                TableColumn tableColumn = getColumnModel().getColumn(column);
+                tableColumn.setPreferredWidth(Math.max(rendererWidth + getIntercellSpacing().width, tableColumn.getPreferredWidth()));
+                return component;
+            }
+        };
         table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
         return table;
     }
 
-    static void setTreeExpandedState(final JTree tree, final boolean expanded) {
+    void setTreeExpandedState(final JTree tree, final boolean expanded) {
         DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getModel().getRoot();
         setNodeExpandedState(tree, node, expanded);
     }
 
-    static void setNodeExpandedState(final JTree tree, final DefaultMutableTreeNode node, final boolean expanded) {
+    void setNodeExpandedState(final JTree tree, final DefaultMutableTreeNode node, final boolean expanded) {
         for (Object treeNode : Collections.list(node.children())) {
             setNodeExpandedState(tree, (DefaultMutableTreeNode) treeNode, expanded);
         }
@@ -68,4 +74,17 @@ final class _SwingUtil {
         }
     }
 
+    JPanel canvas(final Consumer<Graphics2D> onRender) {
+
+        val canvas = new JPanel() {
+            private static final long serialVersionUID = 1L;
+            @Override
+            public void paintComponent(final Graphics g) {
+                onRender.accept((Graphics2D)g);
+            }
+        };
+
+        return canvas;
+    }
+
 }
diff --git a/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/graphics/CallStackDiagram.java b/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/graphics/CallStackDiagram.java
new file mode 100644
index 0000000..efcdaa9
--- /dev/null
+++ b/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/graphics/CallStackDiagram.java
@@ -0,0 +1,37 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.commons.internal.debug.xray.graphics;
+
+import java.awt.Font;
+
+import javax.swing.JTextArea;
+
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public class CallStackDiagram {
+
+    private final String textContent;
+
+    public void render(final JTextArea textArea) {
+        textArea.setFont(new Font("Consolas", Font.PLAIN, 13));
+        textArea.setText(textContent);
+    }
+
+}
diff --git a/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/sequence/SequenceDiagram.java b/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/graphics/SequenceDiagram.java
similarity index 91%
rename from commons/src/main/java/org/apache/isis/commons/internal/debug/xray/sequence/SequenceDiagram.java
rename to commons/src/main/java/org/apache/isis/commons/internal/debug/xray/graphics/SequenceDiagram.java
index ea97748..e274c3e 100644
--- a/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/sequence/SequenceDiagram.java
+++ b/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/graphics/SequenceDiagram.java
@@ -16,7 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.isis.commons.internal.debug.xray.sequence;
+package org.apache.isis.commons.internal.debug.xray.graphics;
 
 import java.awt.Color;
 import java.awt.Dimension;
@@ -32,7 +32,7 @@ import java.util.TreeMap;
 import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.internal.base._Refs;
 import org.apache.isis.commons.internal.base._Refs.IntReference;
-import org.apache.isis.commons.internal.debug.xray.sequence._Graphics.TextBlock;
+import org.apache.isis.commons.internal.debug.xray.graphics._Graphics.TextBlock;
 import org.apache.isis.commons.internal.primitives._Ints;
 
 import lombok.Getter;
@@ -50,38 +50,38 @@ public class SequenceDiagram {
 
     private Dimension size;
 
-    public SequenceDiagram alias(String id, String label) {
+    public SequenceDiagram alias(final String id, final String label) {
         aliases.put(id, label);
         return this;
     }
 
-    public void enter(final @NonNull String from, final @NonNull String to, String label) {
+    public void enter(final @NonNull String from, final @NonNull String to, final String label) {
         val p0 = participant(from);
         val p1 = participant(to);
         connections.add(newConnection(p0, p1, label, false));
     }
 
-    public void exit(final @NonNull String from, final @NonNull String to, String label) {
+    public void exit(final @NonNull String from, final @NonNull String to, final String label) {
         val p1 = participant(to);
         val p0 = participant(from);
         connections.add(newConnection(p0, p1, label, true));
     }
 
-    public void enter(String from, String to) {
+    public void enter(final String from, final String to) {
         enter(from, to, null);
     }
 
-    public void exit(String from, String to) {
+    public void exit(final String from, final String to) {
         exit(from, to, null);
     }
 
-    public void activate(String participantId) {
+    public void activate(final String participantId) {
         val participant = participant(participantId);
         val latestConnection = latestConnection();
         lifelines.add(new Lifeline(participant, latestConnection));
     }
 
-    public void deactivate(String participantId) {
+    public void deactivate(final String participantId) {
         val participant = participant(participantId);
         val latestConnection = latestConnection();
         Can.ofCollection(lifelines).reverse().stream()
@@ -95,11 +95,11 @@ public class SequenceDiagram {
     private Color connectionArrowColor;
     private Color connectionLabelColor;
 
-    public void setConnectionArrowColor(Color connectionArrowColor) {
+    public void setConnectionArrowColor(final Color connectionArrowColor) {
         this.connectionArrowColor = connectionArrowColor;
     }
 
-    public void setConnectionLabelColor(Color connectionLabelColor) {
+    public void setConnectionLabelColor(final Color connectionLabelColor) {
         this.connectionLabelColor = connectionLabelColor;
     }
 
@@ -120,7 +120,7 @@ public class SequenceDiagram {
                 getConnectionLabelColor());
     }
 
-    private Participant participant(String participantId) {
+    private Participant participant(final String participantId) {
         return participantsById
                 .computeIfAbsent(participantId, id->new Participant(aliases.getOrDefault(id, id)));
     }
@@ -186,7 +186,7 @@ public class SequenceDiagram {
         int y_bottom;
         int height;
 
-        void layout(Graphics2D g, IntReference y_offset, List<Lifeline> lifelines) {
+        void layout(final Graphics2D g, final IntReference y_offset, final List<Lifeline> lifelines) {
 
             x_from = from.getX_middle();
             x_to = to.getX_middle();
@@ -244,7 +244,7 @@ public class SequenceDiagram {
 
         TextBlock textBlock;
 
-        void layout(Graphics2D g, IntReference x_offset) {
+        void layout(final Graphics2D g, final IntReference x_offset) {
 
             x_left = x_offset.getValue();
             y_top = PARTICIPANT_MARGIN_V;
@@ -282,7 +282,7 @@ public class SequenceDiagram {
         int y_bottom;
         int height;
 
-        void layout(Graphics2D g, int min_y, int max_y) {
+        void layout(final Graphics2D g, final int min_y, final int max_y) {
 
             width = LIFELINE_WIDTH;
             x_left = participant.getX_middle() - LIFELINE_WIDTH / 2;
@@ -298,7 +298,7 @@ public class SequenceDiagram {
             height = y_bottom - y_top;
         }
 
-        public boolean overlaps(Connection connection) {
+        public boolean overlaps(final Connection connection) {
             val lowerBound = _Ints.Bound.inclusive(startAt != null
                     ? startAt.index
                     : -1);
@@ -310,7 +310,7 @@ public class SequenceDiagram {
     }
 
 
-    public Dimension layout(Graphics2D g) {
+    public Dimension layout(final Graphics2D g) {
 
         PARTICIPANT_FONT.ifPresent(g::setFont);
 
@@ -343,7 +343,7 @@ public class SequenceDiagram {
         return this.size;
     }
 
-    public void render(Graphics2D g) {
+    public void render(final Graphics2D g) {
 
         _Graphics.enableTextAntialiasing(g);
 
@@ -416,7 +416,4 @@ public class SequenceDiagram {
 
     }
 
-
-
-
 }
diff --git a/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/sequence/_Graphics.java b/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/graphics/_Graphics.java
similarity index 98%
rename from commons/src/main/java/org/apache/isis/commons/internal/debug/xray/sequence/_Graphics.java
rename to commons/src/main/java/org/apache/isis/commons/internal/debug/xray/graphics/_Graphics.java
index fdfc6b4..31edcb1 100644
--- a/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/sequence/_Graphics.java
+++ b/commons/src/main/java/org/apache/isis/commons/internal/debug/xray/graphics/_Graphics.java
@@ -16,7 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.isis.commons.internal.debug.xray.sequence;
+package org.apache.isis.commons.internal.debug.xray.graphics;
 
 import java.awt.BasicStroke;
 import java.awt.Color;
diff --git a/commons/src/test/java/org/apache/isis/commons/internal/base/debug/XrayUiTest.java b/commons/src/test/java/org/apache/isis/commons/internal/base/debug/XrayUiTest.java
index d98daef..a026ec3 100644
--- a/commons/src/test/java/org/apache/isis/commons/internal/base/debug/XrayUiTest.java
+++ b/commons/src/test/java/org/apache/isis/commons/internal/base/debug/XrayUiTest.java
@@ -18,8 +18,13 @@
  */
 package org.apache.isis.commons.internal.base.debug;
 
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
 import javax.swing.JFrame;
 
+import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.internal.debug._Debug;
 import org.apache.isis.commons.internal.debug.xray.XrayDataModel;
 import org.apache.isis.commons.internal.debug.xray.XrayModel;
@@ -29,14 +34,34 @@ import lombok.val;
 
 class XrayUiTest {
 
-    public static void main(final String[] args) {
+    public static void main(final String[] args) throws InterruptedException {
         XrayUi.start(JFrame.EXIT_ON_CLOSE);
 
-        _Debug.log("%s", "Hallo World!");
+        val ex = Executors.newSingleThreadExecutor();
+        ex.execute(new SampleLogs());
+        ex.shutdown();
+        ex.awaitTermination(1L, TimeUnit.SECONDS);
 
         XrayUi.updateModel(XrayUiTest::populate);
     }
 
+    private static class SampleLogs implements Runnable {
+
+        @Override
+        public void run() {
+            Can.of("Hallo World! from Can")
+            .forEach(this::log);
+
+            List.of("Hallo World! from List")
+            .forEach(this::log);
+        }
+
+        private void log(final String x) {
+            _Debug.log(x);
+        }
+
+    }
+
     private static void populate(final XrayModel model) {
 
         val root = model.getRootNode();
diff --git a/commons/src/test/java/org/apache/isis/commons/internal/debug/xray/CallStackMergerTest.java b/commons/src/test/java/org/apache/isis/commons/internal/debug/xray/CallStackMergerTest.java
new file mode 100644
index 0000000..35046e2
--- /dev/null
+++ b/commons/src/test/java/org/apache/isis/commons/internal/debug/xray/CallStackMergerTest.java
@@ -0,0 +1,76 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.commons.internal.debug.xray;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.isis.commons.internal.debug.xray._CallStackMerger.IntTreeVisitor;
+
+import lombok.val;
+
+class CallStackMergerTest {
+
+    private List<int[]> executionLanes;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        executionLanes = List.of(
+                new int[] {1, 2, 3, 4, 5, 6},
+                new int[] {1, 2, 3, 7, 8, 6, 9},
+                new int[] {1, 2, 3, 4, 8}
+                );
+    }
+
+    /**
+     * expected ...<pre>
+-1
+└── 1
+    └── 2
+        └── 3
+            ├── 4
+            │   ├── 5
+            │   │   └── 6
+            │   └── 8
+            └── 7
+                └── 8
+                    └── 6
+                        └── 9
+    </pre>
+     */
+    @Test
+    void test() {
+        val root = _CallStackMerger.merge(executionLanes);
+        //System.err.printf("%s%n", root);
+
+        val sb = new StringBuilder();
+        root.visitDepthFirst(new IntTreeVisitor() {
+            @Override
+            public void accept(final int level, final int value) {
+                sb.append(level).append(":").append(value).append(",");
+            }
+        });
+        assertEquals("0:-1,1:1,2:2,3:3,4:4,5:5,6:6,5:8,4:7,5:8,6:6,7:9,", sb.toString());
+    }
+
+}
diff --git a/core/security/src/main/java/org/apache/isis/core/security/util/XrayUtil.java b/core/security/src/main/java/org/apache/isis/core/security/util/XrayUtil.java
index 092e99b..b5b2f10 100644
--- a/core/security/src/main/java/org/apache/isis/core/security/util/XrayUtil.java
+++ b/core/security/src/main/java/org/apache/isis/core/security/util/XrayUtil.java
@@ -25,8 +25,8 @@ import java.util.function.Consumer;
 import org.apache.isis.applib.services.iactn.InteractionProvider;
 import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.internal.debug.xray.XrayModel.ThreadMemento;
+import org.apache.isis.commons.internal.debug.xray.graphics.SequenceDiagram;
 import org.apache.isis.commons.internal.debug.xray.XrayUi;
-import org.apache.isis.commons.internal.debug.xray.sequence.SequenceDiagram;
 import org.apache.isis.commons.internal.exceptions._Exceptions;
 
 import lombok.Builder;