Skip to content

Commit

Permalink
matdbg: fix deadlock and add experimental UI (#7275)
Browse files Browse the repository at this point in the history
 - Ensure that waiting on lock times out so that we don't lock
   up a thread when the client is gone.
 - Add an experimental folder to matdbg/web/ for the new
   UI work.
  • Loading branch information
poweifeng authored Oct 24, 2023
1 parent d3016ad commit deb3eb0
Show file tree
Hide file tree
Showing 6 changed files with 959 additions and 9 deletions.
17 changes: 11 additions & 6 deletions libs/matdbg/src/ApiHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@
#include <CivetServer.h>

#include <sstream>
#include <chrono>

namespace filament::matdbg {

using namespace filament::backend;
using namespace std::chrono_literals;

namespace {

Expand Down Expand Up @@ -227,14 +229,17 @@ bool ApiHandler::handleGetStatus(struct mg_connection* conn,
return true;
}

{
std::unique_lock<utils::Mutex> lock(mStatusMutex);
uint64_t const currentStatusCount = mCurrentStatus;
mStatusCondition.wait(lock,
[this, currentStatusCount] { return currentStatusCount < mCurrentStatus; });

std::unique_lock<utils::Mutex> lock(mStatusMutex);
uint64_t const currentStatusCount = mCurrentStatus;
if (mStatusCondition.wait_for(lock, 10s,
[this, currentStatusCount] { return currentStatusCount < mCurrentStatus; })) {
mg_printf(conn, kSuccessHeader.data(), "application/txt");
mg_write(conn, statusMaterialId, 8);
} else {
mg_printf(conn, kSuccessHeader.data(), "application/txt");
// Use '1' to indicate a no-op. This ensures that we don't block forever if the client is
// gone.
mg_write(conn, "1", 1);
}
return true;
}
Expand Down
33 changes: 32 additions & 1 deletion libs/matdbg/src/DebugServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ using utils::FixedCapacityVector;
// serves files directly from the source code tree.
#define SERVE_FROM_SOURCE_TREE 0

// When set to 1, we will serve an experimental frontend, which will potentially replace the current
// frontend when ready.
#define EXPERIMENTAL_WEB_FRAMEWORK 0

#if !SERVE_FROM_SOURCE_TREE
#include "matdbg_resources.h"
#endif
Expand All @@ -70,10 +74,36 @@ std::string_view const DebugServer::kErrorHeader =
"HTTP/1.1 404 Not Found\r\nContent-Type: %s\r\n"
"Connection: close\r\n\r\n";

#if EXPERIMENTAL_WEB_FRAMEWORK

namespace {

}// namespace
std::string const BASE_URL = "libs/matdbg/web/experiment";

} // anonymous

class FileRequestHandler : public CivetHandler {
public:
FileRequestHandler(DebugServer* server) : mServer(server) {}
bool handleGet(CivetServer *server, struct mg_connection *conn) {
auto const& kSuccessHeader = DebugServer::kSuccessHeader;
struct mg_request_info const* request = mg_get_request_info(conn);
std::string uri(request->request_uri);
if (uri == "/") {
uri = "/index.html";
}
if (uri == "/index.html" || uri == "/app.js" || uri == "/api.js") {
mg_send_file(conn, (BASE_URL + uri).c_str());
return true;
}
slog.e << "DebugServer: bad request at line " << __LINE__ << ": " << uri << io::endl;
return false;
}
private:
DebugServer* mServer;
};

#else
class FileRequestHandler : public CivetHandler {
public:
FileRequestHandler(DebugServer* server) : mServer(server) {}
Expand Down Expand Up @@ -115,6 +145,7 @@ class FileRequestHandler : public CivetHandler {
private:
DebugServer* mServer;
};
#endif

DebugServer::DebugServer(Backend backend, int port) : mBackend(backend) {
#if !SERVE_FROM_SOURCE_TREE
Expand Down
129 changes: 129 additions & 0 deletions libs/matdbg/web/experiment/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright (C) 2023 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.
*/

// api.js encapsulates all of the REST endpoints that the server provides

async function _fetchJson(uri) {
const response = await fetch(uri);
return await response.json();
}

async function _fetchText(uri) {
const response = await fetch(uri);
return await response.text();
}

async function fetchShaderCode(matid, backend, language, index) {
let query;
switch (backend) {
case "opengl":
query = `type=${language}&glindex=${index}`;
break;
case "vulkan":
query = `type=${language}&vkindex=${index}`;
break;
case "metal":
query = `type=${language}&metalindex=${index}`;
break;
}
return await _fetchText(`api/shader?matid=${matid}&${query}`);
}

async function fetchMaterials() {
const matJson = await _fetchJson("api/materials")
const ret = {};
for (const matInfo of matJson) {
ret[matInfo.matid] = matInfo;
}
return ret;
}

async function fetchMaterial(matId) {
const matInfo = await _fetchJson(`api/material?matid=${matid}`);
matInfo.matid = matid;
return matInfo;
}

async function fetchMatIds() {
const matInfo = await _fetchJson("api/matids");
const ret = [];
for (matid of matInfo) {
ret.push(matid);
}
return ret;
}

async function queryActiveShaders() {
const activeMaterials = await _fetchJson("api/active");
const actives = {};
for (matid in activeMaterials) {
const backend = activeMaterials[matid][0];
const variants = activeMaterials[matid].slice(1);
actives[matid] = {
backend, variants
};
}
return actives;
}

function rebuildMaterial(materialId, backend, shaderIndex, editedText) {
let api = 0;
switch (backend) {
case "opengl": api = 1; break;
case "vulkan": api = 2; break;
case "metal": api = 3; break;
}
return new Promise((ok, fail) => {
const req = new XMLHttpRequest();
req.open('POST', '/api/edit');
req.send(`${materialId} ${api} ${shaderIndex} ${editedText}`);
req.onload = ok;
req.onerror = fail;
});
}

function activeShadersLoop(isConnected, onActiveShaders) {
setInterval(async () => {
if (isConnected()) {
onActiveShaders(await queryActiveShaders());
}
}, 1000);
}

const STATUS_LOOP_TIMEOUT = 3000;

const STATUS_CONNECTED = 1;
const STATUS_DISCONNECTED = 2;
const STATUS_MATERIAL_UPDATED = 3;

// Status function should be of the form function(status, data)
async function statusLoop(isConnected, onStatus) {
// This is a hanging get except for when transition from disconnected to connected, which
// should return immediately.
try {
const matid = await _fetchText("api/status" + (isConnected() ? '' : '?firstTime'));
// A first-time request returned successfully
if (matid === '0') {
onStatus(STATUS_CONNECTED);
} else if (matid !== '1') {
onStatus(STATUS_MATERIAL_UPDATED, matid);
} // matid == '1' is no-op, just loop again
statusLoop(isConnected, onStatus);
} catch {
onStatus(STATUS_DISCONNECTED);
setTimeout(() => statusLoop(isConnected, onStatus), STATUS_LOOP_TIMEOUT)
}
}
Loading

0 comments on commit deb3eb0

Please sign in to comment.