Files
IfcOpenShell/src/serializers/rocks_db_serializer.cpp
T
Dion Moult a92bebd73e ifcparse: lazy loading, opt in with file::lazy_loading() / open(lazy=True)
A lazy open reads the DATA section once with the tokenizer's index
policy and builds what indexes the file: a shell per instance (name and
declaration, no attribute array), the complete inverse index with
attribute indices, the GlobalId map and the by-type lists. No attribute
value is decoded. The first time an instance's attributes are touched,
ensure_loaded() seeks the retained paged reader to the instance and runs
the same load_attributes() the full parse runs, with inverse registration
off, then resolves that instance's references from its own slots. A
modified instance is materialised first, so writing works.

There is no scanner of its own: the index pass consumes next<index_tokens>()
and counts parentheses and commas on the operator tokens; a keyword where
an instance should start, or a token the tokenizer rejects, stops the
index and the file is parsed in full. The offset of each instance's
attribute list is kept in one sorted vector that exists only in lazy
mode, so a full parse pays nothing for it. Materialising from several
threads at once is not safe.

TXG 58 MB / 210_King 147 MB / OKgate22 231 MB, single thread: lazy open
0.61 / 1.73 / 2.86 s against the full parse's 1.05 / 2.69 / 4.99 s, at
141 / 374 / 534 MB against 274 / 654 / 1036 MB; reading one attribute of
every instance afterwards costs a further 0.56 / 1.44 / 4.86 s.

This commit was written by an AI coding tool and has not been verified by
a human.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wcN7XquTfUi4vsKQ4KchL
2026-09-14 19:34:08 +10:00

320 lines
10 KiB
C++

#ifdef IFOPSH_WITH_ROCKSDB
#include "rocks_db_serializer.h"
#include <rocksdb/options.h>
#include <cstdint>
#include <cstring>
#include "../ifcparse/logger.h"
RocksDbSerializer::RocksDbSerializer(const std::string& input_filename, const std::string& rocksdb_filename, const std::vector<std::string>& skip_supertypes, ifcopenshell::logger* logger)
: input_filename_(input_filename)
, rocksdb_filename_(rocksdb_filename)
, skip_supertypes_(skip_supertypes)
{
}
namespace {
// @nb copied from instance_data.cpp but operating on unresolved instances
bool serialize(std::string& val, const ifcopenshell::reference_or_simple_type& t)
{
auto s = sizeof(size_t);
val.resize(s + 2);
val[0] = ifcopenshell::type_encoder::encode_type<express::base>();
// 1 = entity - stored by id (entity name)
// 2 = type - stored by identity (internal counter in class)
val[1] = t.index() == 0 ? 'i' : 't';
size_t iden;
if (auto* name = std::get_if<ifcopenshell::instance_reference>(&t)) {
iden = *name;
} else if (auto* inst = std::get_if<express::base>(&t)) {
iden = (*inst).identity();
}
memcpy(val.data() + 2, &iden, s);
return true;
}
bool serialize(std::string& val, const std::vector<ifcopenshell::reference_or_simple_type>& t)
{
// no attempt at alignment
val.resize(t.size() * (sizeof(size_t) + 1) + 1);
val[0] = ifcopenshell::type_encoder::encode_type<std::vector<express::base>>();
char* ptr = val.data() + 1;
for (auto it = t.begin(); it != t.end(); ++it) {
*ptr = it->index() == 0 ? 'i' : 't';
ptr++;
size_t iden = 0;
if (auto* name = std::get_if<ifcopenshell::instance_reference>(&*it)) {
iden = *name;
} else if (auto* inst = std::get_if<express::base>(&*it)) {
iden = (*inst).identity();
}
memcpy(ptr, &iden, sizeof(size_t));
ptr += sizeof(size_t);
}
return true;
}
bool serialize(std::string& val, const std::vector<std::vector<ifcopenshell::reference_or_simple_type>>& t)
{
std::ostringstream oss;
oss.put(ifcopenshell::type_encoder::encode_type<std::vector<std::vector<express::base>>>());
auto write_size = [&oss](size_t sz) {
std::string size_str;
size_str.resize(sizeof(size_t));
memcpy(size_str.data(), &sz, sizeof(size_t));
oss.write(size_str.data(), size_str.size());
};
// write_size(t.size());
for (auto it = t.begin(); it != t.end(); ++it) {
// size of inner aggregate
write_size(it->size() * 9);
// values
for (auto jt = it->begin(); jt != it->end(); ++jt) {
char c = jt->index() == 0 ? 'i' : 't';
oss.put(c);
size_t iden = 0;
if (auto* name = std::get_if<ifcopenshell::instance_reference>(&*jt)) {
iden = *name;
} else if (auto* inst = std::get_if<express::base>(&*jt)) {
iden = (*inst).identity();
}
std::string iden_str;
iden_str.resize(sizeof(size_t));
memcpy(iden_str.data(), &iden, sizeof(size_t));
oss.write(iden_str.data(), iden_str.size());
}
}
val = oss.str();
return true;
}
}
namespace {
template <typename T>
std::string to_string_fixed_width(const T& t, size_t w) {
// @todo currently inactive
std::ostringstream oss;
oss << /*std::setfill('0') << std::setw(w) <<*/ t;
return oss.str();
}
}
void RocksDbSerializer::write_streaming_() {
ifcopenshell::impl::rocks_db_file_storage storage(rocksdb_filename_, nullptr);
std::string tmp;
// Resolved lazily from the first non-header declaration encountered, because
// the schema is only known once the header has been read.
const ifcopenshell::declaration* ifcroot_type = nullptr;
ifcopenshell::instance_streamer streamer(input_filename_);
// We do not want to coerce attribute counts here, because we want
// to store exactly what is in the file for validation purposes
streamer.coerce_attribute_count = false;
while (streamer) {
auto inst = streamer.read_instance();
if (inst) {
// name can be zero in case of header instances
auto name = std::get<0>(*inst);
const auto* decl = std::get<1>(*inst);
const auto& data = std::get<2>(*inst);
const bool is_header = decl->schema() == &Header_section_schema::get_schema();
if (!is_header && decl->as_entity() && !skip_supertypes_.empty()) {
bool skip = false;
for (const auto& super : skip_supertypes_) {
if (decl->is(super)) {
skip = true;
break;
}
}
if (skip) {
streamer.references().clear();
streamer.inverses().clear();
continue;
}
}
std::vector<express::base> simple_type_instances;
data->ensure_loaded();
for (size_t i = 0; i < data->storage_->size(); i++) {
auto val = data->get_attribute_value(i);
val.apply_visitor([&](const auto& t) {
using T = std::decay_t<decltype(t)>;
if constexpr (std::is_same_v<T, express::base>) {
// instance is per definition a simple type here, because instance
// references are not resolved yet, but provided in vector of
// references
simple_type_instances.push_back(t);
}
ifcopenshell::rocks_db_attribute_storage{}.set(&storage, decl, name, i, t);
});
}
std::set<size_t> type_identities_wrote_as_refs;
for (auto& p : streamer.references()) {
// @nb cast to int in order not be interpreted as a char when appending to string
int index = p.first.index_;
auto key = (is_header ? "h|" : (decl->as_entity() ? "i|" : "t|")) +
(is_header ? decl->name() : std::to_string(p.first.name_)) + "|" +
std::to_string(index);
if (storage.db->Get(storage.ropts, key, &tmp) == rocksdb::Status::OK() && tmp.size() == (sizeof(size_t) + 2) && tmp[0] == ifcopenshell::type_encoder::encode_type<express::base>() && tmp[1] == 't')
{
size_t iden;
memcpy(&iden, tmp.data() + 2, sizeof(size_t));
key = "t|" + std::to_string(iden) + "|0";
type_identities_wrote_as_refs.insert(iden);
}
std::visit([&](const auto& v) {
serialize(tmp, v);
using T = std::decay_t<decltype(v)>;
if constexpr (std::is_same_v<T, ifcopenshell::reference_or_simple_type>) {
if (auto* inst = std::get_if<express::base>(&v)) {
// So this never happens?
simple_type_instances.push_back(*inst);
}
} else if constexpr (std::is_same_v<T, std::vector<ifcopenshell::reference_or_simple_type>>) {
for (auto const& inner : v) {
if (auto* inst = std::get_if<express::base>(&inner)) {
simple_type_instances.push_back(*inst);
}
}
} else if constexpr (std::is_same_v<T, std::vector<std::vector<ifcopenshell::reference_or_simple_type>>>) {
for (auto const& inner : v) {
for (auto const& innermost : inner) {
if (auto* inst = std::get_if<express::base>(&innermost)) {
simple_type_instances.push_back(*inst);
}
}
}
}
}, p.second);
storage.db->Put(
storage.wopts,
key, tmp);
auto write_inverse = [&](const ifcopenshell::reference_or_simple_type& v) {
if (auto* ref = std::get_if<ifcopenshell::instance_reference>(&v)) {
auto key = "v|" + to_string_fixed_width(*ref, 10) + "|" + to_string_fixed_width(decl->index_in_schema(), 4) + "|" + to_string_fixed_width(index, 2);
static std::string s;
uint32_t vv = name;
s.resize(sizeof(uint32_t));
memcpy(s.data(), &vv, sizeof(uint32_t));
storage.db->Merge(
storage.wopts, key, s);
}
};
std::visit([&](auto const& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, ifcopenshell::reference_or_simple_type>) {
write_inverse(val);
} else if constexpr (std::is_same_v<T, std::vector<ifcopenshell::reference_or_simple_type>>) {
std::for_each(val.begin(), val.end(), write_inverse);
} else if constexpr (std::is_same_v<T, std::vector<std::vector<ifcopenshell::reference_or_simple_type>>>) {
for (auto const& inner : val) {
std::for_each(inner.begin(), inner.end(), write_inverse);
}
}
}, p.second);
}
for (const auto& inst : simple_type_instances) {
std::string s(sizeof(size_t), ' ');
size_t v = inst.declaration().index_in_schema();
memcpy(s.data(), &v, sizeof(size_t));
storage.db->Put(
storage.wopts,
(inst.declaration().as_entity() ? "i|" : "t|") + std::to_string(inst.identity()) + "|_", s);
if (type_identities_wrote_as_refs.find(inst.identity()) != type_identities_wrote_as_refs.end()) {
// already written as reference, skip
// only applies to the value though, the type declaration still needs to be written
continue;
}
auto val = inst.get_attribute_value(0);
// @todo if statement?
// if (val.array_.storage_ptr->size() > 0) {
val.apply_visitor([&](const auto& t) {
ifcopenshell::rocks_db_attribute_storage{}.set(&storage, &inst.declaration(), inst.identity(), 0, t);
});
// }
// @nb we also need to delete them
// not anymore, as they are now registered as unique_ptr in the in_memory_file_storage
// delete inst;
}
// Entity type as numeric ref to index_in_schema
if (!is_header) {
std::string s(sizeof(size_t), ' ');
size_t v = decl->index_in_schema();
memcpy(s.data(), &v, sizeof(size_t));
storage.db->Put(
storage.wopts,
(decl->as_entity() ? "i|" : "t|") + std::to_string(name) + "|_", s);
{
size_t v = name;
std::string s(sizeof(size_t), ' ');
memcpy(s.data(), &v, sizeof(size_t));
storage.db->Merge(storage.wopts, "t|" + std::to_string(decl->index_in_schema()), s);
}
// GlobalId as numeric ref to instance name, so that the guid map in
// rocks_db_file_storage can resolve by_guid() lookups.
if (ifcroot_type == nullptr) {
ifcroot_type = decl->schema()->declaration_by_name("IfcRoot");
}
if (decl->is(*ifcroot_type)) {
// @nb attribute counts are not coerced, so the attribute may be absent
data->ensure_loaded();
const bool has_guid = data->storage_->size() > 0 && data->get_attribute_value(0).type() == ifcopenshell::Argument_STRING;
if (has_guid) {
size_t v = name;
std::string s(sizeof(size_t), ' ');
memcpy(s.data(), &v, sizeof(size_t));
storage.db->Put(storage.wopts, "g|" + (std::string)data->get_attribute_value(0), s);
} else {
ifcopenshell::logger::root().error("Instance #" + std::to_string(name) + " has no GlobalId, omitted from guid index");
}
}
}
streamer.references().clear();
streamer.inverses().clear();
}
}
}
void RocksDbSerializer::finalize() {
write_streaming_();
}
#endif