mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 07:49:59 +00:00
ifcviewer-web: stream getObjects() per model instead of one scene-sized JSON
getObjects() is on every real host page's path — a click hands back an object id, and resolving it to a GlobalId/name/type needs the element tables (the JS layer also builds its GUID index from this call). The implementation materialised a vector of ElementRef (three fresh std::strings per element), serialised the entire scene into one JSON string grown by +=, and UTF8ToString'd the whole thing — several hundred MB simultaneously alive at ~600k elements. The wasm heap never returns pages, so that transient became the session's permanent floor. ViewportCore::visitModelElements hands out one model's elements as slices into its string table (no per-element copies), and the export serialises straight from those, one model per batch, reusing one string whose capacity grows only to the largest model. The JS side accumulates batches and resolves the same array as before — the page API is unchanged. Peak is now one model's JSON instead of the scene's. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -195,9 +195,10 @@ int fillIdsAscending(const std::unordered_set<std::uint32_t>& ids,
|
||||
// Quote `s` as a JSON string literal. IFC names come straight from the model
|
||||
// and can hold quotes, backslashes and control characters; UTF-8 continuation
|
||||
// bytes are already legal JSON and pass through untouched.
|
||||
std::string jsonString(const std::string& s) {
|
||||
std::string out = "\"";
|
||||
for (unsigned char c : s) {
|
||||
void appendJsonString(std::string& out, const char* data, std::uint32_t length) {
|
||||
out += '"';
|
||||
for (std::uint32_t i = 0; i < length; ++i) {
|
||||
const unsigned char c = (unsigned char)data[i];
|
||||
switch (c) {
|
||||
case '"': out += "\\\""; break;
|
||||
case '\\': out += "\\\\"; break;
|
||||
@@ -216,9 +217,10 @@ std::string jsonString(const std::string& s) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return out + '"';
|
||||
out += '"';
|
||||
}
|
||||
|
||||
|
||||
NavKind classifyPress(const ViewportCore::NavBindings& b, int em_button,
|
||||
bool shift, bool ctrl, bool alt) {
|
||||
using MB = ViewportCore::MouseBtn; using M = ViewportCore::NavMod;
|
||||
@@ -822,27 +824,49 @@ extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_get_model_georef_c(int source_id, doubl
|
||||
// Promise the JS layer is holding.
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void ifcv_request_objects_c(int token) {
|
||||
if (!g_app || !g_app->ready) {
|
||||
EM_ASM({ if (Module.__ifcvOnObjects) Module.__ifcvOnObjects($0, '[]'); }, token);
|
||||
EM_ASM({ if (Module.__ifcvOnObjectsDone) Module.__ifcvOnObjectsDone($0); }, token);
|
||||
return;
|
||||
}
|
||||
g_app->core.loadAllElementMetadataWeb([token](bool) {
|
||||
// Partial failures are not fatal: a model whose element block failed to
|
||||
// fetch simply contributes no rows, and the rest still resolve.
|
||||
std::string json = "[";
|
||||
bool first = true;
|
||||
for (const ViewportCore::ElementRef& e : g_app->core.elements()) {
|
||||
if (!first) json += ',';
|
||||
first = false;
|
||||
json += "{\"objectId\":" + std::to_string(e.object_id)
|
||||
+ ",\"model\":" + std::to_string(e.model_index)
|
||||
+ ",\"sourceId\":" + std::to_string(e.source_id)
|
||||
+ ",\"guid\":" + jsonString(e.guid)
|
||||
+ ",\"name\":" + jsonString(e.name)
|
||||
+ ",\"type\":" + jsonString(e.type) + '}';
|
||||
//
|
||||
// Serialised one model per batch, straight from string-table slices.
|
||||
// The whole-scene single-string version materialised three string
|
||||
// copies per element plus a scene-sized JSON blob simultaneously —
|
||||
// a 400+ MB transient at ~600k elements, and the wasm heap never
|
||||
// returns pages, so that peak became the session's floor. Peak is
|
||||
// now one model's JSON; the string keeps its capacity across models
|
||||
// so it reallocates only up to the largest one.
|
||||
std::string json;
|
||||
const int model_count = g_app->core.streamingModelCount();
|
||||
for (int model_index = 0; model_index < model_count; ++model_index) {
|
||||
json.clear();
|
||||
json += '[';
|
||||
bool first = true;
|
||||
g_app->core.visitModelElements(model_index,
|
||||
[&](const ViewportCore::ElementSlices& e) {
|
||||
if (!first) json += ',';
|
||||
first = false;
|
||||
json += "{\"objectId\":";
|
||||
json += std::to_string(e.object_id);
|
||||
json += ",\"model\":";
|
||||
json += std::to_string(model_index);
|
||||
json += ",\"sourceId\":";
|
||||
json += std::to_string(e.source_id);
|
||||
json += ",\"guid\":";
|
||||
appendJsonString(json, e.guid, e.guid_len);
|
||||
json += ",\"name\":";
|
||||
appendJsonString(json, e.name, e.name_len);
|
||||
json += ",\"type\":";
|
||||
appendJsonString(json, e.type, e.type_len);
|
||||
json += '}';
|
||||
});
|
||||
json += ']';
|
||||
EM_ASM({ if (Module.__ifcvOnObjectsBatch) Module.__ifcvOnObjectsBatch($0, UTF8ToString($1)); },
|
||||
token, json.c_str());
|
||||
}
|
||||
json += ']';
|
||||
EM_ASM({ if (Module.__ifcvOnObjects) Module.__ifcvOnObjects($0, UTF8ToString($1)); },
|
||||
token, json.c_str());
|
||||
EM_ASM({ if (Module.__ifcvOnObjectsDone) Module.__ifcvOnObjectsDone($0); }, token);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -257,13 +257,22 @@
|
||||
// Completion side of ifcv_request_objects_c: the element tables have all
|
||||
// landed and the scene's objects are ready as JSON. `token` matches the
|
||||
// request to its pending Promise.
|
||||
// token -> {rows, resolve}. The wasm side streams the objects array one
|
||||
// model per batch (so the whole-scene JSON never exists in one string on
|
||||
// its heap); Done resolves with the accumulated rows.
|
||||
const pendingObjects = new Map();
|
||||
let objectsToken = 0;
|
||||
Module.__ifcvOnObjects = function (token, json) {
|
||||
const resolve = pendingObjects.get(token);
|
||||
if (!resolve) return;
|
||||
Module.__ifcvOnObjectsBatch = function (token, json) {
|
||||
const pending = pendingObjects.get(token);
|
||||
if (!pending) return;
|
||||
const rows = JSON.parse(json);
|
||||
for (let i = 0; i < rows.length; ++i) pending.rows.push(rows[i]);
|
||||
};
|
||||
Module.__ifcvOnObjectsDone = function (token) {
|
||||
const pending = pendingObjects.get(token);
|
||||
if (!pending) return;
|
||||
pendingObjects.delete(token);
|
||||
resolve(JSON.parse(json));
|
||||
pending.resolve(pending.rows);
|
||||
};
|
||||
|
||||
// Some test harnesses / the fullscreen page want the raw module on window.
|
||||
@@ -442,7 +451,7 @@
|
||||
getObjects: function () {
|
||||
const token = ++objectsToken;
|
||||
return new Promise(function (resolve) {
|
||||
pendingObjects.set(token, resolve);
|
||||
pendingObjects.set(token, { rows: [], resolve: resolve });
|
||||
Module._ifcv_request_objects_c(token);
|
||||
}).then(function (objects) {
|
||||
objectIndex = new Map();
|
||||
|
||||
@@ -4514,6 +4514,36 @@ std::vector<ViewportCore::ElementRef> ViewportCore::elements() const {
|
||||
return out;
|
||||
}
|
||||
|
||||
void ViewportCore::visitModelElements(
|
||||
int model_index,
|
||||
const std::function<void(const ElementSlices&)>& visit) const {
|
||||
const std::vector<std::uint32_t> ids = modelIdsInLoadOrder();
|
||||
if (model_index < 0 || std::size_t(model_index) >= ids.size()) return;
|
||||
auto it = models_gpu_.find(ids[std::size_t(model_index)]);
|
||||
if (it == models_gpu_.end()) return;
|
||||
const ModelGpuData& m = it->second;
|
||||
auto slice = [&m](std::uint32_t offset, std::uint32_t length,
|
||||
const char*& out_ptr, std::uint32_t& out_len) {
|
||||
// Out-of-range or zero-length offsets mean "no string was written".
|
||||
if (length > 0 && std::size_t(offset) + length <= m.string_table.size()) {
|
||||
out_ptr = m.string_table.data() + offset;
|
||||
out_len = length;
|
||||
} else {
|
||||
out_ptr = nullptr;
|
||||
out_len = 0;
|
||||
}
|
||||
};
|
||||
for (const ElementTableRecord& e : m.elements) {
|
||||
ElementSlices out;
|
||||
out.object_id = e.object_id;
|
||||
out.source_id = m.web_source_id;
|
||||
slice(e.guid_offset, e.guid_length, out.guid, out.guid_len);
|
||||
slice(e.name_offset, e.name_length, out.name, out.name_len);
|
||||
slice(e.type_offset, e.type_length, out.type, out.type_len);
|
||||
visit(out);
|
||||
}
|
||||
}
|
||||
|
||||
bool ViewportCore::elementForObject(std::uint32_t object_id, ElementRef& out) const {
|
||||
InstanceCompose::InstanceLookup lk;
|
||||
if (!findInstance(object_id, lk)) return false;
|
||||
|
||||
@@ -597,6 +597,20 @@ public:
|
||||
// resident. On web that means calling loadAllElementMetadataWeb first —
|
||||
// models still lazily un-fetched simply contribute nothing.
|
||||
std::vector<ElementRef> elements() const;
|
||||
// One model's elements (by load-order index, same as modelProgress),
|
||||
// handed out as slices into the model's string table — valid only for
|
||||
// the duration of the visit, no per-element string copies. The web
|
||||
// objects export serialises hundreds of thousands of elements straight
|
||||
// from these; materialising ElementRefs there tripled the peak heap.
|
||||
struct ElementSlices {
|
||||
std::uint32_t object_id = 0;
|
||||
int source_id = -1;
|
||||
const char* guid = nullptr; std::uint32_t guid_len = 0;
|
||||
const char* name = nullptr; std::uint32_t name_len = 0;
|
||||
const char* type = nullptr; std::uint32_t type_len = 0;
|
||||
};
|
||||
void visitModelElements(int model_index,
|
||||
const std::function<void(const ElementSlices&)>& visit) const;
|
||||
|
||||
// The single element behind one object_id — the pick path's lookup, which
|
||||
// must not pay for materialising the whole table. Scans only the model that
|
||||
|
||||
Reference in New Issue
Block a user