From e768f679ff9655c3546ee4ec94ee619ca4f17f68 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Jan 2025 18:11:27 +0530 Subject: [PATCH 01/11] perf: Speedup rendering of simple templates Also, slowdown rendering of complex ones. Nothing comes free. --- frappe/website/page_renderers/template_page.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frappe/website/page_renderers/template_page.py b/frappe/website/page_renderers/template_page.py index d3b2b07506..55243ab5db 100644 --- a/frappe/website/page_renderers/template_page.py +++ b/frappe/website/page_renderers/template_page.py @@ -148,7 +148,7 @@ class TemplatePage(BaseTemplatePage): def setup_template_source(self): """Setup template source, frontmatter and markdown conversion""" - self.source = self.get_raw_template() + self.original_source = self.source = self.get_raw_template() self.extract_frontmatter() self.convert_from_markdown() @@ -233,7 +233,10 @@ class TemplatePage(BaseTemplatePage): else: safe_render = True - html = frappe.render_template(self.source, self.context, safe_render=safe_render) + src_modified = self.source is not self.original_source + html = frappe.render_template( + self.source if src_modified else self.context.template, self.context, safe_render=safe_render + ) return html From a89fd99794a32a79a8e92f696c526496dc32ac2f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Jan 2025 18:18:19 +0530 Subject: [PATCH 02/11] perf: keep jloader across requests This doesn't have anything that needs to be created for each request. --- frappe/utils/jinja.py | 44 +++++++++++++++++++++++----------------- frappe/website/router.py | 2 +- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/frappe/utils/jinja.py b/frappe/utils/jinja.py index 6be55d7fcb..0dccfcde6a 100644 --- a/frappe/utils/jinja.py +++ b/frappe/utils/jinja.py @@ -1,5 +1,9 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import frappe +from frappe.utils.caching import site_cache + + def get_jenv(): import frappe @@ -136,30 +140,32 @@ def guess_is_path(template): def get_jloader(): + jloader = _get_jloader() + frappe.local.jloader = jloader # backward compat + return jloader + + +@site_cache(ttl=10 * 60, maxsize=5) +def _get_jloader(): + from jinja2 import ChoiceLoader, PackageLoader, PrefixLoader + import frappe - if not getattr(frappe.local, "jloader", None): - from jinja2 import ChoiceLoader, PackageLoader, PrefixLoader + apps = frappe.get_hooks("template_apps") + if not apps: + apps = list(reversed(frappe.get_installed_apps(_ensure_on_bench=True))) - apps = frappe.get_hooks("template_apps") - if not apps: - apps = list( - reversed( - frappe.local.flags.web_pages_apps or frappe.get_installed_apps(_ensure_on_bench=True) - ) - ) + if "frappe" not in apps: + apps.append("frappe") - if "frappe" not in apps: - apps.append("frappe") + jloader = ChoiceLoader( + # search for something like app/templates/... + [PrefixLoader({app: PackageLoader(app, ".") for app in apps})] + # search for something like templates/... + + [PackageLoader(app, ".") for app in apps] + ) - frappe.local.jloader = ChoiceLoader( - # search for something like app/templates/... - [PrefixLoader({app: PackageLoader(app, ".") for app in apps})] - # search for something like templates/... - + [PackageLoader(app, ".") for app in apps] - ) - - return frappe.local.jloader + return jloader def set_filters(jenv): diff --git a/frappe/website/router.py b/frappe/website/router.py index ac0ecc951b..766d13fb6d 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -89,7 +89,7 @@ def get_pages(app=None): if app: apps = [app] else: - apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps() + apps = frappe.get_installed_apps() for app in apps: app_path = frappe.get_app_path(app) From 80f324cc0429cd8b56f0e195a0273cca0f22b98e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Jan 2025 18:24:38 +0530 Subject: [PATCH 03/11] perf: cache simple jinja templates --- frappe/utils/jinja.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frappe/utils/jinja.py b/frappe/utils/jinja.py index 0dccfcde6a..0759ae9804 100644 --- a/frappe/utils/jinja.py +++ b/frappe/utils/jinja.py @@ -92,11 +92,11 @@ def render_template(template, context=None, is_path=None, safe_render=True): if context is None: context = {} - jenv: SandboxedEnvironment = get_jenv() if is_path or guess_is_path(template): is_path = True - compiled_template = jenv.get_template(template) + compiled_template = compile_template(template) else: + jenv: SandboxedEnvironment = get_jenv() if safe_render and ".__" in template: throw(_("Illegal template")) try: @@ -145,7 +145,13 @@ def get_jloader(): return jloader -@site_cache(ttl=10 * 60, maxsize=5) +@site_cache(ttl=10 * 60, maxsize=16) +def compile_template(path): + jenv = get_jenv() + return jenv.get_template(path) + + +@site_cache(ttl=10 * 60, maxsize=8) def _get_jloader(): from jinja2 import ChoiceLoader, PackageLoader, PrefixLoader From a8265bdc307cdb3907024c12aedd1c6c1eaac182 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Jan 2025 18:45:08 +0530 Subject: [PATCH 04/11] perf: avoid unnecessary query for user type WHY? --- frappe/www/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/www/app.py b/frappe/www/app.py index 4414b5a849..114bd62292 100644 --- a/frappe/www/app.py +++ b/frappe/www/app.py @@ -23,7 +23,7 @@ def get_context(context): frappe.msgprint(_("Log in to access this page.")) frappe.redirect(f"/login?{urlencode({'redirect-to': frappe.request.path})}") - elif frappe.db.get_value("User", frappe.session.user, "user_type", order_by=None) == "Website User": + elif frappe.session.data.user_type == "Website User": frappe.throw(_("You are not permitted to access this page."), frappe.PermissionError) try: From f963758a1d9d20a9f6839b4519bb4b2250bd0508 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Jan 2025 18:48:28 +0530 Subject: [PATCH 05/11] perf: Avoid duplicate queries --- frappe/sessions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/sessions.py b/frappe/sessions.py index d903ba7896..d73f31f18b 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -175,7 +175,7 @@ def get(): "default_path": get_default_path() or "", } - bootinfo["desk_theme"] = frappe.db.get_value("User", frappe.session.user, "desk_theme") or "Light" + bootinfo["desk_theme"] = frappe.get_cached_value("User", frappe.session.user, "desk_theme") or "Light" bootinfo["user"]["impersonated_by"] = frappe.session.data.get("impersonated_by") bootinfo["navbar_settings"] = frappe.get_cached_doc("Navbar Settings") bootinfo.has_app_updates = has_app_update_notifications() From 8682014259ec1895ac239cc4404182788ff09515 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Jan 2025 18:49:19 +0530 Subject: [PATCH 06/11] perf: use client cached navbar --- frappe/sessions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/sessions.py b/frappe/sessions.py index d73f31f18b..272505d91b 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -177,7 +177,7 @@ def get(): bootinfo["desk_theme"] = frappe.get_cached_value("User", frappe.session.user, "desk_theme") or "Light" bootinfo["user"]["impersonated_by"] = frappe.session.data.get("impersonated_by") - bootinfo["navbar_settings"] = frappe.get_cached_doc("Navbar Settings") + bootinfo["navbar_settings"] = frappe.client_cache.get_doc("Navbar Settings") bootinfo.has_app_updates = has_app_update_notifications() return bootinfo From 9bd6c95b3f8d9ab0412699cf7c78a828bda561d9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Jan 2025 18:52:50 +0530 Subject: [PATCH 07/11] perf: don't yaml-parse empty string and get nothing back *surprise* --- frappe/website/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/website/utils.py b/frappe/website/utils.py index e8587f551f..94a923729e 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -423,7 +423,7 @@ def get_frontmatter(string): body = result.group(2) return { - "attributes": yaml.safe_load(frontmatter), + "attributes": yaml.safe_load(frontmatter) if frontmatter else "", "body": body, } From dc7636de8fe4297d94150d738e45719b47d81121 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Jan 2025 19:01:31 +0530 Subject: [PATCH 08/11] perf: use cached user document --- frappe/utils/user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/utils/user.py b/frappe/utils/user.py index 872fa8dca8..355d968eb7 100644 --- a/frappe/utils/user.py +++ b/frappe/utils/user.py @@ -287,8 +287,8 @@ def get_user_fullname(user: str) -> str: def get_fullname_and_avatar(user: str) -> _dict: - first_name, last_name, avatar, name = frappe.db.get_value( - "User", user, ["first_name", "last_name", "user_image", "name"], order_by=None + first_name, last_name, avatar, name = frappe.get_cached_value( + "User", user, ["first_name", "last_name", "user_image", "name"] ) return _dict( { From 0ff3e6fd4c4943069d85fca003a05232d4581048 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Jan 2025 19:19:02 +0530 Subject: [PATCH 09/11] perf: cache meta tags existence --- .../doctype/website_route_meta/website_route_meta.py | 6 ++++++ frappe/website/website_components/metatags.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/frappe/website/doctype/website_route_meta/website_route_meta.py b/frappe/website/doctype/website_route_meta/website_route_meta.py index 9164196b13..0846ec130f 100644 --- a/frappe/website/doctype/website_route_meta/website_route_meta.py +++ b/frappe/website/doctype/website_route_meta/website_route_meta.py @@ -20,3 +20,9 @@ class WebsiteRouteMeta(Document): def autoname(self): if self.name and self.name.startswith("/"): self.name = self.name[1:] + + def clear_cache(self): + from frappe.website.website_components.metatags import has_meta_tags + + has_meta_tags.clear_cache() + return super().clear_cache() diff --git a/frappe/website/website_components/metatags.py b/frappe/website/website_components/metatags.py index 3d35be5514..7708a5d42e 100644 --- a/frappe/website/website_components/metatags.py +++ b/frappe/website/website_components/metatags.py @@ -1,4 +1,5 @@ import frappe +from frappe.utils.caching import site_cache METATAGS = ("title", "description", "image", "author", "published_on") @@ -58,14 +59,17 @@ class MetaTags: route = self.path if route == "": # homepage - route = frappe.db.get_single_value("Website Settings", "home_page") + route = frappe.get_website_settings("home_page") - route_exists = ( - route and not route.endswith((".js", ".css")) and frappe.db.exists("Website Route Meta", route) - ) + route_exists = route and not route.endswith((".js", ".css")) and has_meta_tags(route) if route_exists: website_route_meta = frappe.get_doc("Website Route Meta", route) for meta_tag in website_route_meta.meta_tags: d = meta_tag.get_meta_dict() self.tags.update(d) + + +@site_cache(ttl=10 * 60, maxsize=16) +def has_meta_tags(route): + return bool(frappe.db.exists("Website Route Meta", route)) From 129212a916ea01960615c70d2354bbbf0747e6e6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Jan 2025 19:31:46 +0530 Subject: [PATCH 10/11] perf: cache unseen notes --- frappe/boot.py | 19 ----------------- frappe/desk/doctype/note/note.py | 36 ++++++++++++++++++++++++++++++++ frappe/sessions.py | 3 ++- frappe/tests/test_boot.py | 4 ++-- 4 files changed, 40 insertions(+), 22 deletions(-) diff --git a/frappe/boot.py b/frappe/boot.py index d20aa5b12e..3ad9ec800d 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -385,25 +385,6 @@ def load_print_css(bootinfo, print_settings): ) -def get_unseen_notes(): - note = DocType("Note") - nsb = DocType("Note Seen By").as_("nsb") - - return ( - frappe.qb.from_(note) - .select(note.name, note.title, note.content, note.notify_on_every_login) - .where( - (note.notify_on_login == 1) - & (note.expire_notification_on > frappe.utils.now()) - & ( - ParameterizedValueWrapper(frappe.session.user).notin( - SubQuery(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)) - ) - ) - ) - ).run(as_dict=1) - - def get_success_action(): return frappe.get_all("Success Action", fields=["*"]) diff --git a/frappe/desk/doctype/note/note.py b/frappe/desk/doctype/note/note.py index e303f9b80f..e650b48b12 100644 --- a/frappe/desk/doctype/note/note.py +++ b/frappe/desk/doctype/note/note.py @@ -4,6 +4,8 @@ import frappe from frappe.model.document import Document +UNSEEN_NOTES_KEY = "unseen_notes::" + class Note(Document): # begin: auto-generated types @@ -39,6 +41,10 @@ class Note(Document): self.print_heading = self.name self.sub_heading = "" + def clear_cache(self): + frappe.cache.delete_keys(UNSEEN_NOTES_KEY) + return super().clear_cache() + def mark_seen_by(self, user: str) -> None: if user in [d.user for d in self.seen_by]: return @@ -62,3 +68,33 @@ def get_permission_query_conditions(user): def has_permission(doc, user): return bool(doc.public or doc.owner == user) + + +def get_unseen_notes(): + from frappe.query_builder.terms import ParameterizedValueWrapper, SubQuery + + def _get_unseen_notes(): + note = frappe.qb.DocType("Note") + nsb = frappe.qb.DocType("Note Seen By").as_("nsb") + + return ( + frappe.qb.from_(note) + .select(note.name, note.title, note.content, note.notify_on_every_login) + .where( + (note.notify_on_login == 1) + & (note.expire_notification_on > frappe.utils.now()) + & ( + ParameterizedValueWrapper(frappe.session.user).notin( + SubQuery(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)) + ) + ) + ) + ).run(as_dict=1) + + return ( + frappe.cache.get_value( + f"{UNSEEN_NOTES_KEY}{frappe.session.user}", + generator=_get_unseen_notes, + ) + or [] + ) diff --git a/frappe/sessions.py b/frappe/sessions.py index 272505d91b..56595e4502 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -126,7 +126,8 @@ def clear_expired_sessions(): def get(): """get session boot info""" - from frappe.boot import get_bootinfo, get_unseen_notes + from frappe.boot import get_bootinfo + from frappe.desk.doctype.note.note import get_unseen_notes from frappe.utils.change_log import get_change_log bootinfo = None diff --git a/frappe/tests/test_boot.py b/frappe/tests/test_boot.py index e7b04a4033..b28cfa6ff1 100644 --- a/frappe/tests/test_boot.py +++ b/frappe/tests/test_boot.py @@ -1,6 +1,6 @@ import frappe -from frappe.boot import get_unseen_notes, get_user_pages_or_reports -from frappe.desk.doctype.note.note import mark_as_seen +from frappe.boot import get_user_pages_or_reports +from frappe.desk.doctype.note.note import get_unseen_notes, mark_as_seen from frappe.tests import IntegrationTestCase From 01f978773651791b201051912b3fd88d6b765a9b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Jan 2025 20:04:10 +0530 Subject: [PATCH 11/11] fix: catch template error for on-disk paths too --- frappe/utils/jinja.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/frappe/utils/jinja.py b/frappe/utils/jinja.py index 0759ae9804..4e5a2b6a1a 100644 --- a/frappe/utils/jinja.py +++ b/frappe/utils/jinja.py @@ -92,22 +92,23 @@ def render_template(template, context=None, is_path=None, safe_render=True): if context is None: context = {} - if is_path or guess_is_path(template): - is_path = True - compiled_template = compile_template(template) - else: - jenv: SandboxedEnvironment = get_jenv() - if safe_render and ".__" in template: - throw(_("Illegal template")) - try: - compiled_template = jenv.from_string(template) - except TemplateError: - import html + try: + if is_path or guess_is_path(template): + is_path = True + compiled_template = compile_template(template) + else: + jenv: SandboxedEnvironment = get_jenv() + if safe_render and ".__" in template: + throw(_("Illegal template")) - throw( - title="Jinja Template Error", - msg=f"
{template}
{html.escape(get_traceback())}
", - ) + compiled_template = jenv.from_string(template) + except TemplateError: + import html + + throw( + title="Jinja Template Error", + msg=f"
{template}
{html.escape(get_traceback())}
", + ) import time