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:
Dion Moult
2026-08-24 08:51:21 +10:00
parent 091b4d4113
commit 6f24133d35
4 changed files with 101 additions and 24 deletions
+30
View File
@@ -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;
+14
View File
@@ -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