Skip to content

Commit 10f1424

Browse files
mergify[bot]sagarvoraankush
authored andcommitted
perf: cache Meta directly and other improvements (backport frappe#18164) (frappe#18181)
* perf: initialise field map when initialising meta (cherry picked from commit df8399f) * perf: simpler, faster meta cache (cherry picked from commit 6c6a969) * fix: ensure error is thrown (cherry picked from commit b529c27) * refactor: drop _allow_dict support This is not required anymore since we store full doc anyway. Also since they share same cache key calling get_cached_value and get_cached_doc can end up doing duplicate / double work. (cherry picked from commit abd0187) * fix: pickle doc objects directly This seems to be missed out and causes performance regression in IRL usage when get_doc is called on cached doc already. (cherry picked from commit bcb5fe9) * test: basic perf test for rps on get_list (cherry picked from commit c28ddcc) * build: pin testing-library/dom temporarily refer testing-library/dom-testing-library#1169 (cherry picked from commit 40f6a34) * chore: semantic changes to document caching code * fix: drop Meta cache during update Revert "fix: drop Meta cache during update" (frappe#18186) * Revert "fix: drop Meta cache during update (frappe#18182)" This reverts commit 656f6df. * fix: replace meta cache keys Old keys stored different types of data `dict` changing key to indicate change in type. * perf: cache `FormMeta` directly (frappe#18165) * perf: cache `FormMeta` directly * perf: check if `dt` is table, use `db.get_value` instead of `get_all` Co-authored-by: Sagar Vora <[email protected]> Co-authored-by: Ankush Menat <[email protected]>
1 parent 07b64fe commit 10f1424

File tree

9 files changed

+121
-97
lines changed

9 files changed

+121
-97
lines changed

frappe/__init__.py

+11-24
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,6 @@ def init(site: str, sites_path: str = ".", new_site: bool = False) -> None:
239239
local.jloader = None
240240
local.cache = {}
241241
local.document_cache = {}
242-
local.meta_cache = {}
243242
local.form_dict = _dict()
244243
local.preload_assets = {"style": [], "script": []}
245244
local.session = _dict()
@@ -1065,21 +1064,9 @@ def set_value(doctype, docname, fieldname, value=None):
10651064
return frappe.client.set_value(doctype, docname, fieldname, value)
10661065

10671066

1068-
@overload
1069-
def get_cached_doc(doctype, docname, _allow_dict=True) -> dict:
1070-
...
1071-
1072-
1073-
@overload
10741067
def get_cached_doc(*args, **kwargs) -> "Document":
1075-
...
1076-
1077-
1078-
def get_cached_doc(*args, **kwargs):
1079-
allow_dict = kwargs.pop("_allow_dict", False)
1080-
10811068
def _respond(doc, from_redis=False):
1082-
if not allow_dict and isinstance(doc, dict):
1069+
if isinstance(doc, dict):
10831070
local.document_cache[key] = doc = get_doc(doc)
10841071

10851072
elif from_redis:
@@ -1103,6 +1090,12 @@ def _respond(doc, from_redis=False):
11031090
if not key:
11041091
key = get_document_cache_key(doc.doctype, doc.name)
11051092

1093+
_set_document_in_cache(key, doc)
1094+
1095+
return doc
1096+
1097+
1098+
def _set_document_in_cache(key: str, doc: "Document") -> None:
11061099
local.document_cache[key] = doc
11071100

11081101
# Avoid setting in local.cache since we're already using local.document_cache above
@@ -1112,8 +1105,6 @@ def _respond(doc, from_redis=False):
11121105
except Exception:
11131106
cache().hset("document_cache", key, doc.as_dict(), cache_locally=False)
11141107

1115-
return doc
1116-
11171108

11181109
def can_cache_doc(args) -> str | None:
11191110
"""
@@ -1152,7 +1143,7 @@ def get_cached_value(
11521143
doctype: str, name: str, fieldname: str = "name", as_dict: bool = False
11531144
) -> Any:
11541145
try:
1155-
doc = get_cached_doc(doctype, name, _allow_dict=True)
1146+
doc = get_cached_doc(doctype, name)
11561147
except DoesNotExistError:
11571148
clear_last_message()
11581149
return
@@ -1188,13 +1179,9 @@ def get_doc(*args, **kwargs) -> "Document":
11881179

11891180
doc = frappe.model.document.get_doc(*args, **kwargs)
11901181

1191-
# Replace cache
1192-
if key := can_cache_doc(args):
1193-
if key in local.document_cache:
1194-
local.document_cache[key] = doc
1195-
1196-
if cache().hexists("document_cache", key):
1197-
cache().hset("document_cache", key, doc.as_dict())
1182+
# Replace cache if stale one exists
1183+
if (key := can_cache_doc(args)) and cache().hexists("document_cache", key):
1184+
_set_document_in_cache(key, doc)
11981185

11991186
return doc
12001187

frappe/cache_manager.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@
5959
)
6060

6161
doctype_cache_keys = (
62-
"meta",
63-
"form_meta",
62+
"doctype_meta",
63+
"doctype_form_meta",
6464
"table_columns",
6565
"last_modified",
6666
"linked_doctypes",
@@ -117,9 +117,6 @@ def clear_doctype_cache(doctype=None):
117117
clear_controller_cache(doctype)
118118
cache = frappe.cache()
119119

120-
if getattr(frappe.local, "meta_cache") and (doctype in frappe.local.meta_cache):
121-
del frappe.local.meta_cache[doctype]
122-
123120
for key in ("is_table", "doctype_modules", "document_cache"):
124121
cache.delete_value(key)
125122

frappe/commands/utils.py

+1
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,7 @@ def run_ui_tests(
881881
"@4tw/cypress-drag-drop@^2",
882882
"cypress-real-events",
883883
"@testing-library/cypress@^8",
884+
"@testing-library/[email protected]",
884885
"@cypress/code-coverage@^3",
885886
]
886887
)

frappe/core/doctype/doctype/doctype.py

-4
Original file line numberDiff line numberDiff line change
@@ -414,10 +414,6 @@ def on_update(self):
414414
if not frappe.flags.in_install and hasattr(self, "before_update"):
415415
self.sync_global_search()
416416

417-
# clear from local cache
418-
if self.name in frappe.local.meta_cache:
419-
del frappe.local.meta_cache[self.name]
420-
421417
clear_linked_doctype_cache()
422418

423419
def setup_autoincrement_and_sequence(self):

frappe/desk/form/load.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,9 @@ def getdoctype(doctype, with_parent=False, cached_timestamp=None):
6464
parent_dt = None
6565

6666
# with parent (called from report builder)
67-
if with_parent:
68-
parent_dt = frappe.model.meta.get_parent_dt(doctype)
69-
if parent_dt:
70-
docs = get_meta_bundle(parent_dt)
71-
frappe.response["parent_dt"] = parent_dt
67+
if with_parent and (parent_dt := frappe.model.meta.get_parent_dt(doctype)):
68+
docs = get_meta_bundle(parent_dt)
69+
frappe.response["parent_dt"] = parent_dt
7270

7371
if not docs:
7472
docs = get_meta_bundle(doctype)

frappe/desk/form/meta.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,10 @@
3535
def get_meta(doctype, cached=True):
3636
# don't cache for developer mode as js files, templates may be edited
3737
if cached and not frappe.conf.developer_mode:
38-
meta = frappe.cache().hget("form_meta", doctype)
39-
if meta:
40-
meta = FormMeta(meta)
41-
else:
38+
meta = frappe.cache().hget("doctype_form_meta", doctype)
39+
if not meta:
4240
meta = FormMeta(doctype)
43-
frappe.cache().hset("form_meta", doctype, meta.as_dict())
41+
frappe.cache().hset("doctype_form_meta", doctype, meta)
4442
else:
4543
meta = FormMeta(doctype)
4644

frappe/model/meta.py

+61-53
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,31 @@
4040
from frappe.modules import load_doctype_module
4141
from frappe.utils import cast, cint, cstr
4242

43+
DEFAULT_FIELD_LABELS = {
44+
"name": lambda: _("ID"),
45+
"creation": lambda: _("Created On"),
46+
"docstatus": lambda: _("Document Status"),
47+
"idx": lambda: _("Index"),
48+
"modified": lambda: _("Last Updated On"),
49+
"modified_by": lambda: _("Last Updated By"),
50+
"owner": lambda: _("Created By"),
51+
"_user_tags": lambda: _("Tags"),
52+
"_liked_by": lambda: _("Liked By"),
53+
"_comments": lambda: _("Comments"),
54+
"_assign": lambda: _("Assigned To"),
55+
}
56+
4357

4458
def get_meta(doctype, cached=True) -> "Meta":
45-
if cached:
46-
if not frappe.local.meta_cache.get(doctype):
47-
meta = frappe.cache().hget("meta", doctype)
48-
if meta:
49-
meta = Meta(meta)
50-
else:
51-
meta = Meta(doctype)
52-
frappe.cache().hset("meta", doctype, meta.as_dict())
53-
frappe.local.meta_cache[doctype] = meta
59+
if not cached:
60+
return Meta(doctype)
5461

55-
return frappe.local.meta_cache[doctype]
56-
else:
57-
return load_meta(doctype)
62+
if meta := frappe.cache().hget("doctype_meta", doctype):
63+
return meta
64+
65+
meta = Meta(doctype)
66+
frappe.cache().hset("doctype_meta", doctype, meta)
67+
return meta
5868

5969

6070
def load_meta(doctype):
@@ -86,32 +96,33 @@ def load_doctype_from_file(doctype):
8696
class Meta(Document):
8797
_metaclass = True
8898
default_fields = list(default_fields)[1:]
89-
special_doctypes = (
99+
special_doctypes = {
90100
"DocField",
91101
"DocPerm",
92102
"DocType",
93103
"Module Def",
94104
"DocType Action",
95105
"DocType Link",
96106
"DocType State",
97-
)
107+
}
98108
standard_set_once_fields = [
99109
frappe._dict(fieldname="creation", fieldtype="Datetime"),
100110
frappe._dict(fieldname="owner", fieldtype="Data"),
101111
]
102112

103113
def __init__(self, doctype):
104-
self._fields = {}
114+
# from cache
105115
if isinstance(doctype, dict):
106116
super().__init__(doctype)
117+
self.init_field_map()
118+
return
107119

108-
elif isinstance(doctype, Document):
120+
if isinstance(doctype, Document):
109121
super().__init__(doctype.as_dict())
110-
self.process()
111-
112122
else:
113123
super().__init__("DocType", doctype)
114-
self.process()
124+
125+
self.process()
115126

116127
def load_from_db(self):
117128
try:
@@ -126,10 +137,12 @@ def process(self):
126137
# don't process for special doctypes
127138
# prevent's circular dependency
128139
if self.name in self.special_doctypes:
140+
self.init_field_map()
129141
return
130142

131143
self.add_custom_fields()
132144
self.apply_property_setters()
145+
self.init_field_map()
133146
self.sort_fields()
134147
self.get_valid_columns()
135148
self.set_custom_permissions()
@@ -233,36 +246,24 @@ def get_table_field_doctype(self, fieldname):
233246

234247
def get_field(self, fieldname):
235248
"""Return docfield from meta"""
236-
if not self._fields:
237-
for f in self.get("fields"):
238-
self._fields[f.fieldname] = f
239249

240250
return self._fields.get(fieldname)
241251

242252
def has_field(self, fieldname):
243253
"""Returns True if fieldname exists"""
244-
return True if self.get_field(fieldname) else False
254+
255+
return fieldname in self._fields
245256

246257
def get_label(self, fieldname):
247258
"""Get label of the given fieldname"""
248-
df = self.get_field(fieldname)
249-
if df:
250-
label = df.label
251-
else:
252-
label = {
253-
"name": _("ID"),
254-
"creation": _("Created On"),
255-
"docstatus": _("Document Status"),
256-
"idx": _("Index"),
257-
"modified": _("Last Updated On"),
258-
"modified_by": _("Last Updated By"),
259-
"owner": _("Created By"),
260-
"_user_tags": _("Tags"),
261-
"_liked_by": _("Liked By"),
262-
"_comments": _("Comments"),
263-
"_assign": _("Assigned To"),
264-
}.get(fieldname) or _("No Label")
265-
return label
259+
260+
if df := self.get_field(fieldname):
261+
return df.label
262+
263+
if fieldname in DEFAULT_FIELD_LABELS:
264+
return DEFAULT_FIELD_LABELS[fieldname]()
265+
266+
return _("No Label")
266267

267268
def get_options(self, fieldname):
268269
return self.get_field(fieldname).options
@@ -273,12 +274,9 @@ def get_link_doctype(self, fieldname):
273274
if df.fieldtype == "Link":
274275
return df.options
275276

276-
elif df.fieldtype == "Dynamic Link":
277+
if df.fieldtype == "Dynamic Link":
277278
return self.get_options(df.options)
278279

279-
else:
280-
return None
281-
282280
def get_search_fields(self):
283281
search_fields = self.search_fields or "name"
284282
search_fields = [d.strip() for d in search_fields.split(",")]
@@ -340,20 +338,20 @@ def get_translatable_fields(self):
340338

341339
def is_translatable(self, fieldname):
342340
"""Return true of false given a field"""
343-
field = self.get_field(fieldname)
344-
return field and field.translatable
341+
342+
if field := self.get_field(fieldname):
343+
return field.translatable
345344

346345
def get_workflow(self):
347346
return get_workflow_name(self.name)
348347

349348
def get_naming_series_options(self) -> list[str]:
350349
"""Get list naming series options."""
351350

352-
field = self.get_field("naming_series")
353-
if field:
351+
if field := self.get_field("naming_series"):
354352
options = field.options or ""
355-
356353
return options.split("\n")
354+
357355
return []
358356

359357
def add_custom_fields(self):
@@ -450,6 +448,9 @@ def add_custom_links_and_actions(self):
450448

451449
self.set(fieldname, new_list)
452450

451+
def init_field_map(self):
452+
self._fields = {field.fieldname: field for field in self.fields}
453+
453454
def sort_fields(self):
454455
"""sort on basis of insert_after"""
455456
custom_fields = sorted(self.get_custom_fields(), key=lambda df: df.idx)
@@ -666,10 +667,17 @@ def is_single(doctype):
666667

667668

668669
def get_parent_dt(dt):
669-
parent_dt = frappe.get_all(
670-
"DocField", "parent", dict(fieldtype=["in", frappe.model.table_fields], options=dt), limit=1
670+
if not frappe.is_table(dt):
671+
return ""
672+
673+
return (
674+
frappe.db.get_value(
675+
"DocField",
676+
{"fieldtype": ("in", frappe.model.table_fields), "options": dt},
677+
"parent",
678+
)
679+
or ""
671680
)
672-
return parent_dt and parent_dt[0].parent or ""
673681

674682

675683
def set_fieldname(field_id, fieldname):

0 commit comments

Comments
 (0)