From 18934c3a4566a201f1f5dcf7425420d699fb6ab1 Mon Sep 17 00:00:00 2001 From: Robert Nishihara Date: Fri, 7 Oct 2016 11:00:46 -0700 Subject: [PATCH] Make photon client into a C extension. (#8) * Make photon client into a C extension. * Fix formatting. * Rename extension from PhotonClient to Photon. * Update common submodule. * Fix Makefile to compile with fPIC. * Update common submodule. * Compile C extension against common. * Fix formatting. * Remove unnecessary include. * Update common submodule and rename Photon -> PhotonClient. * Drop global interpretor lock during get_task. --- .travis.yml | 12 ++- Makefile | 9 ++- common | 2 +- install-dependencies.sh | 21 +++++ lib/python/photon.py | 99 ------------------------ lib/python/photon_extension.c | 140 ++++++++++++++++++++++++++++++++++ lib/python/setup.py | 14 ++++ test/test.py | 55 +++++++++++-- 8 files changed, 238 insertions(+), 114 deletions(-) create mode 100755 install-dependencies.sh delete mode 100644 lib/python/photon.py create mode 100644 lib/python/photon_extension.c create mode 100644 lib/python/setup.py diff --git a/.travis.yml b/.travis.yml index caab2e356..360027785 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,11 +44,17 @@ matrix: - python test/test.py valgrind install: + - ./install-dependencies.sh - make - -script: + - cd common/lib/python + - python setup.py install --user + - cd ../../.. + - cd lib/python + - python setup.py install --user + - cd ../.. - cd common - make test - cd .. - - source setup-env.sh + +script: - python test/test.py diff --git a/Makefile b/Makefile index ef5de50a7..436502d6a 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ CC = gcc -CFLAGS = -g -Wall --std=c99 -D_XOPEN_SOURCE=500 -D_POSIX_C_SOURCE=200809L -Icommon/thirdparty +CFLAGS = -g -Wall --std=c99 -D_XOPEN_SOURCE=500 -D_POSIX_C_SOURCE=200809L -Icommon/thirdparty -fPIC BUILD = build -all: $(BUILD)/photon_scheduler $(BUILD)/photon_client.so +all: $(BUILD)/photon_scheduler $(BUILD)/photon_client.a -$(BUILD)/photon_client.so: photon_client.h photon_client.c common - $(CC) $(CFLAGS) photon_client.c common/build/libcommon.a -fPIC -shared -o $(BUILD)/photon_client.so +$(BUILD)/photon_client.a: photon_client.o + ar rcs $(BUILD)/photon_client.a photon_client.o $(BUILD)/photon_scheduler: photon.h photon_scheduler.c common $(CC) $(CFLAGS) -o $@ photon_scheduler.c common/build/libcommon.a common/thirdparty/hiredis/libhiredis.a -Icommon/thirdparty -Icommon/ @@ -17,5 +17,6 @@ common: FORCE clean: cd common; make clean rm -r $(BUILD)/* + rm *.o FORCE: diff --git a/common b/common index 4204500d2..7be1a93d6 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 4204500d23be7726e27598badb691d29d08a0ad7 +Subproject commit 7be1a93d64ca36fc639e11f81de1483e0bd17b8c diff --git a/install-dependencies.sh b/install-dependencies.sh new file mode 100755 index 000000000..f84da1684 --- /dev/null +++ b/install-dependencies.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE:-$0}")"; pwd) + +platform="unknown" +unamestr="$(uname)" +if [[ "$unamestr" == "Linux" ]]; then + echo "Platform is linux." + platform="linux" +elif [[ "$unamestr" == "Darwin" ]]; then + echo "Platform is macosx." + platform="macosx" +else + echo "Unrecognized platform." + exit 1 +fi + +if [[ $platform == "linux" ]]; then + sudo apt-get update + sudo apt-get install -y git python-dev +fi diff --git a/lib/python/photon.py b/lib/python/photon.py deleted file mode 100644 index 36f06ff2a..000000000 --- a/lib/python/photon.py +++ /dev/null @@ -1,99 +0,0 @@ -import ctypes -import os - -photon_client_library_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "../../build/photon_client.so") -photon_client_library = ctypes.cdll.LoadLibrary(photon_client_library_path) -photon_client_library.alloc_task_spec.restype = ctypes.c_void_p -photon_client_library.photon_connect.restype = ctypes.c_void_p -photon_client_library.photon_submit.restype = None -photon_client_library.photon_get_task.restype = ctypes.c_void_p - -ID = ctypes.c_ubyte * 20 - -buffer_from_read_write_memory = ctypes.pythonapi.PyBuffer_FromReadWriteMemory -buffer_from_read_write_memory.argtypes = [ctypes.c_void_p, ctypes.c_int64] -buffer_from_read_write_memory.restype = ctypes.py_object - -buffer_from_memory = ctypes.pythonapi.PyBuffer_FromMemory -buffer_from_memory.argtypes = [ctypes.c_void_p, ctypes.c_int64] -buffer_from_memory.restype = ctypes.py_object - -photon_client_library.task_function.restype = ctypes.c_void_p -photon_client_library.task_num_args.restype = ctypes.c_int64 -photon_client_library.task_num_returns.restype = ctypes.c_int64 -photon_client_library.task_arg_type.restype = ctypes.c_int8 -photon_client_library.task_arg_id.restype = ctypes.c_void_p -photon_client_library.task_arg_val.restype = ctypes.c_void_p -photon_client_library.task_arg_length.restype = ctypes.c_void_p -photon_client_library.task_return.restype = ctypes.c_void_p - - -class TaskInfo(object): - def __init__(self, function_id, args, return_ids): - self.function_id = function_id - self.args = args - self.return_ids = return_ids - -def extract_task(c_task): - function_id = buffer_from_memory(photon_client_library.task_function(c_task), 20)[:] - num_args = photon_client_library.task_num_args(c_task) - num_returns = photon_client_library.task_num_returns(c_task) - arg_vals_and_ids = [] - for i in range(num_args): - arg_type = photon_client_library.task_arg_type(c_task, i) - if arg_type == 0: - arg_id = buffer_from_memory(photon_client_library.task_arg_id(c_task, i), 20) - arg_vals_and_ids.append((arg_type, arg_id)) - elif arg_type == 1: - arg_val = photon_client_library.task_arg_val(c_task, i)[:] - arg_length = photon_client_library.task_arg_length(c_task, i) - arg_value = buffer_from_memory(arg_val, arg_length)[:] - arg_vals_and_ids.append((arg_type, arg_value)) - else: - raise Exception("arg_type must be 0 or 1") - return_ids = [] - for i in range(num_returns): - ret_id = buffer_from_memory(photon_client_library.task_return(c_task, i), 20) - return_ids.append(ret_id[:]) - return TaskInfo(function_id, arg_vals_and_ids, return_ids) - -class UniqueID(ctypes.Structure): - _fields_ = [("unique_id", ID)] - -def make_id(string): - if len(string) != 20: - raise Exception("PlasmaIDs must be 20 characters long") - unique_id = map(ord, string) - return UniqueID(unique_id=ID(*unique_id)) - -class Task(object): - def __init__(self, function_id, args, return_ids): - function_id = make_id(function_id) - self.task_spec = ctypes.c_void_p(photon_client_library.alloc_task_spec(function_id, len(args), 1, 0)) - for arg in args: - photon_client_library.task_args_add_ref(self.task_spec, make_id(arg)) - - # Add return IDs. This may not be the appropriate place for this. - num_returns = photon_client_library.task_num_returns(self.task_spec) - for i in range(num_returns): - ret_id = buffer_from_read_write_memory(photon_client_library.task_return(self.task_spec, i), 20) - for j in range(20): - ret_id[j] = return_ids[i][j] - - def __del__(self): - photon_client_library.free_task_spec(self.task_spec) - -class PhotonClient(object): - - def __init__(self, socket_name): - self.photon_conn = ctypes.c_void_p(photon_client_library.photon_connect(socket_name)) - - def submit(self, function_id, args, return_ids): - task = Task(function_id, args, return_ids) - photon_client_library.photon_submit(self.photon_conn, task.task_spec) - - def get_task(self): - c_task = ctypes.c_void_p(photon_client_library.photon_get_task(self.photon_conn)) - task = c_task # TODO Extract the actual task. EXTRACT...(c_task) - # photon_client_library.free_task_spec(c_task) - return extract_task(task) diff --git a/lib/python/photon_extension.c b/lib/python/photon_extension.c new file mode 100644 index 000000000..4aa199887 --- /dev/null +++ b/lib/python/photon_extension.c @@ -0,0 +1,140 @@ +#include + +#include "common_extension.h" +#include "photon_client.h" +#include "task.h" + +PyObject *PhotonError; + +// clang-format off +typedef struct { + PyObject_HEAD + photon_conn *photon_connection; +} PyPhotonClient; +// clang-format on + +static int PyPhotonClient_init(PyPhotonClient *self, PyObject *args, + PyObject *kwds) { + char *socket_name; + if (!PyArg_ParseTuple(args, "s", &socket_name)) { + return -1; + } + self->photon_connection = photon_connect(socket_name); + return 0; +} + +static void PyPhotonClient_dealloc(PyPhotonClient *self) { + free(((PyPhotonClient *)self)->photon_connection); + Py_TYPE(self)->tp_free((PyObject *)self); +} + +static PyObject *PyPhotonClient_submit(PyObject *self, PyObject *args) { + PyObject *py_task; + if (!PyArg_ParseTuple(args, "O", &py_task)) { + return NULL; + } + photon_submit(((PyPhotonClient *)self)->photon_connection, + ((PyTask *)py_task)->spec); + Py_RETURN_NONE; +} + +// clang-format off +static PyObject *PyPhotonClient_get_task(PyObject *self) { + task_spec *task_spec; + /* Drop the global interpreter lock while we get a task because + * photon_get_task may block for a long time. */ + Py_BEGIN_ALLOW_THREADS + task_spec = photon_get_task(((PyPhotonClient *)self)->photon_connection); + Py_END_ALLOW_THREADS + return PyTask_make(task_spec); +} +// clang-format on + +static PyMethodDef PyPhotonClient_methods[] = { + {"submit", (PyCFunction)PyPhotonClient_submit, METH_VARARGS, + "Submit a task to the local scheduler."}, + {"get_task", (PyCFunction)PyPhotonClient_get_task, METH_NOARGS, + "Get a task from the local scheduler."}, + {NULL} /* Sentinel */ +}; + +static PyTypeObject PyPhotonClientType = { + PyObject_HEAD_INIT(NULL) 0, /* ob_size */ + "photon.PhotonClient", /* tp_name */ + sizeof(PyPhotonClient), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyPhotonClient_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + "PhotonClient object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + PyPhotonClient_methods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)PyPhotonClient_init, /* tp_init */ + 0, /* tp_alloc */ + PyType_GenericNew, /* tp_new */ +}; + +static PyMethodDef photon_methods[] = { + {"check_simple_value", check_simple_value, METH_VARARGS, + "Should the object be passed by value?"}, + {NULL} /* Sentinel */ +}; + +#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ +#define PyMODINIT_FUNC void +#endif + +PyMODINIT_FUNC initphoton(void) { + PyObject *m; + + if (PyType_Ready(&PyTaskType) < 0) + return; + + if (PyType_Ready(&PyObjectIDType) < 0) + return; + + if (PyType_Ready(&PyPhotonClientType) < 0) + return; + + m = Py_InitModule3("photon", photon_methods, + "A module for the local scheduler."); + + Py_INCREF(&PyTaskType); + PyModule_AddObject(m, "Task", (PyObject *)&PyTaskType); + + Py_INCREF(&PyObjectIDType); + PyModule_AddObject(m, "ObjectID", (PyObject *)&PyObjectIDType); + + Py_INCREF(&PyPhotonClientType); + PyModule_AddObject(m, "PhotonClient", (PyObject *)&PyPhotonClientType); + + char photon_error[] = "photon.error"; + PhotonError = PyErr_NewException(photon_error, NULL, NULL); + Py_INCREF(PhotonError); + PyModule_AddObject(m, "photon_error", PhotonError); +} diff --git a/lib/python/setup.py b/lib/python/setup.py new file mode 100644 index 000000000..d94fd5a83 --- /dev/null +++ b/lib/python/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup, find_packages, Extension + +photon_module = Extension("photon", + sources=["photon_extension.c", "../../common/lib/python/common_extension.c"], + include_dirs=["../../", "../../common/", + "../../common/thirdparty/", + "../../common/lib/python"], + extra_objects=["../../build/photon_client.a", "../../common/build/libcommon.a"], + extra_compile_args=["--std=c99", "-Werror"]) + +setup(name="Photon", + version="0.1", + description="Photon library for Ray", + ext_modules=[photon_module]) diff --git a/test/test.py b/test/test.py index bbd164c37..1d4cbc5af 100644 --- a/test/test.py +++ b/test/test.py @@ -45,13 +45,54 @@ class TestPhotonClient(unittest.TestCase): self.p2.kill() - def test_create(self): - l = [20 * "a", 20 * "b", 20 * "c"] - r = [20 * "e", 20 * "f"] - # Submit a task. - self.photon_client.submit(20 * "d", l, r) - # Get the task. - task = self.photon_client.get_task() + def test_submit_and_get_task(self): + # TODO(rkn): This should be a FunctionID. + function_id = photon.ObjectID(20 * "a") + object_ids = [photon.ObjectID(20 * chr(i)) for i in range(256)] + args_list = [ + [], + 1 * [1], + 10 * [1], + 100 * [1], + 1000 * [1], + 1 * ["a"], + 10 * ["a"], + 100 * ["a"], + 1000 * ["a"], + [1, 1.3, 2L, 1L << 100, "hi", u"hi", [1, 2]], + object_ids[:1], + object_ids[:2], + object_ids[:3], + object_ids[:4], + object_ids[:5], + object_ids[:10], + object_ids[:100], + object_ids[:256], + [1, object_ids[0]], + [object_ids[0], "a"], + [1, object_ids[0], "a"], + [object_ids[0], 1, object_ids[1], "a"], + object_ids[:3] + [1, "hi", 2.3] + object_ids[:5], + object_ids + 100 * ["a"] + object_ids + ] + + for args in args_list: + for num_return_vals in [0, 1, 2, 3, 5, 10, 100]: + task = photon.Task(function_id, args, num_return_vals) + # Submit a task. + self.photon_client.submit(task) + # Get the task. + new_task = self.photon_client.get_task() + self.assertEqual(task.function_id().id(), new_task.function_id().id()) + retrieved_args = new_task.arguments() + returns = new_task.returns() + self.assertEqual(len(args), len(retrieved_args)) + self.assertEqual(num_return_vals, len(returns)) + for i in range(len(retrieved_args)): + if isinstance(args[i], photon.ObjectID): + self.assertEqual(args[i].id(), retrieved_args[i].id()) + else: + self.assertEqual(args[i], retrieved_args[i]) if __name__ == "__main__": if len(sys.argv) > 1: