Skip to content

Commit

Permalink
SHOT-4407: Improve Alias start up (#77)
Browse files Browse the repository at this point in the history
* Add option to load api and cache on server side
* Modify the client to call server to load the api
* Pop the timeout kwarg because the base class does not accept it
  • Loading branch information
staceyoue authored Dec 6, 2024
1 parent e353c41 commit 112affc
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 49 deletions.
57 changes: 13 additions & 44 deletions python/tk_framework_alias/client/socketio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,14 @@
# agreement to the ShotGrid Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Autodesk, Inc.

import filecmp
import json
import os
import shutil
import logging
import socketio
import threading

from .client_json import AliasClientJSON
from ..utils.decorators import check_server_result, check_client_connection
from tk_framework_alias_utils import utils as framework_utils
from tk_framework_alias_utils import environment_utils as framework_env_utils


class AliasSocketIoClient(socketio.Client):
Expand Down Expand Up @@ -58,10 +55,10 @@ def __init__(self, *args, **kwargs):
self.__class__.__name__, "sio_client"
)

super(AliasSocketIoClient, self).__init__(*args, **kwargs)

# The connection timeout in seconds
self.__timeout = kwargs.get("timeout", 20)
self.__timeout = kwargs.pop("timeout", 20)

super(AliasSocketIoClient, self).__init__(*args, **kwargs)

# The callbacks registry. Callback functions passed to the server are stored in the
# client by their id, such that they can be looked up and executed when the server
Expand Down Expand Up @@ -328,43 +325,15 @@ def get_alias_api_module_proxy(self):
:rtype: AliasClientModuleProxyWrapper
"""

# Get information about the api module
api_info = self.call_threadsafe("get_alias_api_info")

# Get the cached files for the api module
filename = os.path.basename(api_info["file_path"]).split(".")[0]
cache_filepath = framework_env_utils.get_alias_api_cache_file_path(
filename, api_info["alias_version"], api_info["python_version"]
)
api_ext = os.path.splitext(api_info["file_path"])[1]
cache_api_filepath = os.path.join(
os.path.dirname(cache_filepath),
f"{os.path.splitext(cache_filepath)[0]}{api_ext}",
)

cache_loaded = False
if os.path.exists(cache_filepath) and os.path.exists(cache_api_filepath):
# The cache exists, check if it requires updating before using it.
if filecmp.cmp(api_info["file_path"], cache_api_filepath):
# The cache is still up to date, load it in.
with open(cache_filepath, "r") as fp:
module_proxy = json.load(fp, cls=self.get_json_decoder())
cache_loaded = True

if not cache_loaded:
cache_folder = os.path.dirname(cache_filepath)
if not os.path.exists(cache_folder):
os.mkdir(cache_folder)
# The api was not loaded from cache, make a server request to get the api module,
# and cache it
module_proxy = self.call_threadsafe("get_alias_api")
with open(cache_filepath, "w") as fp:
json.dump(module_proxy, fp=fp, cls=self.get_json_encoder())
# Copy the api module to the cache folder in order to determine next time if the
# cache requies an update
shutil.copyfile(api_info["file_path"], cache_api_filepath)

return module_proxy
# The server will JSON-serialize the Alias Python API module to a local
# file on disk. The server will return the path to this file for this
# client to read and load the module from. This is done to avoid sending
# the entire module over the network, which can be slow.
module_filepath = self.call_threadsafe("load_alias_api", timeout=self.__timeout)
with open(module_filepath, "r") as fp:
module_proxy = json.load(fp, cls=self.get_json_decoder())
self.logger.log(logging.DEBUG, module_proxy)
return module_proxy

def get_alias_api(self):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.

import filecmp
import logging
import json
import pprint
import os
import shutil
import socketio

from ...api import alias_api
from tk_framework_alias_utils import environment_utils

from ..api_request import AliasApiRequestWrapper
from ... import alias_bridge
from ...api import alias_api
from ..api_request import AliasApiRequestWrapper
from ..server_json import AliasServerJSON
from ...utils.invoker import execute_in_main_thread
from ...utils.exceptions import (
AliasApiRequestException,
Expand Down Expand Up @@ -172,7 +177,7 @@ def on_get_alias_api(self, sid):
"""
Get the Alias API module.
The module will be JSON-serialized before returning to the client.
The module will be JSON-serialized and returned to the client.
:param sid: The client session id that made the request.
:type sid: str
Expand All @@ -186,6 +191,70 @@ def on_get_alias_api(self, sid):

return alias_api

def on_load_alias_api(self, sid):
"""
Load the Alias API and JSON-serialize the module to file on disk.
This is an alternative method to getting the Alias API module
and returning the module directly to the client. In this method, the
module is JSON-serialized, saved locally to disk, and the file path
to the JSON-serialized module is returned to the client. The client
can then load the JSON-serialized module from the file.
This is a more efficient way for the client to obtain the Alias API
module because the module data is not sent over the network, which will
only become slower as the Alias API module grows (e.g. new functionality
is added).
:param sid: The client session id that made the request.
:type sid: str
:return: The file path to the JSON-serialized Alias API module.
:rtype: str
"""

if self.client_sid is None or sid != self.client_sid:
return

# Get the directory and path to the Alias API cached file. The cache
# file name will have format:
# {alias_api_module_name}{alias_version}_{python_version}.json
api_info = self.on_get_alias_api_info(sid)
api_filename, api_ext = os.path.splitext(
os.path.basename(api_info["file_path"])
)
cache_filepath = environment_utils.get_alias_api_cache_file_path(
api_filename, api_info["alias_version"], api_info["python_version"]
)
cache_dir = os.path.dirname(cache_filepath)

# Get the path to the Alias API module (.pyd) that was used to create
# the cache. The cache module file name will have format:
# {alias_api_module_name}{alias_version}_{python_version}.pyd
base_cache_module_filename, _ = os.path.splitext(
os.path.basename(cache_filepath)
)
cache_module_filename = f"{base_cache_module_filename}.{api_ext}"
cache_module_filepath = os.path.join(cache_dir, cache_module_filename)

# Check if the cache is up-to-date. If not, create a new cache
if (
not os.path.exists(cache_filepath)
or not os.path.exists(cache_module_filepath)
or not filecmp.cmp(api_info["file_path"], cache_module_filepath)
):
# Ensure the cache directory exists
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
# Create the Alias API cache
with open(cache_filepath, "w") as fp:
json.dump(alias_api, fp=fp, cls=AliasServerJSON.encoder_class())
# Copy the module to the cache folder in order to determine next time if the
# cache requies an update
shutil.copyfile(api_info["file_path"], cache_module_filepath)

# Return the path to the Alias API cache file
return cache_filepath

def on_get_alias_api_info(self, sid):
"""
Get the Alias API module info.
Expand Down
14 changes: 12 additions & 2 deletions python/tk_framework_alias/server/socketio/server_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,23 @@
class AliasServerJSON:
"""A custom json module to handle serializing Alias API objects to JSON."""

@staticmethod
def encoder_class():
"""Return the encoder class used by this JSON module."""
return AliasServerJSONEncoder

@staticmethod
def decoder_class():
"""Return the decoder class used by this JSON module."""
return AliasServerJSONDecoder

@staticmethod
def dumps(obj, *args, **kwargs):
return json.dumps(obj, cls=AliasServerJSONEncoder, *args, **kwargs)
return json.dumps(obj, cls=AliasServerJSON.encoder_class(), *args, **kwargs)

@staticmethod
def loads(obj, *args, **kwargs):
return json.loads(obj, cls=AliasServerJSONDecoder, *args, **kwargs)
return json.loads(obj, cls=AliasServerJSON.decoder_class(), *args, **kwargs)


class AliasServerJSONEncoder(json.JSONEncoder):
Expand Down

0 comments on commit 112affc

Please sign in to comment.