From 0c33f9f2a31f51e6bd9af6f9c4ffbfa8b0d77856 Mon Sep 17 00:00:00 2001 From: Doris Wu Date: Tue, 29 Apr 2025 10:39:37 +0800 Subject: [PATCH] [fgviewer] Add graphviz view mode to the web page (#8673) * Export graphviz data to the webview * Fix some errors * Fix colors * Only show toggle when view selected * Update * Merge graphviz data into json string * Mark export_graphviz function as const * Set SERVE_FROM_SOURCE_TREE to 0 * Remove unnecessary import * Refactor * Refactor * New line when early return --- filament/src/fg/DependencyGraph.cpp | 2 +- filament/src/fg/FrameGraph.cpp | 7 +- filament/src/fg/FrameGraph.h | 2 +- filament/src/fg/details/DependencyGraph.h | 2 +- .../include/fgviewer/FrameGraphInfo.h | 5 + libs/fgviewer/src/FrameGraphInfo.cpp | 9 + libs/fgviewer/src/JsonWriter.cpp | 18 +- libs/fgviewer/web/app.js | 222 ++++++++++++++++-- 8 files changed, 235 insertions(+), 32 deletions(-) diff --git a/filament/src/fg/DependencyGraph.cpp b/filament/src/fg/DependencyGraph.cpp index fbcbf97886..2ea3819bb4 100644 --- a/filament/src/fg/DependencyGraph.cpp +++ b/filament/src/fg/DependencyGraph.cpp @@ -139,7 +139,7 @@ void DependencyGraph::clear() noexcept { mNodes.clear(); } -void DependencyGraph::export_graphviz(utils::io::ostream& out, char const* name) { +void DependencyGraph::export_graphviz(utils::io::ostream& out, char const* name) const noexcept { #ifndef NDEBUG const char* graphName = name ? name : "graph"; out << "digraph \"" << graphName << "\" {\n"; diff --git a/filament/src/fg/FrameGraph.cpp b/filament/src/fg/FrameGraph.cpp index 5a9a38810b..b0af89afc0 100644 --- a/filament/src/fg/FrameGraph.cpp +++ b/filament/src/fg/FrameGraph.cpp @@ -476,7 +476,7 @@ bool FrameGraph::isAcyclic() const noexcept { return mGraph.isAcyclic(); } -void FrameGraph::export_graphviz(utils::io::ostream& out, char const* name) { +void FrameGraph::export_graphviz(utils::io::ostream& out, char const* name) const noexcept { mGraph.export_graphviz(out, name); } @@ -573,6 +573,11 @@ fgviewer::FrameGraphInfo FrameGraph::getFrameGraphInfo(const char *viewName) con info.setResources(std::move(resources)); info.setPasses(std::move(passes)); + // Generate GraphViz DOT data + utils::io::sstream out; + this->export_graphviz(out, viewName); + info.setGraphvizData(utils::CString(out.c_str())); + return info; #else return fgviewer::FrameGraphInfo(); diff --git a/filament/src/fg/FrameGraph.h b/filament/src/fg/FrameGraph.h index 954e13b79c..ece245b13e 100644 --- a/filament/src/fg/FrameGraph.h +++ b/filament/src/fg/FrameGraph.h @@ -440,7 +440,7 @@ public: bool isAcyclic() const noexcept; //! export a graphviz view of the graph - void export_graphviz(utils::io::ostream& out, const char* name = nullptr); + void export_graphviz(utils::io::ostream& out, const char* name = nullptr) const noexcept; /** * Export a fgviewer::FrameGraphInfo for current graph. diff --git a/filament/src/fg/details/DependencyGraph.h b/filament/src/fg/details/DependencyGraph.h index d75fa3a5f1..607064cb6b 100644 --- a/filament/src/fg/details/DependencyGraph.h +++ b/filament/src/fg/details/DependencyGraph.h @@ -171,7 +171,7 @@ public: bool isEdgeValid(Edge const* edge) const noexcept; //! export a graphviz view of the graph - void export_graphviz(utils::io::ostream& out, const char* name = nullptr); + void export_graphviz(utils::io::ostream& out, const char* name = nullptr) const noexcept; bool isAcyclic() const noexcept; diff --git a/libs/fgviewer/include/fgviewer/FrameGraphInfo.h b/libs/fgviewer/include/fgviewer/FrameGraphInfo.h index b44420d0a2..2fcd44ce60 100644 --- a/libs/fgviewer/include/fgviewer/FrameGraphInfo.h +++ b/libs/fgviewer/include/fgviewer/FrameGraphInfo.h @@ -78,18 +78,23 @@ public: // The incoming passes should be sorted by the execution order. void setPasses(std::vector sortedPasses); + void setGraphvizData(utils::CString data); + const char* getViewName() const; const std::vector& getPasses() const; const std::unordered_map& getResources() const; + const char* getGraphvizData() const; + private: utils::CString viewName; // The order of the passes in the vector indicates the execution // order of the passes. std::vector passes; std::unordered_map resources; + utils::CString graphvizData; }; } // namespace filament::fgviewer diff --git a/libs/fgviewer/src/FrameGraphInfo.cpp b/libs/fgviewer/src/FrameGraphInfo.cpp index a1b02a261f..3a6f47a917 100644 --- a/libs/fgviewer/src/FrameGraphInfo.cpp +++ b/libs/fgviewer/src/FrameGraphInfo.cpp @@ -31,6 +31,7 @@ FrameGraphInfo::~FrameGraphInfo() = default; FrameGraphInfo::FrameGraphInfo(FrameGraphInfo&& rhs) noexcept = default; bool FrameGraphInfo::operator==(const FrameGraphInfo& rhs) const { + // We skip checking graphviz here since checking passes and resources should be enough. return viewName == rhs.viewName && passes == rhs.passes && resources == rhs.resources; @@ -66,6 +67,10 @@ void FrameGraphInfo::setPasses(std::vector sortedPasses) { passes = std::move(sortedPasses); } +void FrameGraphInfo::setGraphvizData(utils::CString data) { + graphvizData = std::move(data); +} + const char* FrameGraphInfo::getViewName() const { return viewName.c_str_safe(); } @@ -79,4 +84,8 @@ const std::unordered_map& return resources; } +const char* FrameGraphInfo::getGraphvizData() const { + return graphvizData.c_str_safe(); +} + } // namespace filament::fgviewer diff --git a/libs/fgviewer/src/JsonWriter.cpp b/libs/fgviewer/src/JsonWriter.cpp index d43c8a4218..2f354bf66e 100644 --- a/libs/fgviewer/src/JsonWriter.cpp +++ b/libs/fgviewer/src/JsonWriter.cpp @@ -27,6 +27,7 @@ namespace filament::fgviewer { namespace { + void writeJSONString(std::ostream& os, const char* str) { os << '"'; const char* p = str; @@ -46,7 +47,7 @@ void writeJSONString(std::ostream& os, const char* str) { void writeViewName(std::ostream& os, const FrameGraphInfo& frameGraph) { os << " \"viewName\": "; writeJSONString(os, frameGraph.getViewName()); - os << ",\n"; + os << "\n"; } void writeResourceIds(std::ostream& os, const std::vector& resources) { @@ -80,7 +81,7 @@ void writePasses(std::ostream& os, const FrameGraphInfo& frameGraph) { if (i + 1 < passes.size()) os << ","; os << "\n"; } - os << " ],\n"; + os << " ]\n"; } void writeResources(std::ostream& os, const FrameGraphInfo& frameGraph) { @@ -114,6 +115,15 @@ void writeResources(std::ostream& os, const FrameGraphInfo& frameGraph) { } os << " }\n"; } + +void writeGraphviz(std::ostream& os, const FrameGraphInfo& frameGraph) { + const char* graphvizString = frameGraph.getGraphvizData(); + + os << " \"graphviz\": "; + writeJSONString(os, graphvizString); + os << "\n"; +} + } // anonymous const char* JsonWriter::getJsonString() const { @@ -128,8 +138,12 @@ bool JsonWriter::writeFrameGraphInfo(const FrameGraphInfo& frameGraph) { std::ostringstream os; writeViewName(os, frameGraph); + os << ",\n"; writePasses(os, frameGraph); + os << ",\n"; writeResources(os, frameGraph); + os << ",\n"; + writeGraphviz(os, frameGraph); mJsonString = utils::CString(os.str().c_str()); diff --git a/libs/fgviewer/web/app.js b/libs/fgviewer/web/app.js index 071a33e479..c159a81ca7 100644 --- a/libs/fgviewer/web/app.js +++ b/libs/fgviewer/web/app.js @@ -15,6 +15,8 @@ */ import {LitElement, html, css, unsafeCSS, nothing} from "https://unpkg.com/lit@2.8.0?module"; +import { graphviz } from "https://cdn.skypack.dev/d3-graphviz@5.1.0"; +import * as d3 from "https://cdn.skypack.dev/d3@7"; const kUntitledPlaceholder = "Untitled View"; @@ -33,6 +35,11 @@ const READ_WRITE_COLOR = '#ffeb99'; const DEFAULT_COLOR = '#ffffff'; const SUBRESOURCE_COLOR = '#d3d3d3'; +// Constants for view mode toggle +const VIEW_TOGGLE_INACTIVE_COLOR = '#292929'; +const VIEW_TOGGLE_UNSELECTED_COLOR = '#3f4cbe'; +const VIEW_TOGGLE_SELECTED_COLOR = '#2c3892'; + const RESOURCE_USAGE_TYPE_READ = 'read'; const RESOURCE_USAGE_TYPE_WRITE = 'write'; const RESOURCE_USAGE_TYPE_NO_ACCESS = 'no-access'; @@ -40,6 +47,10 @@ const RESOURCE_USAGE_TYPE_READ_WRITE = 'read-write'; const IS_SUBRESOURCE_KEY = 'is_subresource_of' +// View mode constants +const VIEW_MODE_TABLE = 'table'; +const VIEW_MODE_GRAPHVIZ = 'graphviz'; + class MenuSection extends LitElement { static get properties() { return { @@ -150,19 +161,35 @@ class FrameGraphSidePanel extends LitElement { font-weight: bolder; color: ${FOREGROUND_COLOR}; } - .resource-title { - display: flex; - flex-direction: column; - margin-bottom: 5px; - font-size: ${REGULAR_FONT_SIZE}px; - color: ${UNSELECTED_COLOR}; - } .resource-content { display: flex; flex-direction: column; font-size: ${REGULAR_FONT_SIZE}px; color: ${UNSELECTED_COLOR}; } + .view-mode-selector { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: 15px; + } + .view-toggle { + display: inline-block; + background-color: ${this.connected ? VIEW_TOGGLE_UNSELECTED_COLOR:VIEW_TOGGLE_INACTIVE_COLOR}; + color: white; + padding: 5px 10px; + margin: 5px; + border-radius: 4px; + cursor: pointer; + user-select: none; + font-size: 12px; + text-align: center; + flex: 1; + } + .view-toggle.active { + background-color: ${this.connected ? VIEW_TOGGLE_SELECTED_COLOR:VIEW_TOGGLE_INACTIVE_COLOR}; + font-weight: bold; + } `; } @@ -171,6 +198,7 @@ class FrameGraphSidePanel extends LitElement { connected: {type: Boolean, attribute: 'connected'}, selectedFrameGraph: {type: String, attribute: 'selected-framegraph'}, selectedResourceId: {type: Number, attribute: 'selected-resource'}, + viewMode: {type: String, attribute: 'view-mode'}, database: {type: Object, state: true}, framegraphs: {type: Array, state: true}, @@ -182,6 +210,7 @@ class FrameGraphSidePanel extends LitElement { this.connected = false; this.framegraphs = []; this.database = {}; + this.viewMode = VIEW_MODE_TABLE; } updated(props) { @@ -199,7 +228,6 @@ class FrameGraphSidePanel extends LitElement { } } - _handleFrameGraphClick(ev) { this.dispatchEvent(new CustomEvent('select-framegraph', { detail: ev, @@ -208,41 +236,85 @@ class FrameGraphSidePanel extends LitElement { })); } + _handleViewModeClick(mode) { + this.dispatchEvent(new CustomEvent('change-view-mode', { + detail: mode, + bubbles: true, + composed: true + })); + } + _findCurrentResource() { if (!this.selectedFrameGraph) return null; - const frameGraph = this.database[this.selectedFrameGraph]; - return Object.values(frameGraph?.resources) - .find(resource => resource.id === this.selectedResourceId) || null; + + const resources = this.database[this.selectedFrameGraph]?.resources; + if (resources) { + const resource = resources[this.selectedResourceId]; + if (resource) { + return resource; + } + } + return null; } render() { const renderFrameGraphs = (title) => { - if (!this.framegraphs.length) return nothing; + if (!this.framegraphs.length) + return nothing; return html` - ${this.framegraphs.map(({ fgid, name }) => html` -
- ${fgid === this.selectedFrameGraph ? '● ' : ''}${name} + ${this.selectedFrameGraph ? html` +
+
+ Table Mode
- `)} +
+ Graph Mode +
+
+ ` : nothing} + +
+ ${this.framegraphs.map(({fgid, name}) => { + const isSelected = this.selectedFrameGraph === fgid; + return html` +
+ ${isSelected ? '● ' : ''}${name} +
`; + })} +
`; }; const renderResourceDetails = (title) => { const currentResource = this._findCurrentResource(); - if (!currentResource) return nothing; + if (!currentResource) + return nothing; return html` -
${currentResource.id}: ${currentResource.name}
- ${currentResource.properties?.map(({ key, value }) => html` -
${key}: ${value}
- `)} +
+
Name: ${currentResource.name}
+
+
+
ID: ${currentResource.id}
+
+ ${currentResource.properties.length > 0 ? html` +
+
Properties:
+
    + ${currentResource.properties.map(prop => html` +
  • ${prop.key}: ${prop.value}
  • + `)} +
+
+ ` : ''}
`; }; @@ -256,7 +328,6 @@ class FrameGraphSidePanel extends LitElement {
`; } - } customElements.define("framegraph-sidepanel", FrameGraphSidePanel); @@ -480,7 +551,8 @@ class FrameGraphTable extends LitElement { } render() { - if (!this.frameGraphData?.passes || !this.frameGraphData?.resources) return nothing; + if (!this.frameGraphData?.passes || !this.frameGraphData?.resources) + return nothing; const allPasses = this.frameGraphData.passes; const resources = Object.values(this.frameGraphData.resources); @@ -505,6 +577,82 @@ class FrameGraphTable extends LitElement { customElements.define("framegraph-table", FrameGraphTable); +class GraphvizView extends LitElement { + static get styles() { + return css` + :host { + display: block; + flex-grow: 1; + width: 80%; + } + + .graphviz-container { + margin-left: 300px; + height: 100vh; + width: calc(100% - 300px); + overflow: auto; + background-color: white; + } + + #graph { + width: 100%; + height: 100%; + } + `; + } + + static get properties() { + return { + graphvizData: {type: String, state: true}, + }; + } + + constructor() { + super(); + this.graphvizData = ''; + } + + updated(changedProps) { + if (changedProps.has('graphvizData')) { + this._renderGraphviz(); + } + } + + _renderGraphviz() { + if (!this.graphvizData) + return; + + const container = this.renderRoot.querySelector('#graphviz-view'); + if (!container) + return; + + try { + const viz = d3.select(container) + .graphviz({ useWorker: false }) + .zoom(true) + .fit(true); + + viz.renderDot(this.graphvizData); + } catch (error) { + console.error('Failed to render graphviz:', error); + container.innerHTML = `
Failed to render graphviz: ${error.message}
`; + } + } + + render() { + if(!this.graphvizData) + return nothing; + + return html` +
+
+
+ `; + } +} + +customElements.define("graphviz-view", GraphvizView); + class FrameGraphViewer extends LitElement { static get styles() { return css` @@ -523,6 +671,10 @@ class FrameGraphViewer extends LitElement { get _framegraphTable() { return this.renderRoot.querySelector('#table'); } + + get _graphvizView() { + return this.renderRoot.querySelector('#graphviz'); + } async init() { const isConnected = () => this.connected; @@ -535,6 +687,7 @@ class FrameGraphViewer extends LitElement { let fgInfo = await fetchFrameGraph(fgid); this.database[fgInfo.fgid] = fgInfo; this._framegraphTable.frameGraphData = fgInfo; + this._graphvizView.graphvizData = fgInfo.graphviz; } } ); @@ -555,6 +708,7 @@ class FrameGraphViewer extends LitElement { this.database = {}; this.selectedFrameGraph = null; this.selectedResourceId = -1; + this.viewMode = VIEW_MODE_TABLE; this.init(); this.addEventListener('select-framegraph', @@ -568,6 +722,12 @@ class FrameGraphViewer extends LitElement { this.selectedResourceId = ev.detail; } ); + + this.addEventListener('change-view-mode', + (ev) => { + this.viewMode = ev.detail; + } + ); } static get properties() { @@ -576,6 +736,7 @@ class FrameGraphViewer extends LitElement { database: {type: Object, state: true}, selectedFrameGraph: {type: String, state: true}, selectedResourceId: {type: Number, state: true}, + viewMode: {type: String, state: true}, } } @@ -583,6 +744,7 @@ class FrameGraphViewer extends LitElement { if (props.has('selectedFrameGraph') || props.has('database')) { const framegraph = this._getFrameGraph(); this._framegraphTable.frameGraphData = framegraph; + this._graphvizView.graphvizData = framegraph?.graphviz; this._sidePanel.database = this.database; } } @@ -593,13 +755,21 @@ class FrameGraphViewer extends LitElement { + selected-resource="${this.selectedResourceId}" + view-mode="${this.viewMode}"> + + + + `; } }