diff --git a/scripts/logviewer/index.html b/scripts/logviewer/index.html
index ef5f9f23..9b26cfd5 100644
--- a/scripts/logviewer/index.html
+++ b/scripts/logviewer/index.html
@@ -110,7 +110,7 @@
margin: 0;
}
- .timeline div.item {
+ .timeline .item {
--hue: 100deg;
--brightness: 80%;
background-color: hsl(var(--hue), 60%, var(--brightness));
@@ -121,15 +121,16 @@
margin: 1px;
flex: 1;
min-width: 0;
- cursor: pointer;
+ color: inherit;
+ text-decoration: none;
}
- .timeline div.item:not(.has-children) {
+ .timeline .item:not(.has-children) {
margin-left: calc(24px + 4px + 1px);
}
- .timeline div.item .caption {
+ .timeline .item .caption {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@@ -157,15 +158,15 @@
color: white;
}
- .timeline div.item.type-network {
+ .timeline .item.type-network {
--hue: 30deg;
}
- .timeline div.item.type-navigation {
+ .timeline .item.type-navigation {
--hue: 200deg;
}
- .timeline div.item.selected {
+ .timeline .item.selected {
background-color: Highlight;
border-color: Highlight;
color: HighlightText;
diff --git a/scripts/logviewer/main.js b/scripts/logviewer/main.js
index f06ee582..af5f562c 100644
--- a/scripts/logviewer/main.js
+++ b/scripts/logviewer/main.js
@@ -21,37 +21,55 @@ const main = document.querySelector("main");
let selectedItemNode;
let rootItem;
+let itemByRef;
const logLevels = [undefined, "All", "Debug", "Detail", "Info", "Warn", "Error", "Fatal", "Off"];
main.addEventListener("click", event => {
- if (selectedItemNode) {
- selectedItemNode.classList.remove("selected");
- selectedItemNode = null;
- }
if (event.target.classList.contains("toggleExpanded")) {
const li = event.target.parentElement.parentElement;
li.classList.toggle("expanded");
} else {
+ // allow clicking any links other than .item in the timeline, like refs
+ if (event.target.tagName === "A" && !event.target.classList.contains("item")) {
+ return;
+ }
const itemNode = event.target.closest(".item");
if (itemNode) {
- selectedItemNode = itemNode;
- selectedItemNode.classList.add("selected");
- const path = selectedItemNode.dataset.path;
- let item = rootItem;
- let parent;
- if (path.length) {
- const indices = path.split("/").map(i => parseInt(i, 10));
- for(const i of indices) {
- parent = item;
- item = itemChildren(item)[i];
- }
- }
- showItemDetails(item, parent, itemNode);
+ // we don't want scroll to jump when clicking
+ // so prevent default behaviour, and select and push to history manually
+ event.preventDefault();
+ selectNode(itemNode);
+ history.pushState(null, null, `#${itemNode.id}`);
}
}
});
+window.addEventListener("hashchange", () => {
+ const id = window.location.hash.substr(1);
+ const itemNode = document.getElementById(id);
+ if (itemNode && itemNode.closest("main")) {
+ selectNode(itemNode);
+ itemNode.scrollIntoView({behavior: "smooth", block: "nearest"});
+ }
+});
+
+function selectNode(itemNode) {
+ if (selectedItemNode) {
+ selectedItemNode.classList.remove("selected");
+ }
+ selectedItemNode = itemNode;
+ selectedItemNode.classList.add("selected");
+ let item = rootItem;
+ let parent;
+ const indices = selectedItemNode.id.split("/").map(i => parseInt(i, 10));
+ for(const i of indices) {
+ parent = item;
+ item = itemChildren(item)[i];
+ }
+ showItemDetails(item, parent, selectedItemNode);
+}
+
function stringifyItemValue(value) {
if (typeof value === "object" && value !== null) {
return JSON.stringify(value, undefined, 2);
@@ -75,9 +93,20 @@ function showItemDetails(item, parent, itemNode) {
t.p([t.strong("Forced finish: "), (itemForcedFinish(item) || false) + ""]),
t.p(t.strong("Values:")),
t.ul({class: "values"}, Object.entries(itemValues(item)).map(([key, value]) => {
+ let valueNode;
+ if (key === "ref") {
+ const refItem = itemByRef.get(value);
+ if (refItem) {
+ valueNode = t.a({href: `#${refItem.id}`}, itemCaption(refItem));
+ } else {
+ valueNode = `unknown ref ${value}`;
+ }
+ } else {
+ valueNode = stringifyItemValue(value);
+ }
return t.li([
t.span({className: "key"}, normalizeValueKey(key)),
- t.span({className: "value"}, stringifyItemValue(value))
+ t.span({className: "value"}, valueNode)
]);
})),
t.p(expandButton)
@@ -117,6 +146,9 @@ async function loadFile() {
const logs = JSON.parse(json);
logs.items.sort((a, b) => itemStart(a) - itemStart(b));
rootItem = {c: logs.items};
+ itemByRef = new Map();
+ preprocessRecursively(rootItem, itemByRef, []);
+
const fragment = logs.items.reduce((fragment, item, i, items) => {
const prevItem = i === 0 ? null : items[i - 1];
fragment.appendChild(t.section([
@@ -128,6 +160,21 @@ async function loadFile() {
main.replaceChildren(fragment);
}
+function preprocessRecursively(item, refsMap, path) {
+ if (itemRefSource(item)) {
+ refsMap.set(itemRefSource(item), item);
+ }
+ if (itemChildren(item)) {
+ for (let i = 0; i < itemChildren(item).length; i += 1) {
+ // do it in advance for a child as we don't want to do it for the rootItem
+ const child = itemChildren(item)[i];
+ const childPath = path.concat(i);
+ child.id = childPath.join("/");
+ preprocessRecursively(child, refsMap, childPath);
+ }
+ }
+}
+
function formatTime(ms) {
if (ms < 1000) {
return `${ms}ms`;
@@ -152,6 +199,8 @@ function itemLabel(item) { return item.v?.l; }
function itemType(item) { return item.v?.t; }
function itemError(item) { return item.e; }
function itemForcedFinish(item) { return item.f; }
+function itemRef(item) { return item.v?.ref; }
+function itemRefSource(item) { return item.v?.refId; }
function itemShortErrorMessage(item) {
if (itemError(item)) {
const e = itemError(item);
@@ -168,6 +217,13 @@ function itemCaption(item) {
return `${itemLabel(item)} (${itemValues(item).status})`;
} else if (itemLabel(item) && itemError(item)) {
return `${itemLabel(item)} (${itemShortErrorMessage(item)})`;
+ } else if (itemRef(item)) {
+ const refItem = itemByRef.get(itemRef(item));
+ if (refItem) {
+ return `ref "${itemCaption(refItem)}"`
+ } else {
+ return `unknown ref ${itemRef(item)}`
+ }
} else {
return itemLabel(item) || itemType(item);
}
@@ -181,7 +237,7 @@ function normalizeValueKey(key) {
}
// returns the node and the total range (recursively) occupied by the node
-function itemToNode(item, path) {
+function itemToNode(item) {
const hasChildren = !!itemChildren(item)?.length;
const className = {
item: true,
@@ -191,18 +247,29 @@ function itemToNode(item, path) {
[`level-${itemLevel(item)}`]: true,
};
+ const id = item.id;
+ let captionNode;
+ if (itemRef(item)) {
+ const refItem = itemByRef.get(itemRef(item));
+ if (refItem) {
+ captionNode = ["ref ", t.a({href: `#${refItem.id}`}, itemCaption(refItem))];
+ }
+ }
+ if (!captionNode) {
+ captionNode = itemCaption(item);
+ }
const li = t.li([
t.div([
hasChildren ? t.button({className: "toggleExpanded"}) : "",
- t.div({className, "data-path": path.join("/")}, [
- t.span({class: "caption"}, itemCaption(item)),
+ t.a({className, id, href: `#${id}`}, [
+ t.span({class: "caption"}, captionNode),
t.span({class: "duration"}, `(${itemDuration(item)}ms)`),
])
])
]);
if (itemChildren(item) && itemChildren(item).length) {
- li.appendChild(t.ol(itemChildren(item).map((item, i) => {
- return itemToNode(item, path.concat(i));
+ li.appendChild(t.ol(itemChildren(item).map(item => {
+ return itemToNode(item);
})));
}
return li;