Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion python_tests/test_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def test_base_lock_usage_does_not_exceed_limits(db0_fixture):
assert usage_2 - usage_1 < cache_size * 1.5


def test_lang_cache_can_reach_capacity(db0_fixture):
def test_lang_cache_can_reach_capacity(db0_fixture):
buf = db0.list()
# python instances are added to lang cache until it reaches capacity
initial_capacity = db0.get_lang_cache_stats()["capacity"]
Expand Down
64 changes: 64 additions & 0 deletions python_tests/test_memo_no_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from random import random
import pytest
import dbzero as db0
from .memo_test_types import MemoTestClass, TriColor, MemoAnyAttrs
from dataclasses import dataclass
import random
import string
import gc


def rand_string(max_len):
str_len = random.randint(1, max_len)
return ''.join(random.choice(string.ascii_letters) for i in range(str_len))


@db0.memo(no_cache=True)
@dataclass
class MemoNoCacheClass:
data: str

def __init__(self):
self.data = rand_string(12 << 10)


def test_create_memo_no_cache(db0_fixture):
obj_1 = MemoNoCacheClass()
assert obj_1.data is not None


def test_no_cache_instances_removed_from_lang_cache(db0_fixture):
buf = db0.list()
for _ in range(100):
obj = MemoNoCacheClass()
buf.append(obj)

gc.collect()
# make sure objects were not cached
assert db0.get_lang_cache_stats()["size"] < 10


def test_memo_no_cache_issue1(db0_fixture):
"""
Issue: test failing with RuntimeError: BDevStorage::read: page not found: 8, state: 2
Reason: objects marked with no_cache were not being registered with DirtyCache, added no_dirty_cache flag
"""
buf = db0.list()
for _ in range(3):
obj = MemoNoCacheClass()
buf.append(obj)
del obj


def test_excluding_no_cache_instances_from_dbzero_cache(db0_fixture):
buf = db0.list()
initial_cache_size = db0.get_cache_stats()["size"]
for _ in range(100):
obj = MemoNoCacheClass()
buf.append(obj)

gc.collect()
final_cache_size = db0.get_cache_stats()["size"]
# make sure cache utilization is low
assert abs(final_cache_size - initial_cache_size) < (300 << 10)

44 changes: 28 additions & 16 deletions src/dbzero/bindings/python/Memo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ namespace db0::python
PyErr_Restore(ptype, pvalue, ptraceback);
return -1;
}

// invoke post-init on associated dbzero object
auto &object = self->modifyExt();
db0::FixtureLock fixture(object.getFixture());
Expand All @@ -219,10 +219,12 @@ namespace db0::python
// need to call modifyExt again after postInit because the instance has just been created
// and potentially needs to be included in the AtomicContext
self->modifyExt();
fixture->getLangCache().add(object.getAddress(), self);

// finally, unless opted-out, assign the type tag(s) of the entire type hierarchy
const Class *class_ptr = &object.getType();
if (!class_ptr || !class_ptr->isNoCache()) {
fixture->getLangCache().add(object.getAddress(), self);
}

// finally, unless opted-out, assign the type tag(s) of the entire type hierarchy
if (class_ptr && class_ptr->assignDefaultTags()) {
auto &tag_index = fixture->get<TagIndex>();
while (class_ptr) {
Expand All @@ -242,9 +244,12 @@ namespace db0::python
if (memo_obj->ext().isSingleton()) {
db0::FixtureLock lock(memo_obj->ext().getFixture());
memo_obj->modifyExt().unSingleton(lock);
// the acutal destroy will be performed by the GC0 once removed from the LangCache
auto &lang_cache = memo_obj->ext().getFixture()->getLangCache();
lang_cache.erase(memo_obj->ext().getAddress());
if (!memo_obj->ext().isNoCache()) {
// the actual destroy will be performed by the GC0 once removed from the LangCache
auto &lang_cache = memo_obj->ext().getFixture()->getLangCache();
lang_cache.erase(memo_obj->ext().getAddress());
}

return;
}

Expand All @@ -258,11 +263,14 @@ namespace db0::python

// create a null placeholder in place of the original instance to mark as deleted
auto &lang_cache = memo_obj->ext().getFixture()->getLangCache();
bool no_cache = memo_obj->ext().isNoCache();
auto obj_addr = memo_obj->ext().getUniqueAddress();
db0::FixtureLock lock(memo_obj->ext().getFixture());
memo_obj->modifyExt().dropInstance(lock);
// remove instance from the lang cache
lang_cache.erase(obj_addr);
if (!no_cache) {
lang_cache.erase(obj_addr);
}
}

PyObject *tryMemoObject_getattro(MemoObject *memo_obj, PyObject *attr)
Expand Down Expand Up @@ -449,7 +457,7 @@ namespace db0::python

PyObject *wrapPyType(PyTypeObject *base_class, bool is_singleton, bool no_default_tags, const char *prefix_name,
const char *type_id, const char *file_name, std::vector<std::string> &&init_vars, PyObject *py_dyn_prefix_callable,
std::vector<Migration> &&migrations)
std::vector<Migration> &&migrations, bool no_cache)
{
auto py_class = Py_BORROW(base_class);
auto py_module = Py_OWN(findModule(*Py_OWN(PyObject_GetAttrString((PyObject*)*py_class, "__module__"))));
Expand All @@ -473,6 +481,9 @@ namespace db0::python

auto &type_manager = PyToolkit::getTypeManager();
MemoFlags type_flags = no_default_tags ? MemoFlags { MemoOptions::NO_DEFAULT_TAGS } : MemoFlags();
if (no_cache) {
type_flags.set(MemoOptions::NO_CACHE);
}
auto type_info = MemoTypeDecoration(
py_module,
prefix_name,
Expand All @@ -488,7 +499,7 @@ namespace db0::python
PyToolkit::getTypeManager().addMemoType(*new_type, type_id, std::move(type_info));
// register new type with the module where the original type was located
PySafeModule_AddObject(*py_module, type_name.c_str(), new_type);

// add class fields class member to access memo type information
auto py_class_fields = Py_OWN(PyClassFields_create(*new_type));
if (PySafeDict_SetItemString((*new_type)->tp_dict, "__fields__", py_class_fields) < 0) {
Expand All @@ -511,18 +522,19 @@ namespace db0::python
PyObject *py_dyn_prefix = nullptr;
// migrations are only processed for singleton types
PyObject *py_migrations = nullptr;
PyObject *py_no_cache = nullptr;

static const char *kwlist[] = { "input", "singleton", "no_default_tags", "prefix", "id", "py_file", "py_init_vars",
"py_dyn_prefix", "py_migrations", NULL };
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OOOOOOOO", const_cast<char**>(kwlist), &class_obj, &py_singleton,
&py_no_default_tags, &py_prefix_name, &py_type_id, &py_file_name, &py_init_vars, &py_dyn_prefix, &py_migrations))
{
PyErr_SetString(PyExc_TypeError, "Invalid input arguments");
"py_dyn_prefix", "py_migrations", "no_cache", NULL };
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OOOOOOOOO", const_cast<char**>(kwlist), &class_obj, &py_singleton,
&py_no_default_tags, &py_prefix_name, &py_type_id, &py_file_name, &py_init_vars, &py_dyn_prefix, &py_migrations, &py_no_cache))
{
return NULL;
}

bool is_singleton = py_singleton && PyObject_IsTrue(py_singleton);
bool no_default_tags = py_no_default_tags && PyObject_IsTrue(py_no_default_tags);
bool no_cache = py_no_cache && PyObject_IsTrue(py_no_cache);
const char *prefix_name = (py_prefix_name && py_prefix_name != Py_None) ? PyUnicode_AsUTF8(py_prefix_name) : nullptr;
const char *type_id = py_type_id ? PyUnicode_AsUTF8(py_type_id) : nullptr;
const char *file_name = (py_file_name && py_file_name != Py_None) ? PyUnicode_AsUTF8(py_file_name) : nullptr;
Expand Down Expand Up @@ -557,7 +569,7 @@ namespace db0::python

auto migrations = extractMigrations(py_migrations);
return wrapPyType(castToType(class_obj), is_singleton, no_default_tags, prefix_name, type_id, file_name,
std::move(init_vars), py_dyn_prefix, std::move(migrations)
std::move(init_vars), py_dyn_prefix, std::move(migrations), no_cache
);
}

Expand Down
2 changes: 0 additions & 2 deletions src/dbzero/bindings/python/MemoTypeDecoration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
#include <dbzero/workspace/Workspace.hpp>
#include <dbzero/workspace/PrefixName.hpp>

DEFINE_ENUM_VALUES(db0::python::MemoOptions, "NO_DEFAULT_TAGS")

namespace db0::python

{
Expand Down
16 changes: 5 additions & 11 deletions src/dbzero/bindings/python/MemoTypeDecoration.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,15 @@
#include <optional>
#include <dbzero/core/memory/AccessOptions.hpp>
#include <dbzero/workspace/PrefixName.hpp>
#include <dbzero/object_model/object/Options.hpp>

namespace db0::python

{

enum MemoOptions: std::uint16_t
{
// instances of this type opted out of auto-assigned type tags
NO_DEFAULT_TAGS = 0x0001
};

using MemoFlags = db0::FlagSet<MemoOptions>;


using MemoOptions = db0::object_model::MemoOptions;
using MemoFlags = db0::object_model::MemoFlags;

// check for a memo type (i.e. generated by PyAPI_wrapPyClass)
bool PyMemo_Check(PyObject *obj);
bool PyMemoType_Check(PyTypeObject *type);
Expand Down Expand Up @@ -119,5 +115,3 @@ namespace db0::python
};

}

DECLARE_ENUM_VALUES(db0::python::MemoOptions, 1)
28 changes: 25 additions & 3 deletions src/dbzero/bindings/python/PyToolkit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ namespace db0::python
const ClassFactory &class_factory, TypeObjectPtr lang_type_ptr, std::uint16_t instance_id)
{
// try unloading from cache first
auto &lang_cache = fixture->getLangCache();
auto &lang_cache = fixture->getLangCache();
auto obj_ptr = tryUnloadObjectFromCache(lang_cache, address, nullptr);

if (obj_ptr.get()) {
Expand Down Expand Up @@ -173,7 +173,9 @@ namespace db0::python
memo_ptr->unload(fixture, std::move(stem), type, Object::with_type_hint{});
// NOTE: Py_OWN only possible with a proper object
obj_ptr = Py_OWN((PyObject*)memo_ptr);
lang_cache.add(address, obj_ptr.get());
if (!memo_ptr->ext().isNoCache()) {
lang_cache.add(address, obj_ptr.get());
}
return obj_ptr;
}

Expand Down Expand Up @@ -205,7 +207,9 @@ namespace db0::python
memo_ptr->unload(fixture, address, type, Object::with_type_hint{});
// NOTE: Py_OWN only possible with a proper object
obj_ptr = Py_OWN((PyObject*)memo_ptr);
lang_cache.add(address, obj_ptr.get());
if (!memo_ptr->ext().isNoCache()) {
lang_cache.add(address, obj_ptr.get());
}
return obj_ptr;
}

Expand Down Expand Up @@ -521,6 +525,24 @@ namespace db0::python
}
}

bool PyToolkit::isNoCache(TypeObjectPtr py_type)
{
if (isMemoType(py_type)) {
return MemoTypeDecoration::get(py_type).getFlags()[MemoOptions::NO_CACHE];
} else {
return false;
}
}

FlagSet<MemoOptions> PyToolkit::getMemoFlags(TypeObjectPtr py_type)
{
if (isMemoType(py_type)) {
return MemoTypeDecoration::get(py_type).getFlags();
} else {
return {};
}
}

const char *PyToolkit::getPrefixName(TypeObjectPtr memo_type)
{
assert(isMemoType(memo_type));
Expand Down
6 changes: 4 additions & 2 deletions src/dbzero/bindings/python/PyToolkit.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,11 @@ namespace db0::python
static const char *getMemoTypeID(TypeObjectPtr memo_type);
static const std::vector<std::string> &getInitVars(TypeObjectPtr memo_type);

static bool isSingleton(TypeObjectPtr py_type);
static bool isSingleton(TypeObjectPtr);
// check if a memo type is marked with no_default_tags flag
static bool isNoDefaultTags(TypeObjectPtr py_type);
static bool isNoDefaultTags(TypeObjectPtr);
static bool isNoCache(TypeObjectPtr);
static FlagSet<MemoOptions> getMemoFlags(TypeObjectPtr);

inline static void incRef(ObjectPtr py_object) {
Py_INCREF(py_object);
Expand Down
15 changes: 8 additions & 7 deletions src/dbzero/bindings/python/collections/PyDict.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -357,11 +357,12 @@ namespace db0::python
return runSafe(tryDictObject_update, dict_object, args, kwargs);
}

shared_py_object<DictObject*> tryMake_DB0Dict(db0::swine_ptr<Fixture> &fixture, PyObject *args, PyObject *kwargs)
shared_py_object<DictObject*> tryMake_DB0Dict(db0::swine_ptr<Fixture> &fixture, PyObject *args,
PyObject *kwargs, AccessFlags access_mode)
{
auto py_dict = DictDefaultObject_new();
db0::FixtureLock lock(fixture);
auto &dict = py_dict->makeNew(*lock);
db0::FixtureLock lock(fixture);
auto &dict = py_dict->makeNew(*lock, access_mode);

// if args
if (!tryDictObject_update(py_dict.get(), args, kwargs)) {
Expand All @@ -373,16 +374,16 @@ namespace db0::python
return py_dict;
}

shared_py_object<DictObject*> tryMake_DB0DictInternal(PyObject *args, PyObject *kwargs)
shared_py_object<DictObject*> tryMake_DB0DictInternal(PyObject *args, PyObject *kwargs, AccessFlags access_mode)
{
auto fixture = PyToolkit::getPyWorkspace().getWorkspace().getCurrentFixture();
return tryMake_DB0Dict(fixture, args, kwargs);
return tryMake_DB0Dict(fixture, args, kwargs, access_mode);
}

DictObject *PyAPI_makeDict(PyObject *, PyObject* args, PyObject* kwargs)
{
PY_API_FUNC
return runSafe(tryMake_DB0DictInternal, args, kwargs).steal();
PY_API_FUNC
return runSafe(tryMake_DB0DictInternal, args, kwargs, AccessFlags {}).steal();
}

PyObject *tryDictObject_clear(DictObject *dict_obj)
Expand Down
8 changes: 6 additions & 2 deletions src/dbzero/bindings/python/collections/PyDict.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace db0::python

using DictObject = PyWrapper<db0::object_model::Dict>;
using DictIteratorObject = PySharedWrapper<db0::object_model::DictIterator, false>;
using AccessFlags = db0::AccessFlags;

DictObject *DictObject_new(PyTypeObject *type, PyObject *, PyObject *);
shared_py_object<DictObject*> DictDefaultObject_new();
Expand All @@ -28,8 +29,11 @@ namespace db0::python
PyObject *PyAPI_DictObject_items(DictObject *dict_obj);
void PyAPI_DictObject_del(DictObject* dict_obj);
extern PyTypeObject DictObjectType;

shared_py_object<DictObject*> tryMake_DB0Dict(db0::swine_ptr<Fixture> &, PyObject *args, PyObject *kwargs);

shared_py_object<DictObject*> tryMake_DB0Dict(db0::swine_ptr<Fixture> &, PyObject *args,
PyObject *kwargs, AccessFlags
);

DictObject *PyAPI_makeDict(PyObject *, PyObject*, PyObject*);
bool DictObject_Check(PyObject *);

Expand Down
Loading
Loading