perf!: speedup @site_cache by ~4x (#28802)

* perf(site_cache): reduce access to frappe.local namespace

This change also allows calling @site_cache during init, as long as `site` parameter is set.

* test: frappe.init patching

* perf: use monotonic time instead of realtime for eviction

datetime is complex, slow and not really required for this use case.

* perf!: Drop support for unhashable arguments

Just like LRU cache, no need to support unhashable types in site_cache.
Current usage in codebase also shows that it's not required and json.dumps is quite slow.
This commit is contained in:
Ankush Menat 2024-12-17 12:24:09 +05:30 committed by GitHub
commit 98a33b4516
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 22 additions and 18 deletions

View file

@ -204,6 +204,16 @@ class TestPerformance(IntegrationTestCase):
with self.assertRedisCallCounts(1):
frappe.get_doc("User", "Administrator")
def test_one_time_setup(self):
site = frappe.local.site
frappe.init(site, force=True)
run = frappe.qb._BuilderClasss.run
frappe.init(site, force=True)
patched_run = frappe.qb._BuilderClasss.run
self.assertIs(run, patched_run, "frappe.init should run one-time patching code just once")
@run_only_if(db_type_is.MARIADB)
class TestOverheadCalls(FrappeAPITestCase):

View file

@ -1,8 +1,7 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. Check LICENSE
import datetime
import json
import time
from collections import defaultdict
from collections.abc import Callable
from functools import wraps
@ -113,33 +112,28 @@ def site_cache(ttl: int | None = None, maxsize: int | None = None) -> Callable:
if ttl is not None and not callable(ttl):
func.ttl = ttl
func.expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
seconds=func.ttl
)
func.expiration = time.monotonic() + func.ttl
if maxsize is not None and not callable(maxsize):
func.maxsize = maxsize
@wraps(func)
def site_cache_wrapper(*args, **kwargs):
if getattr(frappe.local, "initialised", None):
func_call_key = json.dumps((args, kwargs))
if site := getattr(frappe.local, "site", None):
func_call_key = __generate_request_cache_key(args, kwargs)
if hasattr(func, "ttl") and datetime.datetime.now(datetime.timezone.utc) >= func.expiration:
if hasattr(func, "ttl") and time.monotonic() >= func.expiration:
func.clear_cache()
func.expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
seconds=func.ttl
)
func.expiration = time.monotonic() + func.ttl
if hasattr(func, "maxsize") and len(_SITE_CACHE[func_key][frappe.local.site]) >= func.maxsize:
_SITE_CACHE[func_key][frappe.local.site].pop(
next(iter(_SITE_CACHE[func_key][frappe.local.site])), None
)
if hasattr(func, "maxsize") and len(_SITE_CACHE[func_key][site]) >= func.maxsize:
# Note: This implements FIFO eviction policty
_SITE_CACHE[func_key][site].pop(next(iter(_SITE_CACHE[func_key][site])), None)
if func_call_key not in _SITE_CACHE[func_key][frappe.local.site]:
_SITE_CACHE[func_key][frappe.local.site][func_call_key] = func(*args, **kwargs)
if func_call_key not in _SITE_CACHE[func_key][site]:
_SITE_CACHE[func_key][site][func_call_key] = func(*args, **kwargs)
return _SITE_CACHE[func_key][frappe.local.site][func_call_key]
return _SITE_CACHE[func_key][site][func_call_key]
return func(*args, **kwargs)