mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-08 17:01:40 +00:00
Follow up after 4d688170e0
This commit is contained in:
@@ -299,20 +299,44 @@ def guess_format(path: Path) -> Literal[".ifc", ".ifcZIP", ".ifcXML", ".ifcJSON"
|
||||
return None
|
||||
|
||||
|
||||
def stream2(path: Union[Path, str]):
|
||||
def stream2(path: Union[Path, str], mmap: bool = False, page_size: int = 0):
|
||||
"""Streams the content of a file path from disk, yielding each instance
|
||||
as a dictionary.
|
||||
|
||||
Args:
|
||||
path (Union[Path, str]): input file path
|
||||
mmap (bool): open the file contents using memory mapping
|
||||
page_size (int): open file in python and feed chunks to the parser
|
||||
|
||||
Yields:
|
||||
dict: entity instance dictionaries
|
||||
"""
|
||||
streamer = ifcopenshell_wrapper.InstanceStreamer(str(path))
|
||||
while streamer:
|
||||
if inst := streamer.read_instance_py():
|
||||
yield inst
|
||||
if page_size:
|
||||
import builtins
|
||||
|
||||
f = builtins.open(path, encoding="ascii")
|
||||
strm = ifcopenshell_wrapper.InstanceStreamer()
|
||||
strm.pushPage(f.read(page_size))
|
||||
finished = False
|
||||
while True:
|
||||
while strm.hasSemicolon():
|
||||
if inst := strm.readInstancePy():
|
||||
yield inst
|
||||
else:
|
||||
finished = True
|
||||
break
|
||||
if finished:
|
||||
break
|
||||
else:
|
||||
if data := f.read(page_size):
|
||||
strm.pushPage(data)
|
||||
else:
|
||||
break
|
||||
else:
|
||||
streamer = ifcopenshell_wrapper.InstanceStreamer(str(path), mmap)
|
||||
while streamer:
|
||||
if inst := streamer.readInstancePy():
|
||||
yield inst
|
||||
|
||||
|
||||
def stream2_from_string(data: str):
|
||||
|
||||
@@ -32,6 +32,11 @@ def test_stream():
|
||||
"value": ({"ref": 136}, {"ref": 138}),
|
||||
}
|
||||
|
||||
def test_chunked_stream():
|
||||
assert list(ifcopenshell.stream2(fn)) == list(ifcopenshell.stream2(fn, page_size=1024))
|
||||
|
||||
def test_mmaped_stream():
|
||||
assert list(ifcopenshell.stream2(fn)) == list(ifcopenshell.stream2(fn, mmap=True))
|
||||
|
||||
def test_file():
|
||||
f = ifcopenshell.open(fn)
|
||||
|
||||
+26
-28
@@ -11,11 +11,8 @@
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
|
||||
// Boost iostreams mmap
|
||||
#include <boost/iostreams/device/mapped_file.hpp>
|
||||
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
#if defined(_WIN32)
|
||||
@@ -32,8 +29,6 @@ namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
// ===================== Concrete backends =====================
|
||||
|
||||
using namespace IfcParse;
|
||||
|
||||
struct FullBufferImpl final : FileReader::Impl {
|
||||
@@ -64,7 +59,7 @@ struct PagedFileImpl final : FileReader::Impl {
|
||||
|
||||
// LRU cache
|
||||
size_t capacity_ = 8;
|
||||
mutable std::list<size_t> lru_; // most recent at front
|
||||
mutable std::list<size_t> lru_;
|
||||
struct Entry {
|
||||
FileReader::Page page;
|
||||
std::list<size_t>::iterator it;
|
||||
@@ -90,16 +85,14 @@ struct PagedFileImpl final : FileReader::Impl {
|
||||
char get(size_t pos) const override {
|
||||
if (pos >= file_size_) throw std::out_of_range("get out of range");
|
||||
const size_t pidx = pos / page_size_;
|
||||
const FileReader::Page& p = fetch_page_(pidx);
|
||||
const FileReader::Page& p = fetchPage_(pidx);
|
||||
const size_t off = pos % page_size_;
|
||||
if (off >= p.data.size()) throw std::out_of_range("offset beyond valid page bytes");
|
||||
// Opportunistic read-ahead for sequential scans
|
||||
// if (off + 1 == p.data.size()) (void)try_prefetch_(pidx + 1);
|
||||
return p.data[off];
|
||||
}
|
||||
|
||||
private:
|
||||
const FileReader::Page& fetch_page_(size_t idx) const {
|
||||
const FileReader::Page& fetchPage_(size_t idx) const {
|
||||
auto it = map_.find(idx);
|
||||
if (it != map_.end()) {
|
||||
touch_(it);
|
||||
@@ -116,7 +109,8 @@ private:
|
||||
const size_t nread = std::fread(pg.data.data(), 1, avail, fp_);
|
||||
if (nread != avail) throw std::runtime_error("Short fread on page");
|
||||
}
|
||||
pg.data.resize(avail); // trim to actual size
|
||||
// trim to actual size
|
||||
pg.data.resize(avail);
|
||||
|
||||
// Insert into LRU
|
||||
if (map_.size() >= capacity_) evict_();
|
||||
@@ -127,16 +121,6 @@ private:
|
||||
return emplaced_it->second.page;
|
||||
}
|
||||
|
||||
/*
|
||||
bool try_prefetch_(size_t idx) const {
|
||||
if (idx * page_size_ >= file_size_) return false;
|
||||
if (map_.find(idx) != map_.end()) return true;
|
||||
if (map_.size() + 1 > capacity_) return false;
|
||||
(void)fetch_page_(idx);
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
|
||||
void touch_(typename std::unordered_map<size_t, Entry>::iterator it) const {
|
||||
lru_.erase(it->second.it);
|
||||
lru_.push_front(it->first);
|
||||
@@ -189,7 +173,7 @@ struct PushedSequentialImpl final : std::enable_shared_from_this<PushedSequentia
|
||||
}
|
||||
|
||||
// Drop fully-consumed pages so pos is guaranteed to be within the first page
|
||||
void drop_consumed_up_to(size_t pos) {
|
||||
void dropPages(size_t pos) override {
|
||||
while (!pages_.empty()) {
|
||||
if (pos - discarded_page_bytes_ >= pages_.front().data.size()) {
|
||||
discarded_page_bytes_ += pages_.front().data.size();
|
||||
@@ -202,11 +186,17 @@ struct PushedSequentialImpl final : std::enable_shared_from_this<PushedSequentia
|
||||
|
||||
char get(size_t pos) const override {
|
||||
auto self = const_cast<PushedSequentialImpl*>(this);
|
||||
|
||||
/*
|
||||
// We do not do this automatically because all variable width tokens:
|
||||
// ENUM/STRING/BINARY/KEYWORD are stored as file offsets until a full
|
||||
// entity instance is finalized.
|
||||
if (this->shared_from_this().use_count() == 2) {
|
||||
// only drop pages when there is only one active client.
|
||||
// NB this->shared_from_this() increases count by 1
|
||||
self->drop_consumed_up_to(pos);
|
||||
}
|
||||
*/
|
||||
|
||||
const size_t avail_end = size();
|
||||
if (pos >= avail_end) throw std::out_of_range("pushed backend: position not committed yet");
|
||||
@@ -226,14 +216,12 @@ struct PushedSequentialImpl final : std::enable_shared_from_this<PushedSequentia
|
||||
throw std::out_of_range("pushed backend: internal inconsistency");
|
||||
}
|
||||
|
||||
void push_next_page(const std::string& data) override {
|
||||
void pushNextPage(const std::string& data) override {
|
||||
FileReader::Page p; p.data.assign(data.data(), data.data() + data.size());
|
||||
pages_.push_back(std::move(p));
|
||||
}
|
||||
};
|
||||
|
||||
// ===================== FileReader public API =====================
|
||||
|
||||
IfcParse::FileReader::FileReader(const std::string& fn)
|
||||
: cursor_(0)
|
||||
{
|
||||
@@ -255,7 +243,7 @@ IfcParse::FileReader::FileReader(const caller_fed_tag&)
|
||||
IfcParse::FileReader::FileReader(const std::string& content, const caller_fed_tag&)
|
||||
{
|
||||
impl_ = std::make_shared<PushedSequentialImpl>();
|
||||
impl_->push_next_page(content);
|
||||
impl_->pushNextPage(content);
|
||||
}
|
||||
|
||||
IfcParse::FileReader::FileReader(const std::string& fn, size_t page_size, size_t page_capacity)
|
||||
@@ -289,9 +277,19 @@ void FileReader::increment(size_t n) {
|
||||
cursor_ += n;
|
||||
}
|
||||
|
||||
void IfcParse::FileReader::push_next_page(const std::string& data)
|
||||
void IfcParse::FileReader::pushNextPage(const std::string& data)
|
||||
{
|
||||
impl_->push_next_page(data);
|
||||
impl_->pushNextPage(data);
|
||||
}
|
||||
|
||||
void IfcParse::FileReader::dropPages()
|
||||
{
|
||||
impl_->dropPages(0);
|
||||
}
|
||||
|
||||
void IfcParse::FileReader::dropPages(size_t up_to_pos)
|
||||
{
|
||||
impl_->dropPages(up_to_pos);
|
||||
}
|
||||
|
||||
bool IfcParse::FileReader::eof() const
|
||||
|
||||
+11
-58
@@ -29,61 +29,6 @@
|
||||
|
||||
#include "ifc_parse_api.h"
|
||||
|
||||
/*
|
||||
#include <string>
|
||||
|
||||
#ifdef USE_MMAP
|
||||
#include <boost/iostreams/device/mapped_file.hpp>
|
||||
#endif
|
||||
|
||||
namespace IfcParse {
|
||||
/// The FileReader class represents a ISO 10303-21 IFC-SPF file in memory.
|
||||
/// The file is interpreted as a sequence of tokens which are lazily
|
||||
/// interpreted only when requested.
|
||||
class IFC_PARSE_API FileReader {
|
||||
private:
|
||||
#ifdef USE_MMAP
|
||||
boost::iostreams::mapped_file_source mfs;
|
||||
#endif
|
||||
FILE* stream_;
|
||||
const char* buffer_;
|
||||
size_t ptr_;
|
||||
size_t len_;
|
||||
size_t buf_size_;
|
||||
size_t ptr_offset_ = 0;
|
||||
|
||||
public:
|
||||
bool valid;
|
||||
bool eof;
|
||||
size_t size;
|
||||
|
||||
FileReader(const std::string& path, bool mmap = false, size_t buf_size = 0);
|
||||
FileReader(std::istream& stream, int length);
|
||||
FileReader(void* data, int length);
|
||||
~FileReader();
|
||||
/// Returns the character at the cursor
|
||||
char peek();
|
||||
/// Returns the character at specified offset
|
||||
char Read(size_t offset);
|
||||
/// Increment the file cursor and reads new page if necessary
|
||||
void increment();
|
||||
void Close();
|
||||
/// Moves the file cursor to an arbitrary offset in the file
|
||||
void seek(size_t offset);
|
||||
/// Returns the cursor position
|
||||
size_t Tell() const;
|
||||
|
||||
bool is_eof_at(size_t) const;
|
||||
void increment_at(size_t&);
|
||||
char peek_at(size_t);
|
||||
|
||||
operator bool() const { return valid && !eof; }
|
||||
};
|
||||
} // namespace IfcParse
|
||||
|
||||
#endif
|
||||
*/
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@@ -147,8 +92,13 @@ public:
|
||||
|
||||
/// \brief Push the next sequential page (pushed backend only).
|
||||
/// \param data Contents of the page.
|
||||
/// \throws std::logic_error if the current backend is not pushed mode, or if a next page is already queued.
|
||||
void push_next_page(const std::string& data);
|
||||
/// \throws std::logic_error if the current backend is not in pushed mode
|
||||
void pushNextPage(const std::string& data);
|
||||
|
||||
/// \brief Drops pages up to cursor position or provided offset. Does nothing when current backend is not in pushed mode
|
||||
/// \param up_to_pos Pages with an end offset before up_to_pos are dropped from memory
|
||||
void dropPages();
|
||||
void dropPages(size_t up_to_pos);
|
||||
|
||||
/// \brief Returns true if the cursor is at or beyond the end of available data.
|
||||
/// For the pushed backend, EOF means all pushed bytes have been consumed.
|
||||
@@ -165,9 +115,12 @@ public:
|
||||
virtual size_t size() const = 0;
|
||||
virtual char get(size_t pos) const = 0;
|
||||
/// \brief Backend may support pushing pages; default throws.
|
||||
virtual void push_next_page(const std::string&) {
|
||||
virtual void pushNextPage(const std::string&) {
|
||||
throw std::logic_error("push_next_page: backend does not support pushed mode");
|
||||
}
|
||||
virtual void dropPages(size_t) {
|
||||
// empty on purpose
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
@@ -651,9 +651,7 @@ IfcParse::filetype IfcParse::guess_file_type(const std::string& fn) {
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::tuple<size_t, const IfcParse::declaration*, IfcEntityInstanceData>> IfcParse::InstanceStreamer::read_instance() {
|
||||
// std::cout << "global: " << stream_->tell() << std::endl;
|
||||
|
||||
std::optional<std::tuple<size_t, const IfcParse::declaration*, IfcEntityInstanceData>> IfcParse::InstanceStreamer::readInstance() {
|
||||
std::optional<std::tuple<size_t, const IfcParse::declaration*, IfcEntityInstanceData>> return_value;
|
||||
|
||||
if (header_ && yielded_header_instances_ < 3) {
|
||||
@@ -741,11 +739,12 @@ std::optional<std::tuple<size_t, const IfcParse::declaration*, IfcEntityInstance
|
||||
break;
|
||||
}
|
||||
|
||||
// std::cout << next_token.startPos << " " << TokenFunc::toString(next_token) << std::endl;
|
||||
|
||||
token_stream_.push_back(next_token);
|
||||
}
|
||||
|
||||
// Free pages in front of cursor when variable-width tokens are materialized into entity instance data objects
|
||||
(stream_ ? stream_ : (lexer_)->stream)->dropPages();
|
||||
|
||||
return return_value;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,13 +126,13 @@ public:
|
||||
return storage_.byref_excl_;
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<IfcUtil::IfcBaseClass>> steal_instances() {
|
||||
std::vector<std::unique_ptr<IfcUtil::IfcBaseClass>> stealInstances() {
|
||||
return storage_.steal_instances();
|
||||
}
|
||||
|
||||
bool has_semicolon() const;
|
||||
bool hasSemicolon() const;
|
||||
|
||||
void push_page(const std::string& page);
|
||||
void pushPage(const std::string& page);
|
||||
|
||||
InstanceStreamer();
|
||||
|
||||
@@ -150,7 +150,7 @@ public:
|
||||
delete header_;
|
||||
}
|
||||
|
||||
std::optional<std::tuple<size_t, const IfcParse::declaration*, IfcEntityInstanceData>> read_instance();
|
||||
std::optional<std::tuple<size_t, const IfcParse::declaration*, IfcEntityInstanceData>> readInstance();
|
||||
};
|
||||
|
||||
/// This class provides access to the entity instances in an IFC file
|
||||
|
||||
+18
-17
@@ -336,11 +336,6 @@ Token IfcParse::GeneralTokenPtr(IfcSpfLexer* lexer, size_t start, const std::str
|
||||
token.type = Token_KEYWORD;
|
||||
}
|
||||
|
||||
// todo STRING/BINARY/ENUM/KEYWORD not stored
|
||||
// tokenStr is gone after this.
|
||||
// Orrrr we could only free pages when after a full instance is formed because after that no
|
||||
// references to prior token ranges exist anymore.
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
@@ -1262,7 +1257,7 @@ IfcFile::IfcFile(std::istream& stream, int length)
|
||||
std::string string_data;
|
||||
string_data.resize(length);
|
||||
stream.read(string_data.data(), length);
|
||||
s.push_next_page(string_data);
|
||||
s.pushNextPage(string_data);
|
||||
|
||||
storage_.emplace<1>(this);
|
||||
std::get<impl::in_memory_file_storage>(storage_).read_from_stream(&s, schema_, max_id_);
|
||||
@@ -1335,27 +1330,33 @@ IfcFile::IfcFile(const IfcParse::schema_definition* schema, filetype ty, const s
|
||||
setDefaultHeaderValues();
|
||||
}
|
||||
|
||||
bool IfcParse::InstanceStreamer::has_semicolon() const
|
||||
bool IfcParse::InstanceStreamer::hasSemicolon() const
|
||||
{
|
||||
auto local_stream = stream_->clone();
|
||||
auto local_lexer = IfcSpfLexer(&local_stream);
|
||||
Token t = local_lexer.Next();
|
||||
Token t;
|
||||
try {
|
||||
t = local_lexer.Next();
|
||||
} catch (const std::out_of_range&) {
|
||||
return false;
|
||||
}
|
||||
while (t.type != Token_NONE) {
|
||||
if (TokenFunc::isOperator(t, ';')) {
|
||||
return true;
|
||||
}
|
||||
// probably the issue is that this moves the cursor past the page boundary on the local stream which also affects the global one
|
||||
// 'freeze' the stream so that no pages are cleared
|
||||
t = local_lexer.Next();
|
||||
try {
|
||||
t = local_lexer.Next();
|
||||
} catch (const std::out_of_range&) {
|
||||
// This most likely happens when a page boundary is contained within a string
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::cout << "local: " << local_stream.tell() << " global: " << stream_->tell() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
void IfcParse::InstanceStreamer::push_page(const std::string& page)
|
||||
void IfcParse::InstanceStreamer::pushPage(const std::string& page)
|
||||
{
|
||||
std::cout << "global: " << stream_->tell() << std::endl;
|
||||
stream_->push_next_page(page);
|
||||
stream_->pushNextPage(page);
|
||||
if (good_ == file_open_status::NO_HEADER) {
|
||||
header_ = new IfcParse::IfcSpfHeader(lexer_);
|
||||
if (header_->tryRead() && header_->file_schema()->schema_identifiers().size() == 1) {
|
||||
@@ -1503,7 +1504,7 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::FileRead
|
||||
|
||||
while (streamer) {
|
||||
|
||||
auto inst = streamer.read_instance();
|
||||
auto inst = streamer.readInstance();
|
||||
|
||||
if (!inst) {
|
||||
// No more instances to read
|
||||
@@ -1558,7 +1559,7 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::FileRead
|
||||
byref_excl_ = streamer.inverses();
|
||||
|
||||
// Move the storage of simple type instances so that they are retained during the lifetime of the file
|
||||
read_simple_type_instances = streamer.steal_instances();
|
||||
read_simple_type_instances = streamer.stealInstances();
|
||||
|
||||
Logger::Status("\rDone scanning file ");
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ private:
|
||||
|
||||
%ignore IfcParse::InstanceStreamer::InstanceStreamer(const IfcParse::schema_definition* schema, IfcParse::IfcSpfLexer* lexer);
|
||||
|
||||
%ignore IfcParse::InstanceStreamer::read_instance;
|
||||
%ignore IfcParse::InstanceStreamer::steal_instances;
|
||||
%ignore IfcParse::InstanceStreamer::readInstance;
|
||||
%ignore IfcParse::InstanceStreamer::stealInstances;
|
||||
|
||||
%ignore in_memory_file_storage;
|
||||
%ignore rocks_db_file_storage;
|
||||
@@ -1003,7 +1003,7 @@ private:
|
||||
%}
|
||||
|
||||
%extend IfcParse::InstanceStreamer {
|
||||
PyObject* read_instance_py() {
|
||||
PyObject* readInstancePy() {
|
||||
auto simply_type_to_dictionary = [&](IfcUtil::IfcBaseClass* t) -> PyObject* {
|
||||
const auto& nm = t->declaration().name();
|
||||
auto ifc_val = t->get_attribute_value(0);
|
||||
@@ -1071,7 +1071,7 @@ private:
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
auto inst = self->read_instance();
|
||||
auto inst = self->readInstance();
|
||||
if (!inst) {
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
|
||||
@@ -135,7 +135,7 @@ void RocksDbSerializer::write_streaming_() {
|
||||
streamer.coerce_attribute_count = false;
|
||||
|
||||
while (streamer) {
|
||||
auto inst = streamer.read_instance();
|
||||
auto inst = streamer.readInstance();
|
||||
if (inst) {
|
||||
// name can be zero in case of header instances
|
||||
auto name = std::get<0>(*inst);
|
||||
|
||||
Reference in New Issue
Block a user