diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 643e32cc..83014e53 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -467,6 +467,10 @@ class Server(): implementation.on_after_startup() pluginLifecycleManager.add_callback("enabled", call_on_after_startup) + # when we are through with that we also run our preemptive cache + if settings().getBoolean(["devel", "cache", "preemptive"]): + self._execute_preemptive_flask_caching() + import threading threading.Thread(target=work).start() ioloop.add_callback(on_after_startup) @@ -526,7 +530,7 @@ class Server(): if default_language is not None and not default_language == "_default" and default_language in LANGUAGES: return Locale.negotiate([default_language], LANGUAGES) - return request.accept_languages.best_match(LANGUAGES) + return Locale.parse(request.accept_languages.best_match(LANGUAGES)) def _setup_logging(self, debug, logConf=None): defaultConfig = { @@ -663,6 +667,30 @@ class Server(): self._register_template_plugins() + def _execute_preemptive_flask_caching(self): + from octoprint.server.util.flask import get_preemptive_cache_data + from werkzeug.test import EnvironBuilder + + cache_data = get_preemptive_cache_data() + if not cache_data: + return + + def execute_caching(): + for route in sorted(cache_data.keys(), key=lambda x: (x.count("/"), x)): + entries = cache_data[route] + for kwargs in entries: + try: + self._logger.info("Preemptively caching {} for {!r}".format(route, kwargs)) + builder = EnvironBuilder(**kwargs) + app(builder.get_environ(), lambda *a, **kw: None) + except: + self._logger.exception("Error while trying to preemptively cache {} for {!r}".format(route, kwargs)) + + import threading + cache_thread = threading.Thread(target=execute_caching, name="Preemptive Cache Worker") + cache_thread.daemon = True + cache_thread.start() + def _register_template_plugins(self): template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin) for plugin in template_plugins: diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index ce59f6d6..0a83f690 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -376,6 +376,69 @@ def cache_check_response_headers(response): return False +_preemptive_flask_cache = "preemptive_flask_cache.yaml" + +def preemptively_cached(data, unless=None): + def decorator(f): + @functools.wraps(f) + def decorated_function(*args, **kwargs): + if not (callable(unless) and unless()): + entry_data = data + if callable(entry_data): + entry_data = entry_data() + + if entry_data is not None: + from flask import request + from octoprint.util import atomic_write + import yaml + + data_folder = settings().getBaseFolder("data") + cache_data_file = os.path.join(data_folder, _preemptive_flask_cache) + cache_data = get_preemptive_cache_data() + + if not request.path in cache_data: + cache_data[request.path] = [] + + cache_data_for_path = cache_data.get(request.path, []) + if all(map(lambda entry: entry_data != entry, cache_data_for_path)): + logging.getLogger(__name__).info("Adding {} for {!r} to views to preemptively cache".format(request.path, entry_data)) + cache_data[request.path] = cache_data_for_path + [entry_data] + try: + with atomic_write(cache_data_file, "wb", prefix="octoprint-{}-".format(_preemptive_flask_cache[:-len(".yaml")]), suffix=".yaml") as handle: + yaml.safe_dump(cache_data, handle,default_flow_style=False, indent=" ", allow_unicode=True) + except: + logging.getLogger(__name__).exception("Error while writing {}".format(_preemptive_flask_cache)) + + return f(*args, **kwargs) + + return decorated_function + + return decorator + + +def get_preemptive_cache_data(root=None): + import yaml + + data_folder = settings().getBaseFolder("data") + cache_data_file = os.path.join(data_folder, _preemptive_flask_cache) + if not os.path.isfile(cache_data_file): + return dict() + + cache_data = None + try: + with open(cache_data_file, "r") as f: + cache_data = yaml.safe_load(f) + except: + logging.getLogger(__name__).exception("Error while reading {}".format(_preemptive_flask_cache)) + + if cache_data is None: + cache_data = dict() + + if root: + return cache_data.get(root, dict()) + else: + return cache_data + def add_non_caching_response_headers(response): response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0" diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py index 3399a740..34d701a8 100644 --- a/src/octoprint/server/views.py +++ b/src/octoprint/server/views.py @@ -29,10 +29,11 @@ _valid_div_re = re.compile("[a-zA-Z_-]+") @app.route("/") @util.flask.cached(timeout=-1, refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, - key=lambda: "view:{}:{}".format(request.base_url, g.locale), + key=lambda: "view:{}:{}".format(request.base_url, g.locale.language if g.locale else "default"), unless_response=util.flask.cache_check_response_headers) +@util.flask.preemptively_cached(data=lambda: dict(path=request.path, base_url=request.url_root, query_string="l10n={}".format(g.locale.language)) if g.locale else None, + unless=lambda: request.url_root in settings().get(["server", "preemptiveCache", "exceptions"])) def index(): - #~~ a bunch of settings enable_gcodeviewer = settings().getBoolean(["gcodeViewer", "enabled"]) @@ -404,7 +405,9 @@ def robotsTxt(): @app.route("/i18n//.js") @util.flask.cached(timeout=-1, refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, - key=lambda: "view:{}:{}".format(request.base_url, g.locale)) + key=lambda: "view:{}".format(request.base_url)) +@util.flask.preemptively_cached(data=lambda: dict(path=request.path, base_url=request.url_root) if g.locale else None, + unless=lambda: request.url_root in settings().get(["server", "preemptiveCache", "exceptions"])) def localeJs(locale, domain): messages = dict() plural_expr = None diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index a7137e14..ffd972db 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -114,6 +114,9 @@ default_settings = { "diskspace": { "warning": 500 * 1024 * 1024, # 500 MB "critical": 200 * 1024 * 1024, # 200 MB + }, + "preemptiveCache": { + "exceptions": [] } }, "webcam": { @@ -262,7 +265,8 @@ default_settings = { "devel": { "stylesheet": "css", "cache": { - "enabled": True + "enabled": True, + "preemptive": True }, "webassets": { "minify": False,