From b857a4099a110603e03f973f335a63de00a03eb7 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Sat, 21 Jun 2025 21:44:11 +0530 Subject: [PATCH] perf: use `orjson` for faster request processing --- frappe/__init__.py | 3 ++- frappe/app.py | 5 ++--- frappe/utils/response.py | 33 ++++++++++++++++++++++----------- pyproject.toml | 1 + 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 8f5e2c436f..4f8e8b6e88 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -34,6 +34,7 @@ from typing import ( ) import click +import orjson from werkzeug.datastructures import Headers import frappe @@ -1265,7 +1266,7 @@ def get_installed_apps(*, _ensure_on_bench: bool = False) -> list[str]: if not db: connect() - installed = json.loads(db.get_global("installed_apps") or "[]") + installed = orjson.loads(db.get_global("installed_apps") or "[]") if _ensure_on_bench: all_apps = cache.get_value("all_apps", get_all_apps) diff --git a/frappe/app.py b/frappe/app.py index 04e06171ef..1bad66a891 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -5,6 +5,7 @@ import functools import logging import os +import orjson from werkzeug.exceptions import HTTPException, NotFound from werkzeug.middleware.profiler import ProfilerMiddleware from werkzeug.middleware.proxy_fix import ProxyFix @@ -297,11 +298,9 @@ def set_cors_headers(response): def make_form_dict(request: Request): - import json - request_data = request.get_data(as_text=True) if request_data and request.is_json: - args = json.loads(request_data) + args = orjson.loads(request_data) else: args = {} args.update(request.args or {}) diff --git a/frappe/utils/response.py b/frappe/utils/response.py index dac5237c76..77ce8364f0 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -2,18 +2,19 @@ # License: MIT. See LICENSE import datetime -import decimal -import json +import functools import mimetypes import os import sys -import uuid from collections.abc import Iterable +from decimal import Decimal from pathlib import Path from re import Match from typing import TYPE_CHECKING from urllib.parse import quote +from uuid import UUID +import orjson import werkzeug.utils from werkzeug.exceptions import Forbidden, NotFound from werkzeug.local import LocalProxy @@ -32,6 +33,7 @@ if TYPE_CHECKING: from frappe.core.doctype.file.file import File DateOrTimeTypes = datetime.date | datetime.datetime | datetime.time +timedelta = datetime.timedelta def report_error(status_code): @@ -148,7 +150,7 @@ def as_json(): del frappe.local.response["http_status_code"] response.mimetype = "application/json" - response.data = json.dumps(frappe.local.response, default=json_handler, separators=(",", ":")) + response.data = dump_response(frappe.local.response) return response @@ -191,13 +193,15 @@ def _make_logs_v1(): if frappe.error_log and is_traceback_allowed(): if source := guess_exception_source(frappe.local.error_log and frappe.local.error_log[0]["exc"]): response["_exc_source"] = source - response["exc"] = json.dumps([frappe.utils.cstr(d["exc"]) for d in frappe.local.error_log]) + response["exc"] = orjson.dumps([frappe.utils.cstr(d["exc"]) for d in frappe.local.error_log]).decode() if frappe.local.message_log: - response["_server_messages"] = json.dumps([json.dumps(d) for d in frappe.local.message_log]) + response["_server_messages"] = orjson.dumps( + [orjson.dumps(d).decode() for d in frappe.local.message_log] + ).decode() if frappe.debug_log: - response["_debug_messages"] = json.dumps(frappe.local.debug_log) + response["_debug_messages"] = orjson.dumps(frappe.local.debug_log).decode() if frappe.flags.error_message: response["_error_message"] = frappe.flags.error_message @@ -219,7 +223,7 @@ def json_handler(obj): if isinstance(obj, DateOrTimeTypes): return str(obj) - elif isinstance(obj, datetime.timedelta): + elif isinstance(obj, timedelta): return format_timedelta(obj) elif isinstance(obj, LocalProxy): @@ -231,7 +235,7 @@ def json_handler(obj): elif isinstance(obj, Iterable): return list(obj) - elif isinstance(obj, decimal.Decimal): + elif isinstance(obj, Decimal): return float(obj) elif isinstance(obj, Match): @@ -243,16 +247,23 @@ def json_handler(obj): elif callable(obj): return repr(obj) - elif isinstance(obj, uuid.UUID): + elif isinstance(obj, Path): return str(obj) - elif isinstance(obj, Path): + # orjson does this already + # but json_handler needs to be compatible with built-in json module also + elif isinstance(obj, UUID): return str(obj) else: raise TypeError(f"""Object of type {type(obj)} with value of {obj!r} is not JSON serializable""") +dump_response = functools.partial( + orjson.dumps, default=json_handler, option=orjson.OPT_PASSTHROUGH_DATETIME | orjson.OPT_NON_STR_KEYS +) + + def as_page(): """print web page""" from frappe.website.serve import get_response diff --git a/pyproject.toml b/pyproject.toml index d9fcd21a32..8187e872e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ dependencies = [ "num2words~=0.5.14", "oauthlib~=3.2.2", "openpyxl~=3.1.5", + "orjson~=3.10.18", "passlib~=1.7.4", "pdfkit~=1.0.0", "phonenumbers~=9.0.7",