Repository: ray-project/ray

Issue:
Task timeline fails when arguments to remote functions are not object IDs.
The error can be reproduced as follows.

```python
import ray

ray.init()

@ray.remote
def f(x):
    pass

f.remote(3)
```

Then open the UI, run all the cells, and click "View task timeline". This produces the following error.

```
1 tasks to trace
Dumping task profiling data to /var/folders/15/54jf68993rd7753c5fms424r0000gn/T/tmpnsl3ltkw.json
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
~/Workspace/ray/python/ray/experimental/ui.py in handle_submit(sender)
    402                                              breakdowns=breakdown,
    403                                              obj_dep=obj_dep.value,
--> 404                                              task_dep=task_dep.value)
    405 
    406         print("Opening html file in browser...")

~/Workspace/ray/python/ray/experimental/state.py in dump_catapult_trace(self, path, task_info, breakdowns, task_dep, obj_dep)
    738                     if isinstance(arg, ray.local_scheduler.ObjectID):
    739                         continue
--> 740                     object_info = self._object_table(arg)
    741                     if object_info["IsPut"]:
    742                         continue

~/Workspace/ray/python/ray/experimental/state.py in _object_table(self, object_id)
    176         # Allow the argument to be either an ObjectID or a hex string.
    177         if not isinstance(object_id, ray.local_scheduler.ObjectID):
--> 178             object_id = ray.local_scheduler.ObjectID(hex_to_binary(object_id))
    179 
    180         # Return information about a single object ID.

~/Workspace/ray/python/ray/utils.py in hex_to_binary(hex_identifier)
     59 
     60 def hex_to_binary(hex_identifier):
---> 61     return binascii.unhexlify(hex_identifier)
     62 
     63 

TypeError: argument should be bytes, buffer or ASCII string, not 'int'
```

cc @alanamarzoev


Discussion:

Resolving patch:
diff --git a/python/ray/experimental/state.py b/python/ray/experimental/state.py
--- a/python/ray/experimental/state.py
+++ b/python/ray/experimental/state.py
@@ -416,7 +416,6 @@ def task_profiles(self, start=None, end=None, num_tasks=None, fwd=True):
                 list of profiling information for tasks where the events have
                 no task ID.
         """
-
         task_info = dict()
         event_log_sets = self.redis_client.keys("event_log*")
 
@@ -550,7 +549,6 @@ def dump_catapult_trace(self,
             obj_dep: Boolean indicating whether or not object dependency edges
                 should be included in the trace.
         """
-
         workers = self.workers()
         start_time = None
         for info in task_info.values():
@@ -570,10 +568,20 @@ def micros_rel(ts):
 
         full_trace = []
         for task_id, info in task_info.items():
-            # total_info is what is displayed when selecting a task in the
-            # timeline.
-            total_info = dict()
-            total_info["task_id"] = task_id
+            worker = workers[info["worker_id"]]
+            task_t_info = task_table[task_id]
+
+            # The total_info dictionary is what is displayed when selecting a
+            # task in the timeline. We copy the task spec so that we don't
+            # modify it in place since we will use the original values later.
+            total_info = copy.copy(task_table[task_id]["TaskSpec"])
+            total_info["Args"] = [
+                oid.hex() if isinstance(oid, ray.local_scheduler.ObjectID)
+                else oid for oid in task_t_info["TaskSpec"]["Args"]]
+            total_info["ReturnObjectIDs"] = [
+                oid.hex() for oid
+                in task_t_info["TaskSpec"]["ReturnObjectIDs"]]
+            total_info["LocalSchedulerID"] = task_t_info["LocalSchedulerID"]
             total_info["get_arguments"] = (info["get_arguments_end"] -
                                            info["get_arguments_start"])
             total_info["execute"] = (info["execute_end"] -
@@ -582,23 +590,12 @@ def micros_rel(ts):
                                            info["store_outputs_start"])
             total_info["function_name"] = info["function_name"]
             total_info["worker_id"] = info["worker_id"]
-            worker = workers[info["worker_id"]]
-            task_t_info = task_table[task_id]
-            task_spec = task_table[task_id]["TaskSpec"]
-            task_spec["Args"] = [oid.hex() if isinstance(oid,
-                                 ray.local_scheduler.ObjectID) else oid
-                                 for oid in task_t_info["TaskSpec"]["Args"]]
-            task_spec["ReturnObjectIDs"] = [oid.hex() for oid in
-                                            (task_t_info["TaskSpec"]
-                                             ["ReturnObjectIDs"])]
-            task_spec["LocalSchedulerID"] = task_t_info["LocalSchedulerID"]
-            total_info = copy.copy(task_spec)
 
             parent_info = task_info.get(
                             task_table[task_id]["TaskSpec"]["ParentTaskID"])
             worker = workers[info["worker_id"]]
             # The catapult trace format documentation can be found here:
-            # https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview  # NOQA 
+            # https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview  # noqa: E501
             if breakdowns:
                 if "get_arguments_end" in info:
                     get_args_trace = {
@@ -735,55 +732,60 @@ def micros_rel(ts):
             if obj_dep:
                 args = task_table[task_id]["TaskSpec"]["Args"]
                 for arg in args:
+                    # Don't visualize arguments that are not object IDs.
                     if isinstance(arg, ray.local_scheduler.ObjectID):
-                        continue
-                    object_info = self._object_table(arg)
-                    if object_info["IsPut"]:
-                        continue
-                    if arg not in seen_obj:
-                        seen_obj[arg] = 0
-                    seen_obj[arg] += 1
-                    owner_task = self._object_table(arg)["TaskID"]
-                    owner_worker = (workers[task_profiles
-                                    [owner_task]["worker_id"]])
-                    # Adding/subtracting 2 to the time associated with the
-                    # beginning/ending of the flow event is necessary to
-                    # make the flow events show up reliably. When these times
-                    # are exact, this is presumably an edge case, and catapult
-                    # doesn't recognize that there is a duration event at that
-                    # exact point in time that the flow event should be bound
-                    # to. This issue is solved by adding the 2 ms to the
-                    # start/end time of the flow event, which guarantees
-                    # overlap with the duration event that it's associated
-                    # with, and the flow event therefore always gets drawn.
-                    owner = {
-                        "cat": "obj_dependency",
-                        "pid": "Node " + owner_worker["node_ip_address"],
-                        "tid": task_profiles[owner_task]["worker_id"],
-                        "ts": micros_rel(task_profiles[owner_task]
-                                         ["store_outputs_end"]) - 2,
-                        "ph": "s",
-                        "name": "ObjectDependency",
-                        "args": {},
-                        "bp": "e",
-                        "cname": "cq_build_attempt_failed",
-                        "id": "obj" + str(arg) + str(seen_obj[arg])
-                    }
-                    full_trace.append(owner)
-
-                    dependent = {
-                        "cat": "obj_dependency",
-                        "pid":  "Node " + worker["node_ip_address"],
-                        "tid": info["worker_id"],
-                        "ts": micros_rel(info["get_arguments_start"]) + 2,
-                        "ph": "f",
-                        "name": "ObjectDependency",
-                        "args": {},
-                        "cname": "cq_build_attempt_failed",
-                        "bp": "e",
-                        "id": "obj" + str(arg) + str(seen_obj[arg])
-                    }
-                    full_trace.append(dependent)
+                        object_info = self._object_table(arg)
+                        # Don't visualize objects that were created by calls to
+                        # put.
+                        if not object_info["IsPut"]:
+                            if arg not in seen_obj:
+                                seen_obj[arg] = 0
+                            seen_obj[arg] += 1
+                            owner_task = self._object_table(arg)["TaskID"]
+                            owner_worker = (workers[
+                                task_profiles[owner_task]["worker_id"]])
+                            # Adding/subtracting 2 to the time associated with
+                            # the beginning/ending of the flow event is
+                            # necessary to make the flow events show up
+                            # reliably. When these times are exact, this is
+                            # presumably an edge case, and catapult doesn't
+                            # recognize that there is a duration event at that
+                            # exact point in time that the flow event should be
+                            # bound to. This issue is solved by adding the 2 ms
+                            # to the start/end time of the flow event, which
+                            # guarantees overlap with the duration event that
+                            # it's associated with, and the flow event
+                            # therefore always gets drawn.
+                            owner = {
+                                "cat": "obj_dependency",
+                                "pid": ("Node " +
+                                        owner_worker["node_ip_address"]),
+                                "tid": task_profiles[owner_task]["worker_id"],
+                                "ts": micros_rel(task_profiles[
+                                    owner_task]["store_outputs_end"]) - 2,
+                                "ph": "s",
+                                "name": "ObjectDependency",
+                                "args": {},
+                                "bp": "e",
+                                "cname": "cq_build_attempt_failed",
+                                "id": "obj" + str(arg) + str(seen_obj[arg])
+                            }
+                            full_trace.append(owner)
+
+                            dependent = {
+                                "cat": "obj_dependency",
+                                "pid":  "Node " + worker["node_ip_address"],
+                                "tid": info["worker_id"],
+                                "ts": micros_rel(
+                                    info["get_arguments_start"]) + 2,
+                                "ph": "f",
+                                "name": "ObjectDependency",
+                                "args": {},
+                                "cname": "cq_build_attempt_failed",
+                                "bp": "e",
+                                "id": "obj" + str(arg) + str(seen_obj[arg])
+                            }
+                            full_trace.append(dependent)
 
         print("Creating JSON {}/{}".format(len(full_trace), len(task_info)))
         with open(path, "w") as outfile:


Test patch: