Files
filament/libs/fgviewer/web/app.js
Doris Wu 1b59e24e99 fgviewer improvement (#8450)
* Introduce FrameGraphInfo class

* Move the assignment into pimpl

* Make ctors explicit

* Add ctors to fg info structs

* Revert the macro change to align with existing

* Address the comments

* Remove pimpl and move func def to .cc

* Fix

* Convert the FrameGraph to FrameGraphInfo

* Initialize and update debug server on engine side

* Fix compile error

* Address the comments

* Update

* Use camelCase

* Use camelCase

* Update

* Add JsonWriter to convert fginfo to json

* Add getStatus api and implement it

* Add increment

* Implement GET apis

* Fix compile error

* Update

* Use c_str_safe

* Add operator== for FrameGraphInfo

* Call updateFrameGraph in appropriate locations

* Address the comments

* Add webview for fgviewer

* Remove head and tail bracket

* Fix the table element

* Force web to re-render when fg info gets updated

* Skip culled resources

* Mark subresources as gray

* Add parent info for subresources

* Collapse subresources into their parent resource

* Make the leftmost column fixed

* Remove unused stuff in html

* Fix the comment

* Refactor the code

* Cull unused resources

* Update

* Fix the naming

* Format the file

* Merge multiple resource node in to single resource

* Improve the logic and naming

* Refine the code
2025-02-18 14:46:30 +08:00

530 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed 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.
*/
import {LitElement, html, css, unsafeCSS, nothing} from "https://unpkg.com/lit@2.8.0?module";
const kUntitledPlaceholder = "untitled";
// CSS constants
const FOREGROUND_COLOR = '#fafafa';
const INACTIVE_COLOR = '#6f6f6f';
const UNSELECTED_COLOR = '#dfdfdf';
const BACKGROUND_COLOR = '#5362e5';
const REGULAR_FONT_SIZE = 12;
// Constants for color operations
const READ_COLOR = '#3ac224';
const WRITE_COLOR = '#d43232';
const NO_ACCESS_COLOR = '#d8dde754';
const READ_WRITE_COLOR = '#ffeb99';
const DEFAULT_COLOR = '#ffffff';
const SUBRESOURCE_COLOR = '#d3d3d3';
const RESOURCE_USAGE_TYPE_READ = 'read';
const RESOURCE_USAGE_TYPE_WRITE = 'write';
const RESOURCE_USAGE_TYPE_NO_ACCESS = 'no-access';
const RESOURCE_USAGE_TYPE_READ_WRITE = 'read-write';
const IS_SUBRESOURCE_KEY = 'is_subresource'
class MenuSection extends LitElement {
static get properties() {
return {
showing: {type: Boolean, state: true},
title: {type: String, attribute: 'title'},
};
}
static get styles() {
return css`
:host {
font-size: ${unsafeCSS(REGULAR_FONT_SIZE)}px;
color: ${unsafeCSS(UNSELECTED_COLOR)};
}
.section-title {
font-size: 16px;
color: ${unsafeCSS(UNSELECTED_COLOR)};
cursor: pointer;
}
.container {
margin-bottom: 20px;
}
hr {
display: block;
height: 1px;
border: 0px;
border-top: 1px solid ${unsafeCSS(UNSELECTED_COLOR)};
padding: 0;
width: 100%;
margin: 3px 0 8px 0;
}
.expander {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
`;
}
_showClick() {
this.showing = !this.showing;
}
constructor() {
super();
this.showing = true;
}
render() {
const expandedIcon = this.showing ? '-' : '+';
const slot = (() => html`
<slot></slot>`)();
return html`
<div class="container">
<div class="section-title expander" @click="${this._showClick}">
<span>${this.title}</span> <span>${expandedIcon}</span>
</div>
<hr/>
${this.showing ? slot : []}
</div>
`;
}
}
customElements.define('menu-section', MenuSection);
class FrameGraphSidePanel extends LitElement {
// Setting the style in render() has poor performance implications. We use it simply to avoid
// having another container descending from the root to host the background color.
dynamicStyle() {
return `
:host {
position: fixed;
background: ${this.connected ? BACKGROUND_COLOR : INACTIVE_COLOR};
width: 20%;
height: 100%;
max-width: 250px;
min-width: 250px;
padding: 10px 20px;
overflow-y: auto;
}
.title {
color: white;
width: 100%;
text-align: center;
margin: 0 0 10px 0;
font-size: 20px;
}
.framegraphs {
display: flex;
flex-direction: column;
margin-bottom: 20px;
font-size: ${REGULAR_FONT_SIZE}px;
color: ${UNSELECTED_COLOR};
}
.framegraph:hover {
text-decoration: underline;
}
.framegraph {
cursor: pointer;
}
.selected {
font-weight: bolder;
color: ${FOREGROUND_COLOR};
}
`;
}
static get properties() {
return {
connected: {type: Boolean, attribute: 'connected'},
currentFrameGraph: {type: String, attribute: 'current-framegraph'},
database: {type: Object, state: true},
framegraphs: {type: Array, state: true},
}
}
constructor() {
super();
this.connected = false;
this.framegraphs = [];
this.database = {};
}
updated(props) {
if (props.has('database')) {
const items = [];
// Names need not be unique, so we display a numeric suffix for non-unique names.
// To achieve stable ordering of anonymous framegraphs, we first sort by fgid.
const labels = new Set();
const fgids = Object.keys(this.database).sort();
const duplicatedLabels = {};
for (const fgid of fgids) {
const name = this.database[fgid].viewName || kUntitledPlaceholder;
if (labels.has(name)) {
duplicatedLabels[name] = 0;
} else {
labels.add(name);
}
}
this.framegraphs = fgids.map((fgid) => {
const framegraph = this.database[fgid];
let name = framegraph.viewName || kUntitledPlaceholder;
if (name in duplicatedLabels) {
const index = duplicatedLabels[name];
duplicatedLabels[name] = index + 1;
name = `${name} (${index})`;
}
return {
fgid: fgid,
name: name,
domain: "views"
};
});
}
}
_handleFrameGraphClick(ev) {
this.dispatchEvent(new CustomEvent('select-framegraph', {
detail: ev,
bubbles: true,
composed: true
}));
}
render() {
const sections = (title) => {
const fgs = this.framegraphs
.map((fg) => {
const framegraph = this.database[fg.fgid];
const onClick = this._handleFrameGraphClick.bind(this, fg.fgid);
const isFrameGraphSelected = fg.fgid === this.currentFrameGraph;
const fgName = (isFrameGraphSelected ? '● ' : '') + framegraph.viewName;
return html`
<div class="framegraph" @click="${onClick}" data-id="${fg.fgid}">
${fgName}
</div>
`;
});
if (fgs.length > 0) {
return html`
<menu-section title="${title}">${fgs}</menu-section>`;
}
return null;
};
return html`
<style>${this.dynamicStyle()}</style>
<div class="container">
<div class="title">fgviewer</div>
${sections("Views", "views") ?? nothing}
</div>
`;
}
}
customElements.define("framegraph-sidepanel", FrameGraphSidePanel);
class FrameGraphTable extends LitElement {
static get styles() {
return css`
:host {
display: block;
flex-grow: 1;
width: 80%;
}
.table-container {
max-height: 100%;
max-width: 100%;
overflow: auto;
border: 1px solid #ddd;
margin-left: 300px;
}
.scrollable-table {
width: 100%;
height: 100%;
border-collapse: collapse;
}
.collapsible {
background-color: #f9f9f9;
}
.toggle-icon {
cursor: pointer;
margin-right: 5px;
user-select: none;
}
.hidden {
display: none;
}
.scrollable-table td,
.scrollable-table th {
padding: 12px;
text-align: left;
border: 1px solid #ddd;
}
.scrollable-table tr {
position: sticky;
padding: 12px;
text-align: left;
border: 1px solid #ddd;
left: 0;
z-index: 1;
}
.sticky-col {
position: sticky;
left: 0;
z-index: 1;
}
th {
min-width: 100px;
background-color: #f2f2f2;
padding: 12px;
text-align: left;
border: 1px solid #ddd;
}
`;
}
static get properties() {
return {
frameGraphData: {type: Object, state: true}, // Expecting a JSON frame graph structure
};
}
constructor() {
super();
this.frameGraphData = null;
}
updated(props) {
if (props.has('frameGraphData')) {
this.requestUpdate();
}
}
_getCellColor(type, defaultColor) {
switch (type) {
case RESOURCE_USAGE_TYPE_READ:
return READ_COLOR;
case RESOURCE_USAGE_TYPE_WRITE:
return WRITE_COLOR;
case RESOURCE_USAGE_TYPE_NO_ACCESS:
return NO_ACCESS_COLOR;
case RESOURCE_USAGE_TYPE_READ_WRITE:
return READ_WRITE_COLOR;
default:
return defaultColor;
}
}
_toggleCollapse(resourceIndex) {
const subresourceRows = this.shadowRoot.querySelectorAll(`[id^="subresource-${resourceIndex}-"]`);
const icon = this.shadowRoot.querySelector(`#resource-${resourceIndex} .toggle-icon`);
const isHidden = subresourceRows[0]?.classList.contains('hidden');
subresourceRows.forEach(row => row.classList.toggle('hidden'));
icon.textContent = isHidden ? '▼' : '▶';
}
_getRowHtml(allPasses, resourceId, defaultColor) {
return allPasses.map((passData, index) => {
const isRead = passData?.reads.includes(resourceId);
const isWrite = passData?.writes.includes(resourceId);
let type = null;
const hasUsed = (passData) => {
return passData?.reads.includes(resourceId) || passData?.writes.includes(resourceId);
};
const hasBeenUsedBefore = allPasses.slice(0, index).some(hasUsed);
const willBeUsedLater = allPasses.slice(index + 1).some(hasUsed);
if (isRead && isWrite) type = RESOURCE_USAGE_TYPE_READ_WRITE;
else if (isRead) type = RESOURCE_USAGE_TYPE_READ;
else if (isWrite) type = RESOURCE_USAGE_TYPE_WRITE;
else if (hasBeenUsedBefore && willBeUsedLater) type = RESOURCE_USAGE_TYPE_NO_ACCESS;
return html`
<td style="background-color: ${unsafeCSS(this._getCellColor(type, defaultColor))};">
${type ?? nothing}
</td>`;
});
}
_isSubresourceOfParent(resource, parentResource){
return resource.properties?.some(prop =>
prop.key === IS_SUBRESOURCE_KEY &&
Number(prop.value) === parentResource.id)
}
render() {
if (!this.frameGraphData || !this.frameGraphData.passes || !this.frameGraphData.resources) return nothing;
const allPasses = this.frameGraphData.passes;
const resources = Object.values(this.frameGraphData.resources);
return html`
<div class="table-container">
<table class="scrollable-table">
<thead>
<tr>
<th class="sticky-col">Resources/Passes</th>
${allPasses.map(pass => html`
<th>${pass.name}</th>`)}
</tr>
</thead>
<tbody>
${resources.map((resource, resourceIndex) => {
const isSubresource = resource.properties?.some(prop => prop.key === IS_SUBRESOURCE_KEY);
if (isSubresource) return nothing;
const hasSubresources = resources.some(subresource => this._isSubresourceOfParent(subresource, resource));
return html`
<tr id="resource-${resourceIndex}">
<th class="sticky-col">
${hasSubresources ? html`
<span
class="toggle-icon"
@click="${() => this._toggleCollapse(resourceIndex)}"
>▶</span>` : nothing}
${resource.name}
</th>
${this._getRowHtml(allPasses, resource.id, DEFAULT_COLOR)}
</tr>
${resources.filter(subresource => this._isSubresourceOfParent(subresource, resource)
).map((subresource, subIndex) => html`
<tr id="subresource-${resourceIndex}-${subIndex}"
class="collapsible hidden">
<td class="sticky-col"
style="background-color: ${SUBRESOURCE_COLOR}">
${subresource.name}
</td>
${this._getRowHtml(allPasses, subresource.id, SUBRESOURCE_COLOR)}
</tr>
`)}
`;
})}
</tbody>
</table>
</div>
`;
}
}
customElements.define("framegraph-table", FrameGraphTable);
class FrameGraphViewer extends LitElement {
static get styles() {
return css`
:host {
height: 100%;
width: 100%;
display: flex;
}
`;
}
get _sidePanel() {
return this.renderRoot.querySelector('#sidepanel');
}
get _framegraphTable() {
return this.renderRoot.querySelector('#table');
}
async init() {
const isConnected = () => this.connected;
statusLoop(
isConnected,
async (status, fgid) => {
this.connected = status == STATUS_CONNECTED || status == STATUS_FRAMEGRAPH_UPDATED;
if (status == STATUS_FRAMEGRAPH_UPDATED) {
let fgInfo = await fetchFrameGraph(fgid);
this.database[fgInfo.fgid] = fgInfo;
this._framegraphTable.frameGraphData = fgInfo;
}
}
);
let framegraphs = await fetchFrameGraphs();
this.database = framegraphs;
}
_getFrameGraph() {
const framegraph = (this.database && this.currentFrameGraph) ?
this.database[this.currentFrameGraph] : null;
return framegraph;
}
constructor() {
super();
this.connected = false;
this.database = {};
this.currentFrameGraph = null;
this.init();
this.addEventListener('select-framegraph',
(ev) => {
this.currentFrameGraph = ev.detail;
}
);
}
static get properties() {
return {
connected: {type: Boolean, state: true},
database: {type: Object, state: true},
currentFrameGraph: {type: String, state: true},
}
}
updated(props) {
if (props.has('currentFrameGraph') || props.has('database')) {
const framegraph = this._getFrameGraph();
this._framegraphTable.frameGraphData = framegraph;
this._sidePanel.database = this.database;
}
}
render() {
return html`
<framegraph-sidepanel id="sidepanel"
?connected="${this.connected}"
current-framegraph="${this.currentFrameGraph}" >
</framegraph-sidepanel>
<framegraph-table id="table"
?connected="${this.connected}"
current-framegraph="${this.currentFrameGraph}"
</framegraph-table>
`;
}
}
customElements.define("framegraph-viewer", FrameGraphViewer);