Make MessageQueue growable

* Uses simple vector resizing (po2)
* Uses pair of read and write buffers
This commit is contained in:
lawnjelly 2023-03-30 20:06:43 +01:00
parent b0c399ec8c
commit 5f1e7e6fbc
4 changed files with 223 additions and 123 deletions

View file

@ -136,9 +136,11 @@ public:
}
}
_FORCE_INLINE_ bool empty() const { return count == 0; }
_FORCE_INLINE_ void reserve(U p_size) {
_FORCE_INLINE_ U get_capacity() const { return capacity; }
_FORCE_INLINE_ void reserve(U p_size, bool p_allow_shrink = false) {
p_size = nearest_power_of_2_templated(p_size);
if (p_size > capacity) {
if (!p_allow_shrink ? p_size > capacity : ((p_size >= count) && (p_size != capacity))) {
capacity = p_size;
data = (T *)memrealloc(data, capacity * sizeof(T));
CRASH_COND_MSG(!data, "Out of memory");

View file

@ -44,17 +44,24 @@ Error MessageQueue::push_call(ObjectID p_id, const StringName &p_method, const V
int room_needed = sizeof(Message) + sizeof(Variant) * p_argcount;
if ((buffer_end + room_needed) >= buffer_size) {
Buffer &buffer = buffers[write_buffer];
if ((buffer.end + room_needed) > buffer.data.size()) {
if ((buffer.end + room_needed) > max_allowed_buffer_size) {
String type;
if (ObjectDB::get_instance(p_id)) {
type = ObjectDB::get_instance(p_id)->get_class();
}
print_line("Failed method: " + type + ":" + p_method + " target ID: " + itos(p_id));
print_line("Failed method: " + p_method);
statistics();
ERR_FAIL_V_MSG(ERR_OUT_OF_MEMORY, "Message queue out of memory. Try increasing 'memory/limits/message_queue/max_size_kb' in project settings.");
ERR_FAIL_V_MSG(ERR_OUT_OF_MEMORY, "Message queue out of memory. Try increasing 'memory/limits/message_queue/max_size_mb' in project settings.");
} else {
buffer.data.resize(buffer.end + room_needed);
}
}
Message *msg = memnew_placement(&buffer[buffer_end], Message);
Message *msg = memnew_placement(&buffer.data[buffer.end], Message);
msg->args = p_argcount;
msg->instance_id = p_id;
msg->target = p_method;
@ -63,11 +70,11 @@ Error MessageQueue::push_call(ObjectID p_id, const StringName &p_method, const V
msg->type |= FLAG_SHOW_ERROR;
}
buffer_end += sizeof(Message);
buffer.end += sizeof(Message);
for (int i = 0; i < p_argcount; i++) {
Variant *v = memnew_placement(&buffer[buffer_end], Variant);
buffer_end += sizeof(Variant);
Variant *v = memnew_placement(&buffer.data[buffer.end], Variant);
buffer.end += sizeof(Variant);
*v = *p_args[i];
}
@ -94,26 +101,33 @@ Error MessageQueue::push_set(ObjectID p_id, const StringName &p_prop, const Vari
uint8_t room_needed = sizeof(Message) + sizeof(Variant);
if ((buffer_end + room_needed) >= buffer_size) {
Buffer &buffer = buffers[write_buffer];
if ((buffer.end + room_needed) > buffer.data.size()) {
if ((buffer.end + room_needed) > max_allowed_buffer_size) {
String type;
if (ObjectDB::get_instance(p_id)) {
type = ObjectDB::get_instance(p_id)->get_class();
}
print_line("Failed set: " + type + ":" + p_prop + " target ID: " + itos(p_id));
statistics();
ERR_FAIL_V_MSG(ERR_OUT_OF_MEMORY, "Message queue out of memory. Try increasing 'memory/limits/message_queue/max_size_kb' in project settings.");
ERR_FAIL_V_MSG(ERR_OUT_OF_MEMORY, "Message queue out of memory. Try increasing 'memory/limits/message_queue/max_size_mb' in project settings.");
} else {
buffer.data.resize(buffer.end + room_needed);
}
}
Message *msg = memnew_placement(&buffer[buffer_end], Message);
Message *msg = memnew_placement(&buffer.data[buffer.end], Message);
msg->args = 1;
msg->instance_id = p_id;
msg->target = p_prop;
msg->type = TYPE_SET;
buffer_end += sizeof(Message);
buffer.end += sizeof(Message);
Variant *v = memnew_placement(&buffer[buffer_end], Variant);
buffer_end += sizeof(Variant);
Variant *v = memnew_placement(&buffer.data[buffer.end], Variant);
buffer.end += sizeof(Variant);
*v = p_value;
return OK;
@ -126,20 +140,30 @@ Error MessageQueue::push_notification(ObjectID p_id, int p_notification) {
uint8_t room_needed = sizeof(Message);
if ((buffer_end + room_needed) >= buffer_size) {
Buffer &buffer = buffers[write_buffer];
if ((buffer.end + room_needed) > buffer.data.size()) {
if ((buffer.end + room_needed) > max_allowed_buffer_size) {
String type;
if (ObjectDB::get_instance(p_id)) {
type = ObjectDB::get_instance(p_id)->get_class();
}
print_line("Failed notification: " + itos(p_notification) + " target ID: " + itos(p_id));
statistics();
ERR_FAIL_V_MSG(ERR_OUT_OF_MEMORY, "Message queue out of memory. Try increasing 'memory/limits/message_queue/max_size_kb' in project settings.");
ERR_FAIL_V_MSG(ERR_OUT_OF_MEMORY, "Message queue out of memory. Try increasing 'memory/limits/message_queue/max_size_mb' in project settings.");
} else {
buffer.data.resize(buffer.end + room_needed);
}
}
Message *msg = memnew_placement(&buffer[buffer_end], Message);
Message *msg = memnew_placement(&buffer.data[buffer.end], Message);
msg->type = TYPE_NOTIFICATION;
msg->instance_id = p_id;
//msg->target;
msg->notification = p_notification;
buffer_end += sizeof(Message);
buffer.end += sizeof(Message);
return OK;
}
@ -161,9 +185,11 @@ void MessageQueue::statistics() {
Map<StringName, int> call_count;
int null_count = 0;
Buffer &buffer = buffers[write_buffer];
uint32_t read_pos = 0;
while (read_pos < buffer_end) {
Message *message = (Message *)&buffer[read_pos];
while (read_pos < buffer.end) {
Message *message = (Message *)&buffer.data[read_pos];
Object *target = ObjectDB::get_instance(message->instance_id);
@ -208,7 +234,7 @@ void MessageQueue::statistics() {
}
}
print_line("TOTAL BYTES: " + itos(buffer_end));
print_line("TOTAL BYTES: " + itos(buffer.end));
print_line("NULL count: " + itos(null_count));
for (Map<StringName, int>::Element *E = set_count.front(); E; E = E->next()) {
@ -225,7 +251,8 @@ void MessageQueue::statistics() {
}
int MessageQueue::get_max_buffer_usage() const {
return buffer_max_used;
// Note this may be better read_buffer, or a combination, depending when this is read.
return buffers[write_buffer].data.size();
}
void MessageQueue::_call_function(Object *p_target, const StringName &p_func, const Variant *p_args, int p_argcount, bool p_show_error) {
@ -244,34 +271,82 @@ void MessageQueue::_call_function(Object *p_target, const StringName &p_func, co
}
}
void MessageQueue::flush() {
if (buffer_end > buffer_max_used) {
buffer_max_used = buffer_end;
void MessageQueue::_update_buffer_monitor() {
// The number of flushes is an approximate delay before
// considering shrinking. This is somewhat of a magic number,
// but only acts to prevent excessive oscillations.
if (++_buffer_size_monitor.flush_count == 8192) {
uint32_t max_size = _buffer_size_monitor.max_size;
// Uncomment this define to log message queue sizes and
// auto-shrinking behaviour.
// #define GODOT_DEBUG_MESSAGE_QUEUE_SIZES
#ifdef GODOT_DEBUG_MESSAGE_QUEUE_SIZES
print_line("MessageQueue buffer max size " + itos(max_size) + " bytes.");
#endif
// reset for next time
_buffer_size_monitor.flush_count = 0;
_buffer_size_monitor.max_size = 0;
for (uint32_t n = 0; n < 2; n++) {
uint32_t cap = buffers[n].data.get_capacity();
// Only worry about reducing memory if the capacity is high
// (due to e.g. loading a level or something).
// The shrinking will only take place below 256K, to prevent
// excessive reallocating.
if (cap > (256 * 1024)) {
// Only shrink if we are routinely using a lot less than the capacity.
if ((max_size * 4) < cap) {
buffers[n].data.reserve(cap / 2, true);
#ifdef GODOT_DEBUG_MESSAGE_QUEUE_SIZES
print_line("MessageQueue reducing buffer[" + itos(n) + "] capacity from " + itos(cap) + " bytes to " + itos(cap / 2) + " bytes.");
#endif
}
}
}
}
}
uint32_t read_pos = 0;
void MessageQueue::flush() {
//using reverse locking strategy
_THREAD_SAFE_LOCK_
ERR_FAIL_COND(flushing); //already flushing, you did something odd
if (flushing) {
_THREAD_SAFE_UNLOCK_
ERR_FAIL_MSG("Already flushing"); //already flushing, you did something odd
}
// first flip buffers, in preparation
SWAP(read_buffer, write_buffer);
flushing = true;
_update_buffer_monitor();
_THREAD_SAFE_UNLOCK_
while (read_pos < buffer_end) {
//lock on each iteration, so a call can re-add itself to the message queue
// This loop works by having a read buffer and write buffer.
// While we are reading from one buffer we can be filling another.
// This enables them to be independent, and not require locks per message.
// It also avoids pushing and resizing the write buffer corrupting the read buffer.
// The trade off is that it requires more memory.
// However the peak size of each can be lower, because they do not ADD
// to each other during transit.
Message *message = (Message *)&buffer[read_pos];
while (buffers[read_buffer].data.size()) {
uint32_t read_pos = 0;
Buffer &buffer = buffers[read_buffer];
while (read_pos < buffer.end) {
Message *message = (Message *)&buffer.data[read_pos];
uint32_t advance = sizeof(Message);
if ((message->type & FLAG_MASK) != TYPE_NOTIFICATION) {
advance += sizeof(Variant) * message->args;
}
//pre-advance so this function is reentrant
read_pos += advance;
_THREAD_SAFE_UNLOCK_
Object *target = ObjectDB::get_instance(message->instance_id);
if (target != nullptr) {
@ -307,10 +382,24 @@ void MessageQueue::flush() {
message->~Message();
_THREAD_SAFE_LOCK_
}
} // while going through buffer
buffer_end = 0; // reset buffer
buffer.end = 0; // reset buffer
uint32_t buffer_data_size = buffer.data.size();
buffer.data.clear();
_THREAD_SAFE_LOCK_
// keep track of the maximum used size, so we can downsize buffers when appropriate
_buffer_size_monitor.max_size = MAX(buffer_data_size, _buffer_size_monitor.max_size);
// flip buffers, this is the only part that requires a lock
SWAP(read_buffer, write_buffer);
_THREAD_SAFE_UNLOCK_
} // while read buffer not empty
_THREAD_SAFE_LOCK_
flushing = false;
_THREAD_SAFE_UNLOCK_
}
@ -324,19 +413,19 @@ MessageQueue::MessageQueue() {
singleton = this;
flushing = false;
buffer_end = 0;
buffer_max_used = 0;
buffer_size = GLOBAL_DEF_RST("memory/limits/message_queue/max_size_kb", DEFAULT_QUEUE_SIZE_KB);
ProjectSettings::get_singleton()->set_custom_property_info("memory/limits/message_queue/max_size_kb", PropertyInfo(Variant::INT, "memory/limits/message_queue/max_size_kb", PROPERTY_HINT_RANGE, "1024,4096,1,or_greater"));
buffer_size *= 1024;
buffer = memnew_arr(uint8_t, buffer_size);
max_allowed_buffer_size = GLOBAL_DEF_RST("memory/limits/message_queue/max_size_mb", 32);
ProjectSettings::get_singleton()->set_custom_property_info("memory/limits/message_queue/max_size_mb", PropertyInfo(Variant::INT, "memory/limits/message_queue/max_size_mb", PROPERTY_HINT_RANGE, "4,512,1,or_greater"));
max_allowed_buffer_size *= 1024 * 1024;
}
MessageQueue::~MessageQueue() {
for (int which = 0; which < 2; which++) {
Buffer &buffer = buffers[which];
uint32_t read_pos = 0;
while (read_pos < buffer_end) {
Message *message = (Message *)&buffer[read_pos];
while (read_pos < buffer.end) {
Message *message = (Message *)&buffer.data[read_pos];
Variant *args = (Variant *)(message + 1);
int argc = message->args;
if ((message->type & FLAG_MASK) != TYPE_NOTIFICATION) {
@ -352,6 +441,7 @@ MessageQueue::~MessageQueue() {
}
}
} // for which
singleton = nullptr;
memdelete_arr(buffer);
}

View file

@ -31,16 +31,13 @@
#ifndef MESSAGE_QUEUE_H
#define MESSAGE_QUEUE_H
#include "core/local_vector.h"
#include "core/object.h"
#include "core/os/thread_safe.h"
class MessageQueue {
_THREAD_SAFE_CLASS_
enum {
DEFAULT_QUEUE_SIZE_KB = 4096
};
enum {
TYPE_CALL,
TYPE_NOTIFICATION,
@ -60,12 +57,23 @@ class MessageQueue {
};
};
uint8_t *buffer;
uint32_t buffer_end;
uint32_t buffer_max_used;
uint32_t buffer_size;
struct Buffer {
LocalVector<uint8_t> data;
uint64_t end = 0;
};
Buffer buffers[2];
int read_buffer = 0;
int write_buffer = 1;
uint64_t max_allowed_buffer_size = 0;
struct BufferSizeMonitor {
uint32_t max_size = 0;
uint32_t flush_count = 0;
} _buffer_size_monitor;
void _call_function(Object *p_target, const StringName &p_func, const Variant *p_args, int p_argcount, bool p_show_error);
void _update_buffer_monitor();
static MessageQueue *singleton;

View file

@ -1190,7 +1190,7 @@
</member>
<member name="memory/limits/command_queue/multithreading_queue_size_kb" type="int" setter="" getter="" default="256">
</member>
<member name="memory/limits/message_queue/max_size_kb" type="int" setter="" getter="" default="4096">
<member name="memory/limits/message_queue/max_size_mb" type="int" setter="" getter="" default="32">
Godot uses a message queue to defer some function calls. If you run out of space on it (you will see an error), you can increase the size here.
</member>
<member name="memory/limits/multithreaded_server/rid_pool_prealloc" type="int" setter="" getter="" default="60">