From 9f1c8a3fc199ea8d8d876a4edcc754f44f472ae9 Mon Sep 17 00:00:00 2001 From: lucasvanmol Date: Fri, 17 Jan 2025 17:52:35 +0100 Subject: [PATCH 01/12] don't use key_stack for test_two_entities --- deathstar/test_demo.py | 96 +++--- src/cascade/dataflow/dataflow.py | 55 ++- src/cascade/runtime/flink_runtime.py | 35 +- tests/integration/flink-runtime/common.py | 69 ++-- .../flink-runtime/test_select_all.py | 320 +++++++++--------- .../flink-runtime/test_two_entities.py | 20 +- 6 files changed, 308 insertions(+), 287 deletions(-) diff --git a/deathstar/test_demo.py b/deathstar/test_demo.py index a7e674e..f709948 100644 --- a/deathstar/test_demo.py +++ b/deathstar/test_demo.py @@ -1,49 +1,49 @@ -from deathstar.demo import DeathstarDemo, DeathstarClient -import time -import pytest - -@pytest.mark.integration -def test_deathstar_demo(): - ds = DeathstarDemo("deathstardemo-test", "dsd-out") - ds.init_runtime() - ds.runtime.run(run_async=True) - print("Populating, press enter to go to the next step when done") - ds.populate() - - client = DeathstarClient("deathstardemo-test", "dsd-out") - input() - print("testing user login") - event = client.user_login() - client.send(event) - - input() - print("testing reserve") - event = client.reserve() - client.send(event) - - input() - print("testing search") - event = client.search_hotel() - client.send(event) - - input() - print("testing recommend (distance)") - time.sleep(0.5) - event = client.recommend(req_param="distance") - client.send(event) - - input() - print("testing recommend (price)") - time.sleep(0.5) - event = client.recommend(req_param="price") - client.send(event) - - print(client.client._futures) - input() - print("done!") - print(client.client._futures) - - -if __name__ == "__main__": - test_deathstar_demo() \ No newline at end of file +# from deathstar.demo import DeathstarDemo, DeathstarClient +# import time +# import pytest + +# @pytest.mark.integration +# def test_deathstar_demo(): +# ds = DeathstarDemo("deathstardemo-test", "dsd-out") +# ds.init_runtime() +# ds.runtime.run(run_async=True) +# print("Populating, press enter to go to the next step when done") +# ds.populate() + +# client = DeathstarClient("deathstardemo-test", "dsd-out") +# input() +# print("testing user login") +# event = client.user_login() +# client.send(event) + +# input() +# print("testing reserve") +# event = client.reserve() +# client.send(event) + +# input() +# print("testing search") +# event = client.search_hotel() +# client.send(event) + +# input() +# print("testing recommend (distance)") +# time.sleep(0.5) +# event = client.recommend(req_param="distance") +# client.send(event) + +# input() +# print("testing recommend (price)") +# time.sleep(0.5) +# event = client.recommend(req_param="price") +# client.send(event) + +# print(client.client._futures) +# input() +# print("done!") +# print(client.client._futures) + + +# if __name__ == "__main__": +# test_deathstar_demo() \ No newline at end of file diff --git a/src/cascade/dataflow/dataflow.py b/src/cascade/dataflow/dataflow.py index 2e3d890..ee18005 100644 --- a/src/cascade/dataflow/dataflow.py +++ b/src/cascade/dataflow/dataflow.py @@ -23,10 +23,10 @@ class Filter: @dataclass class Node(ABC): """Base class for Nodes.""" + id: int = field(init=False) """This node's unique id.""" - _id_counter: int = field(init=False, default=0, repr=False) outgoing_edges: list['Edge'] = field(init=False, default_factory=list, repr=False) @@ -41,8 +41,27 @@ class OpNode(Node): A `Dataflow` may reference the same `StatefulOperator` multiple times. The `StatefulOperator` that this node belongs to is referenced by `cls`.""" - operator: Operator + entity: Type method_type: Union[InitClass, InvokeMethod, Filter] + read_key_from: str + """Which variable to take as the key for this StatefulOperator""" + assign_result_to: Optional[str] = field(default=None) + """What variable to assign the result of this node to, if any.""" + is_conditional: bool = field(default=False) + """Whether or not the boolean result of this node dictates the following path.""" + collect_target: Optional['CollectTarget'] = field(default=None) + """Whether the result of this node should go to a CollectNode.""" + +@dataclass +class StatelessOpNode(Node): + """A node in a `Dataflow` corresponding to a method call of a `StatelessOperator`. + + A `Dataflow` may reference the same `StatefulOperator` multiple times. + The `StatefulOperator` that this node belongs to is referenced by `cls`.""" + dataflow: 'DataFlow' + method_type: InvokeMethod + """Which variable to take as the key for this StatefulOperator""" + assign_result_to: Optional[str] = None is_conditional: bool = False """Whether or not the boolean result of this node dictates the following path.""" @@ -176,7 +195,7 @@ class Event(): target: 'Node' """The Node that this Event wants to go to.""" - key_stack: list[str] + # key_stack: list[str] """The keys this event is concerned with. The top of the stack, i.e. `key_stack[-1]`, should always correspond to a key on the StatefulOperator of `target.cls` if `target` is an `OpNode`.""" @@ -203,7 +222,7 @@ def __post_init__(self): self._id = Event._id_counter Event._id_counter += 1 - def propogate(self, key_stack, result) -> Union['EventResult', list['Event']]: + def propogate(self, result) -> Union['EventResult', list['Event']]: """Propogate this event through the Dataflow.""" # TODO: keys should be structs containing Key and Opnode (as we need to know the entity (cls) and method to invoke for that particular key) @@ -216,23 +235,23 @@ def propogate(self, key_stack, result) -> Union['EventResult', list['Event']]: if len(targets) == 0: return EventResult(self._id, result) else: - keys = key_stack.pop() - if not isinstance(keys, list): - keys = [keys] + # keys = key_stack.pop() + # if not isinstance(keys, list): + # keys = [keys] collect_targets: list[Optional[CollectTarget]] # Events with SelectAllNodes need to be assigned a CollectTarget if isinstance(self.target, SelectAllNode): collect_targets = [ - CollectTarget(self.target.collect_target, len(keys), i) - for i in range(len(keys)) + CollectTarget(self.target.collect_target, len(targets), i) + for i in range(len(targets)) ] elif isinstance(self.target, OpNode) and self.target.collect_target is not None: collect_targets = [ - self.target.collect_target for i in range(len(keys)) + self.target.collect_target for i in range(len(targets)) ] else: - collect_targets = [self.collect_target for i in range(len(keys))] + collect_targets = [self.collect_target for i in range(len(targets))] if isinstance(self.target, OpNode) and self.target.is_conditional: # In this case there will be two targets depending on the condition @@ -249,13 +268,13 @@ def propogate(self, key_stack, result) -> Union['EventResult', list['Event']]: return [Event( target_true if result else target_false, - key_stack + [key], + # key_stack + [key], self.variable_map, self.dataflow, _id=self._id, collect_target=ct) - for key, ct in zip(keys, collect_targets)] + for ct in collect_targets] elif len(targets) == 1: # We assume that all keys need to go to the same target @@ -263,26 +282,26 @@ def propogate(self, key_stack, result) -> Union['EventResult', list['Event']]: return [Event( targets[0], - key_stack + [key], + # key_stack + [key], self.variable_map, self.dataflow, _id=self._id, collect_target=ct) - for key, ct in zip(keys, collect_targets)] + for ct in collect_targets] else: # An event with multiple targets should have the same number of # keys in a list on top of its key stack - assert len(targets) == len(keys) + # assert len(targets) == len(keys) return [Event( target, - key_stack + [key], + # key_stack + [key], self.variable_map, self.dataflow, _id=self._id, collect_target=ct) - for target, key, ct in zip(targets, keys, collect_targets)] + for target, ct in zip(targets, collect_targets)] @dataclass class EventResult(): diff --git a/src/cascade/runtime/flink_runtime.py b/src/cascade/runtime/flink_runtime.py index 763582e..15e5a8f 100644 --- a/src/cascade/runtime/flink_runtime.py +++ b/src/cascade/runtime/flink_runtime.py @@ -12,7 +12,7 @@ from pyflink.datastream.connectors.kafka import KafkaOffsetsInitializer, KafkaRecordSerializationSchema, KafkaSource, KafkaSink from pyflink.datastream import ProcessFunction, StreamExecutionEnvironment import pickle -from cascade.dataflow.dataflow import Arrived, CollectNode, CollectTarget, Event, EventResult, Filter, InitClass, InvokeMethod, MergeNode, Node, NotArrived, OpNode, Operator, Result, SelectAllNode +from cascade.dataflow.dataflow import Arrived, CollectNode, CollectTarget, Event, EventResult, Filter, InitClass, InvokeMethod, MergeNode, Node, NotArrived, OpNode, Operator, Result, SelectAllNode, StatelessOpNode from cascade.dataflow.operator import StatefulOperator, StatelessOperator from confluent_kafka import Producer, Consumer import logging @@ -49,12 +49,13 @@ def open(self, runtime_context: RuntimeContext): self.state: ValueState = runtime_context.get_state(descriptor) def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): - key_stack = event.key_stack + # key_stack = event.key_stack # should be handled by filters on this FlinkOperator assert(isinstance(event.target, OpNode)) - assert(isinstance(event.target.operator, StatefulOperator)) - assert(event.target.operator.entity == self.operator.entity) + assert(event.target.entity == self.operator.entity) + key = ctx.get_current_key() + assert(key is not None) logger.debug(f"FlinkOperator {self.operator.entity.__name__}[{ctx.get_current_key()}]: Processing: {event.target.method_type}") if isinstance(event.target.method_type, InitClass): @@ -64,8 +65,8 @@ def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): # Register the created key in FlinkSelectAllOperator register_key_event = Event( - FlinkRegisterKeyNode(key_stack[-1], self.operator.entity), - [], + FlinkRegisterKeyNode(key, self.operator.entity), + # [], {}, None, _id = event._id @@ -74,11 +75,11 @@ def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): yield register_key_event # Pop this key from the key stack so that we exit - key_stack.pop() + # key_stack.pop() self.state.update(pickle.dumps(result)) elif isinstance(event.target.method_type, InvokeMethod): state = pickle.loads(self.state.value()) - result = self.operator.handle_invoke_method(event.target.method_type, variable_map=event.variable_map, state=state, key_stack=key_stack) + result = self.operator.handle_invoke_method(event.target.method_type, variable_map=event.variable_map, state=state, key_stack=[]) # TODO: check if state actually needs to be updated if state is not None: @@ -93,7 +94,7 @@ def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): if event.target.assign_result_to is not None: event.variable_map[event.target.assign_result_to] = result - new_events = event.propogate(key_stack, result) + new_events = event.propogate(result) if isinstance(new_events, EventResult): logger.debug(f"FlinkOperator {self.operator.entity.__name__}[{ctx.get_current_key()}]: Returned {new_events}") yield new_events @@ -113,8 +114,7 @@ def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): key_stack = event.key_stack # should be handled by filters on this FlinkOperator - assert(isinstance(event.target, OpNode)) - assert(isinstance(event.target.operator, StatelessOperator)) + assert(isinstance(event.target, StatelessOpNode)) logger.debug(f"FlinkStatelessOperator {self.operator.dataflow.name}[{event._id}]: Processing: {event.target.method_type}") if isinstance(event.target.method_type, InvokeMethod): @@ -422,18 +422,17 @@ def init(self, kafka_broker="localhost:9092", bundle_time=1, bundle_size=5, para not (isinstance(e.target, SelectAllNode) or isinstance(e.target, FlinkRegisterKeyNode))) ) - event_stream_2 = select_all_stream.union(not_select_all_stream) + operator_stream = select_all_stream.union(not_select_all_stream) - operator_stream = event_stream_2.filter(lambda e: isinstance(e.target, OpNode)).name("OPERATOR STREAM") self.stateful_op_stream = ( operator_stream - .filter(lambda e: isinstance(e.target.operator, StatefulOperator)) + .filter(lambda e: isinstance(e.target, OpNode)) ) self.stateless_op_stream = ( operator_stream - .filter(lambda e: isinstance(e.target.operator, StatelessOperator)) + .filter(lambda e: isinstance(e.target, StatelessOpNode)) ) self.merge_op_stream = ( @@ -455,8 +454,8 @@ def add_operator(self, flink_op: FlinkOperator): """Add a `FlinkOperator` to the Flink datastream.""" op_stream = ( - self.stateful_op_stream.filter(lambda e: e.target.operator.entity == flink_op.operator.entity) - .key_by(lambda e: e.key_stack[-1]) + self.stateful_op_stream.filter(lambda e: e.target.entity == flink_op.operator.entity) + .key_by(lambda e: e.variable_map[e.target.read_key_from]) .process(flink_op) .name("STATEFUL OP: " + flink_op.operator.entity.__name__) ) @@ -467,7 +466,7 @@ def add_stateless_operator(self, flink_op: FlinkStatelessOperator): op_stream = ( self.stateless_op_stream - .filter(lambda e: e.target.operator.dataflow.name == flink_op.operator.dataflow.name) + .filter(lambda e: e.target.dataflow.name == flink_op.operator.dataflow.name) .process(flink_op) .name("STATELESS DATAFLOW: " + flink_op.operator.dataflow.name) ) diff --git a/tests/integration/flink-runtime/common.py b/tests/integration/flink-runtime/common.py index 105fdbd..5a63bdb 100644 --- a/tests/integration/flink-runtime/common.py +++ b/tests/integration/flink-runtime/common.py @@ -40,25 +40,25 @@ def __repr__(self): return f"Item(key='{self.key}', price={self.price})" def update_balance_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() # final function + # key_stack.pop() # final function state.balance += variable_map["amount"] return state.balance >= 0 def get_balance_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() # final function + # key_stack.pop() # final function return state.balance def get_price_compiled(variable_map: dict[str, Any], state: Item, key_stack: list[str]) -> Any: - key_stack.pop() # final function + # key_stack.pop() # final function return state.price # Items (or other operators) are passed by key always def buy_item_0_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append(variable_map["item_key"]) + # key_stack.append(variable_map["item_key"]) return None def buy_item_1_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() + # key_stack.pop() state.balance = state.balance - variable_map["item_price"] return state.balance >= 0 @@ -94,40 +94,43 @@ def buy_2_items_1_compiled(variable_map: dict[str, Any], state: User, key_stack: def user_buy_item_df(): df = DataFlow("user.buy_item") - n0 = OpNode(user_op, InvokeMethod("buy_item_0")) - n1 = OpNode(item_op, InvokeMethod("get_price"), assign_result_to="item_price") - n2 = OpNode(user_op, InvokeMethod("buy_item_1")) + n0 = OpNode(User, InvokeMethod("buy_item_0"), read_key_from="user_key") + n1 = OpNode(Item, + InvokeMethod("get_price"), + assign_result_to="item_price", + read_key_from="item_key") + n2 = OpNode(User, InvokeMethod("buy_item_1"), read_key_from="user_key") df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n1, n2)) df.entry = n0 return df -def user_buy_2_items_df(): - df = DataFlow("user.buy_2_items") - n0 = OpNode(user_op, InvokeMethod("buy_2_items_0")) - n3 = CollectNode(assign_result_to="item_prices", read_results_from="item_price") - n1 = OpNode( - item_op, - InvokeMethod("get_price"), - assign_result_to="item_price", - collect_target=CollectTarget(n3, 2, 0) - ) - n2 = OpNode( - item_op, - InvokeMethod("get_price"), - assign_result_to="item_price", - collect_target=CollectTarget(n3, 2, 1) - ) - n4 = OpNode(user_op, InvokeMethod("buy_2_items_1")) - df.add_edge(Edge(n0, n1)) - df.add_edge(Edge(n0, n2)) - df.add_edge(Edge(n1, n3)) - df.add_edge(Edge(n2, n3)) - df.add_edge(Edge(n3, n4)) - df.entry = n0 - return df +# def user_buy_2_items_df(): +# df = DataFlow("user.buy_2_items") +# n0 = OpNode(user_op, InvokeMethod("buy_2_items_0")) +# n3 = CollectNode(assign_result_to="item_prices", read_results_from="item_price") +# n1 = OpNode( +# item_op, +# InvokeMethod("get_price"), +# assign_result_to="item_price", +# collect_target=CollectTarget(n3, 2, 0) +# ) +# n2 = OpNode( +# item_op, +# InvokeMethod("get_price"), +# assign_result_to="item_price", +# collect_target=CollectTarget(n3, 2, 1) +# ) +# n4 = OpNode(user_op, InvokeMethod("buy_2_items_1")) +# df.add_edge(Edge(n0, n1)) +# df.add_edge(Edge(n0, n2)) +# df.add_edge(Edge(n1, n3)) +# df.add_edge(Edge(n2, n3)) +# df.add_edge(Edge(n3, n4)) +# df.entry = n0 +# return df user_op.dataflows = { - "buy_2_items": user_buy_2_items_df(), + # "buy_2_items": user_buy_2_items_df(), "buy_item": user_buy_item_df() } \ No newline at end of file diff --git a/tests/integration/flink-runtime/test_select_all.py b/tests/integration/flink-runtime/test_select_all.py index 62c371e..2b4de65 100644 --- a/tests/integration/flink-runtime/test_select_all.py +++ b/tests/integration/flink-runtime/test_select_all.py @@ -1,164 +1,164 @@ -""" -Basically we need a way to search through all state. -""" -import math -import random -from dataclasses import dataclass -from typing import Any - -from pyflink.datastream.data_stream import CloseableIterator - -from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, Event, EventResult, Filter, InitClass, InvokeMethod, MergeNode, OpNode, SelectAllNode -from cascade.dataflow.operator import StatefulOperator, StatelessOperator -from cascade.runtime.flink_runtime import FlinkOperator, FlinkRuntime, FlinkStatelessOperator -from confluent_kafka import Producer -import time -import pytest - -@dataclass -class Geo: - x: int - y: int - -class Hotel: - def __init__(self, name: str, loc: Geo): - self.name = name - self.loc = loc - - def get_name(self) -> str: - return self.name +# """ +# Basically we need a way to search through all state. +# """ +# import math +# import random +# from dataclasses import dataclass +# from typing import Any + +# from pyflink.datastream.data_stream import CloseableIterator + +# from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, Event, EventResult, Filter, InitClass, InvokeMethod, MergeNode, OpNode, SelectAllNode +# from cascade.dataflow.operator import StatefulOperator, StatelessOperator +# from cascade.runtime.flink_runtime import FlinkOperator, FlinkRuntime, FlinkStatelessOperator +# from confluent_kafka import Producer +# import time +# import pytest + +# @dataclass +# class Geo: +# x: int +# y: int + +# class Hotel: +# def __init__(self, name: str, loc: Geo): +# self.name = name +# self.loc = loc + +# def get_name(self) -> str: +# return self.name - def distance(self, loc: Geo) -> float: - return math.sqrt((self.loc.x - loc.x) ** 2 + (self.loc.y - loc.y) ** 2) +# def distance(self, loc: Geo) -> float: +# return math.sqrt((self.loc.x - loc.x) ** 2 + (self.loc.y - loc.y) ** 2) - def __repr__(self) -> str: - return f"Hotel({self.name}, {self.loc})" - - -def distance_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: - key_stack.pop() - loc = variable_map["loc"] - return math.sqrt((state.loc.x - loc.x) ** 2 + (state.loc.y - loc.y) ** 2) - -def get_name_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: - key_stack.pop() - return state.name - -hotel_op = StatefulOperator(Hotel, - {"distance": distance_compiled, - "get_name": get_name_compiled}, {}) - - - -def get_nearby(hotels: list[Hotel], loc: Geo, dist: float): - return [hotel.get_name() for hotel in hotels if hotel.distance(loc) < dist] - - -# We compile just the predicate, the select is implemented using a selectall node -def get_nearby_predicate_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): - # the top of the key_stack is already the right key, so in this case we don't need to do anything - # loc = variable_map["loc"] - # we need the hotel_key for later. (body_compiled_0) - variable_map["hotel_key"] = key_stack[-1] - pass - -def get_nearby_predicate_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) -> bool: - loc = variable_map["loc"] - dist = variable_map["dist"] - hotel_dist = variable_map["hotel_distance"] - # key_stack.pop() # shouldn't pop because this function is stateless - return hotel_dist < dist - -def get_nearby_body_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): - key_stack.append(variable_map["hotel_key"]) - -def get_nearby_body_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) -> str: - return variable_map["hotel_name"] - -get_nearby_op = StatelessOperator({ - "get_nearby_predicate_compiled_0": get_nearby_predicate_compiled_0, - "get_nearby_predicate_compiled_1": get_nearby_predicate_compiled_1, - "get_nearby_body_compiled_0": get_nearby_body_compiled_0, - "get_nearby_body_compiled_1": get_nearby_body_compiled_1, -}, None) - -# dataflow for getting all hotels within region -df = DataFlow("get_nearby") -n7 = CollectNode("get_nearby_result", "get_nearby_body") -n0 = SelectAllNode(Hotel, n7) -n1 = OpNode(get_nearby_op, InvokeMethod("get_nearby_predicate_compiled_0")) -n2 = OpNode(hotel_op, InvokeMethod("distance"), assign_result_to="hotel_distance") -n3 = OpNode(get_nearby_op, InvokeMethod("get_nearby_predicate_compiled_1"), is_conditional=True) -n4 = OpNode(get_nearby_op, InvokeMethod("get_nearby_body_compiled_0")) -n5 = OpNode(hotel_op, InvokeMethod("get_name"), assign_result_to="hotel_name") -n6 = OpNode(get_nearby_op, InvokeMethod("get_nearby_body_compiled_1"), assign_result_to="get_nearby_body") - -df.add_edge(Edge(n0, n1)) -df.add_edge(Edge(n1, n2)) -df.add_edge(Edge(n2, n3)) -df.add_edge(Edge(n3, n4, if_conditional=True)) -df.add_edge(Edge(n3, n7, if_conditional=False)) -df.add_edge(Edge(n4, n5)) -df.add_edge(Edge(n5, n6)) -df.add_edge(Edge(n6, n7)) -get_nearby_op.dataflow = df - -@pytest.mark.integration -def test_nearby_hotels(): - runtime = FlinkRuntime("test_nearby_hotels") - runtime.init() - runtime.add_operator(FlinkOperator(hotel_op)) - runtime.add_stateless_operator(FlinkStatelessOperator(get_nearby_op)) - - # Create Hotels - hotels = [] - init_hotel = OpNode(hotel_op, InitClass()) - random.seed(42) - for i in range(20): - coord_x = random.randint(-10, 10) - coord_y = random.randint(-10, 10) - hotel = Hotel(f"h_{i}", Geo(coord_x, coord_y)) - event = Event(init_hotel, [hotel.name], {"name": hotel.name, "loc": hotel.loc}, None) - runtime.send(event) - hotels.append(hotel) - - collected_iterator: CloseableIterator = runtime.run(run_async=True, collect=True) - records = [] - def wait_for_event_id(id: int) -> EventResult: - for record in collected_iterator: - records.append(record) - print(f"Collected record: {record}") - if record.event_id == id: - return record +# def __repr__(self) -> str: +# return f"Hotel({self.name}, {self.loc})" + + +# def distance_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: +# key_stack.pop() +# loc = variable_map["loc"] +# return math.sqrt((state.loc.x - loc.x) ** 2 + (state.loc.y - loc.y) ** 2) + +# def get_name_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: +# key_stack.pop() +# return state.name + +# hotel_op = StatefulOperator(Hotel, +# {"distance": distance_compiled, +# "get_name": get_name_compiled}, {}) + + + +# def get_nearby(hotels: list[Hotel], loc: Geo, dist: float): +# return [hotel.get_name() for hotel in hotels if hotel.distance(loc) < dist] + + +# # We compile just the predicate, the select is implemented using a selectall node +# def get_nearby_predicate_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): +# # the top of the key_stack is already the right key, so in this case we don't need to do anything +# # loc = variable_map["loc"] +# # we need the hotel_key for later. (body_compiled_0) +# variable_map["hotel_key"] = key_stack[-1] +# pass + +# def get_nearby_predicate_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) -> bool: +# loc = variable_map["loc"] +# dist = variable_map["dist"] +# hotel_dist = variable_map["hotel_distance"] +# # key_stack.pop() # shouldn't pop because this function is stateless +# return hotel_dist < dist + +# def get_nearby_body_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): +# key_stack.append(variable_map["hotel_key"]) + +# def get_nearby_body_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) -> str: +# return variable_map["hotel_name"] + +# get_nearby_op = StatelessOperator({ +# "get_nearby_predicate_compiled_0": get_nearby_predicate_compiled_0, +# "get_nearby_predicate_compiled_1": get_nearby_predicate_compiled_1, +# "get_nearby_body_compiled_0": get_nearby_body_compiled_0, +# "get_nearby_body_compiled_1": get_nearby_body_compiled_1, +# }, None) + +# # dataflow for getting all hotels within region +# df = DataFlow("get_nearby") +# n7 = CollectNode("get_nearby_result", "get_nearby_body") +# n0 = SelectAllNode(Hotel, n7) +# n1 = OpNode(get_nearby_op, InvokeMethod("get_nearby_predicate_compiled_0")) +# n2 = OpNode(hotel_op, InvokeMethod("distance"), assign_result_to="hotel_distance") +# n3 = OpNode(get_nearby_op, InvokeMethod("get_nearby_predicate_compiled_1"), is_conditional=True) +# n4 = OpNode(get_nearby_op, InvokeMethod("get_nearby_body_compiled_0")) +# n5 = OpNode(hotel_op, InvokeMethod("get_name"), assign_result_to="hotel_name") +# n6 = OpNode(get_nearby_op, InvokeMethod("get_nearby_body_compiled_1"), assign_result_to="get_nearby_body") + +# df.add_edge(Edge(n0, n1)) +# df.add_edge(Edge(n1, n2)) +# df.add_edge(Edge(n2, n3)) +# df.add_edge(Edge(n3, n4, if_conditional=True)) +# df.add_edge(Edge(n3, n7, if_conditional=False)) +# df.add_edge(Edge(n4, n5)) +# df.add_edge(Edge(n5, n6)) +# df.add_edge(Edge(n6, n7)) +# get_nearby_op.dataflow = df + +# @pytest.mark.integration +# def test_nearby_hotels(): +# runtime = FlinkRuntime("test_nearby_hotels") +# runtime.init() +# runtime.add_operator(FlinkOperator(hotel_op)) +# runtime.add_stateless_operator(FlinkStatelessOperator(get_nearby_op)) + +# # Create Hotels +# hotels = [] +# init_hotel = OpNode(hotel_op, InitClass()) +# random.seed(42) +# for i in range(20): +# coord_x = random.randint(-10, 10) +# coord_y = random.randint(-10, 10) +# hotel = Hotel(f"h_{i}", Geo(coord_x, coord_y)) +# event = Event(init_hotel, [hotel.name], {"name": hotel.name, "loc": hotel.loc}, None) +# runtime.send(event) +# hotels.append(hotel) + +# collected_iterator: CloseableIterator = runtime.run(run_async=True, collect=True) +# records = [] +# def wait_for_event_id(id: int) -> EventResult: +# for record in collected_iterator: +# records.append(record) +# print(f"Collected record: {record}") +# if record.event_id == id: +# return record - def wait_for_n_records(num: int) -> list[EventResult]: - i = 0 - n_records = [] - for record in collected_iterator: - i += 1 - records.append(record) - n_records.append(record) - print(f"Collected record: {record}") - if i == num: - return n_records - - print("creating hotels") - # Wait for hotels to be created - wait_for_n_records(20) - time.sleep(3) # wait for all hotels to be registered - - dist = 5 - loc = Geo(0, 0) - # because of how the key stack works, we need to supply a key here - event = Event(n0, ["workaround_key"], {"loc": loc, "dist": dist}, df) - runtime.send(event, flush=True) +# def wait_for_n_records(num: int) -> list[EventResult]: +# i = 0 +# n_records = [] +# for record in collected_iterator: +# i += 1 +# records.append(record) +# n_records.append(record) +# print(f"Collected record: {record}") +# if i == num: +# return n_records + +# print("creating hotels") +# # Wait for hotels to be created +# wait_for_n_records(20) +# time.sleep(3) # wait for all hotels to be registered + +# dist = 5 +# loc = Geo(0, 0) +# # because of how the key stack works, we need to supply a key here +# event = Event(n0, ["workaround_key"], {"loc": loc, "dist": dist}, df) +# runtime.send(event, flush=True) - nearby = [] - for hotel in hotels: - if hotel.distance(loc) < dist: - nearby.append(hotel.name) - - event_result = wait_for_event_id(event._id) - results = [r for r in event_result.result if r != None] - print(nearby) - assert set(results) == set(nearby) \ No newline at end of file +# nearby = [] +# for hotel in hotels: +# if hotel.distance(loc) < dist: +# nearby.append(hotel.name) + +# event_result = wait_for_event_id(event._id) +# results = [r for r in event_result.result if r != None] +# print(nearby) +# assert set(results) == set(nearby) \ No newline at end of file diff --git a/tests/integration/flink-runtime/test_two_entities.py b/tests/integration/flink-runtime/test_two_entities.py index 9d2e0cf..54309fa 100644 --- a/tests/integration/flink-runtime/test_two_entities.py +++ b/tests/integration/flink-runtime/test_two_entities.py @@ -15,19 +15,19 @@ def test_two_entities(): # Create a User object foo_user = User("foo", 100) - init_user_node = OpNode(user_op, InitClass()) - event = Event(init_user_node, ["foo"], {"key": "foo", "balance": 100}, None) + init_user_node = OpNode(User, InitClass(), read_key_from="key") + event = Event(init_user_node, {"key": "foo", "balance": 100}, None) runtime.send(event) # Create an Item object fork_item = Item("fork", 5) - init_item_node = OpNode(item_op, InitClass()) - event = Event(init_item_node, ["fork"], {"key": "fork", "price": 5}, None) + init_item_node = OpNode(Item, InitClass(), read_key_from="key") + event = Event(init_item_node, {"key": "fork", "price": 5}, None) runtime.send(event) # Create an expensive Item house_item = Item("house", 1000) - event = Event(init_item_node, ["house"], {"key": "house", "price": 1000}, None) + event = Event(init_item_node, {"key": "house", "price": 1000}, None) runtime.send(event) # Have the User object buy the item @@ -35,10 +35,10 @@ def test_two_entities(): df = user_op.dataflows["buy_item"] # User with key "foo" buys item with key "fork" - user_buys_fork = Event(df.entry, ["foo"], {"item_key": "fork"}, df) + user_buys_fork = Event(df.entry, {"user_key": "foo", "item_key": "fork"}, df) runtime.send(user_buys_fork, flush=True) - collected_iterator: CloseableIterator = runtime.run(run_async=True, collect=True) + collected_iterator: CloseableIterator = runtime.run(run_async=True, output="collect") records = [] def wait_for_event_id(id: int) -> EventResult: @@ -53,8 +53,8 @@ def wait_for_event_id(id: int) -> EventResult: assert buy_fork_result.result == True # Send an event to check if the balance was updated - user_get_balance_node = OpNode(user_op, InvokeMethod("get_balance")) - user_get_balance = Event(user_get_balance_node, ["foo"], {}, None) + user_get_balance_node = OpNode(User, InvokeMethod("get_balance"), read_key_from="key") + user_get_balance = Event(user_get_balance_node, {"key": "foo"}, None) runtime.send(user_get_balance, flush=True) # See that the user's balance has gone down @@ -63,7 +63,7 @@ def wait_for_event_id(id: int) -> EventResult: # User with key "foo" buys item with key "house" foo_user.buy_item(house_item) - user_buys_house = Event(df.entry, ["foo"], {"item_key": "house"}, df) + user_buys_house = Event(df.entry, {"user_key": "foo", "item_key": "house"}, df) runtime.send(user_buys_house, flush=True) # Balance becomes negative when house is bought From b2d33293ff04c443aacf3ebffe29c060efdc2a21 Mon Sep 17 00:00:00 2001 From: lucasvanmol Date: Mon, 20 Jan 2025 10:49:34 +0100 Subject: [PATCH 02/12] remove key stack from collect operator --- src/cascade/runtime/flink_runtime.py | 2 +- tests/integration/flink-runtime/common.py | 56 ++++--- ...e_operator.py => test_collect_operator.py} | 140 +++++++++--------- 3 files changed, 98 insertions(+), 100 deletions(-) rename tests/integration/flink-runtime/{test_merge_operator.py => test_collect_operator.py} (71%) diff --git a/src/cascade/runtime/flink_runtime.py b/src/cascade/runtime/flink_runtime.py index 15e5a8f..2a04b34 100644 --- a/src/cascade/runtime/flink_runtime.py +++ b/src/cascade/runtime/flink_runtime.py @@ -209,7 +209,7 @@ def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): collection = [r.val for r in collection if r.val is not None] # type: ignore (r is of type Arrived) event.variable_map[target_node.assign_result_to] = collection - new_events = event.propogate(event.key_stack, collection) + new_events = event.propogate(collection) self.collection.clear() if isinstance(new_events, EventResult): diff --git a/tests/integration/flink-runtime/common.py b/tests/integration/flink-runtime/common.py index 5a63bdb..7a676e2 100644 --- a/tests/integration/flink-runtime/common.py +++ b/tests/integration/flink-runtime/common.py @@ -64,13 +64,9 @@ def buy_item_1_compiled(variable_map: dict[str, Any], state: User, key_stack: li def buy_2_items_0_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append( - [variable_map["item1_key"], variable_map["item2_key"]] - ) return None def buy_2_items_1_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() state.balance -= variable_map["item_prices"][0] + variable_map["item_prices"][1] return state.balance >= 0 @@ -105,32 +101,34 @@ def user_buy_item_df(): df.entry = n0 return df -# def user_buy_2_items_df(): -# df = DataFlow("user.buy_2_items") -# n0 = OpNode(user_op, InvokeMethod("buy_2_items_0")) -# n3 = CollectNode(assign_result_to="item_prices", read_results_from="item_price") -# n1 = OpNode( -# item_op, -# InvokeMethod("get_price"), -# assign_result_to="item_price", -# collect_target=CollectTarget(n3, 2, 0) -# ) -# n2 = OpNode( -# item_op, -# InvokeMethod("get_price"), -# assign_result_to="item_price", -# collect_target=CollectTarget(n3, 2, 1) -# ) -# n4 = OpNode(user_op, InvokeMethod("buy_2_items_1")) -# df.add_edge(Edge(n0, n1)) -# df.add_edge(Edge(n0, n2)) -# df.add_edge(Edge(n1, n3)) -# df.add_edge(Edge(n2, n3)) -# df.add_edge(Edge(n3, n4)) -# df.entry = n0 -# return df +def user_buy_2_items_df(): + df = DataFlow("user.buy_2_items") + n0 = OpNode(User, InvokeMethod("buy_2_items_0"), read_key_from="user_key") + n3 = CollectNode(assign_result_to="item_prices", read_results_from="item_price") + n1 = OpNode( + Item, + InvokeMethod("get_price"), + assign_result_to="item_price", + collect_target=CollectTarget(n3, 2, 0), + read_key_from="item1_key" + ) + n2 = OpNode( + Item, + InvokeMethod("get_price"), + assign_result_to="item_price", + collect_target=CollectTarget(n3, 2, 1), + read_key_from="item2_key" + ) + n4 = OpNode(User, InvokeMethod("buy_2_items_1"), read_key_from="user_key") + df.add_edge(Edge(n0, n1)) + df.add_edge(Edge(n0, n2)) + df.add_edge(Edge(n1, n3)) + df.add_edge(Edge(n2, n3)) + df.add_edge(Edge(n3, n4)) + df.entry = n0 + return df user_op.dataflows = { - # "buy_2_items": user_buy_2_items_df(), + "buy_2_items": user_buy_2_items_df(), "buy_item": user_buy_item_df() } \ No newline at end of file diff --git a/tests/integration/flink-runtime/test_merge_operator.py b/tests/integration/flink-runtime/test_collect_operator.py similarity index 71% rename from tests/integration/flink-runtime/test_merge_operator.py rename to tests/integration/flink-runtime/test_collect_operator.py index d136d99..574c739 100644 --- a/tests/integration/flink-runtime/test_merge_operator.py +++ b/tests/integration/flink-runtime/test_collect_operator.py @@ -1,71 +1,71 @@ -"""A test script for dataflows with merge operators""" - -from pyflink.datastream.data_stream import CloseableIterator -from common import Item, User, item_op, user_op -from cascade.dataflow.dataflow import Event, EventResult, InitClass, InvokeMethod, OpNode -from cascade.runtime.flink_runtime import FlinkOperator, FlinkRuntime -import pytest - -@pytest.mark.integration -def test_merge_operator(): - runtime = FlinkRuntime("test_merge_operator") - runtime.init() - runtime.add_operator(FlinkOperator(item_op)) - runtime.add_operator(FlinkOperator(user_op)) - - - # Create a User object - foo_user = User("foo", 100) - init_user_node = OpNode(user_op, InitClass()) - event = Event(init_user_node, ["foo"], {"key": "foo", "balance": 100}, None) - runtime.send(event) - - # Create an Item object - fork_item = Item("fork", 5) - init_item_node = OpNode(item_op, InitClass()) - event = Event(init_item_node, ["fork"], {"key": "fork", "price": 5}, None) - runtime.send(event) - - # Create another Item - spoon_item = Item("spoon", 3) - event = Event(init_item_node, ["spoon"], {"key": "spoon", "price": 3}, None) - runtime.send(event, flush=True) - - collected_iterator: CloseableIterator = runtime.run(run_async=True, collect=True) - records = [] - - def wait_for_event_id(id: int) -> EventResult: - for record in collected_iterator: - records.append(record) - print(f"Collected record: {record}") - if record.event_id == id: - return record - - # Make sure the user & items are initialised - wait_for_event_id(event._id) - - # Have the User object buy the item - foo_user.buy_2_items(fork_item, spoon_item) - df = user_op.dataflows["buy_2_items"] - - # User with key "foo" buys item with key "fork" - user_buys_cutlery = Event(df.entry, ["foo"], {"item1_key": "fork", "item2_key": "spoon"}, df) - runtime.send(user_buys_cutlery, flush=True) - - - # Check that we were able to buy the fork - buy_fork_result = wait_for_event_id(user_buys_cutlery._id) - assert buy_fork_result.result == True - - # Send an event to check if the balance was updated - user_get_balance_node = OpNode(user_op, InvokeMethod("get_balance")) - user_get_balance = Event(user_get_balance_node, ["foo"], {}, None) - runtime.send(user_get_balance, flush=True) - - # See that the user's balance has gone down - get_balance = wait_for_event_id(user_get_balance._id) - assert get_balance.result == 92 - - collected_iterator.close() - +"""A test script for dataflows with merge operators""" + +from pyflink.datastream.data_stream import CloseableIterator +from common import Item, User, item_op, user_op +from cascade.dataflow.dataflow import Event, EventResult, InitClass, InvokeMethod, OpNode +from cascade.runtime.flink_runtime import FlinkOperator, FlinkRuntime +import pytest + +@pytest.mark.integration +def test_merge_operator(): + runtime = FlinkRuntime("test_collect_operator") + runtime.init() + runtime.add_operator(FlinkOperator(item_op)) + runtime.add_operator(FlinkOperator(user_op)) + + + # Create a User object + foo_user = User("foo", 100) + init_user_node = OpNode(User, InitClass(), read_key_from="key") + event = Event(init_user_node, {"key": "foo", "balance": 100}, None) + runtime.send(event) + + # Create an Item object + fork_item = Item("fork", 5) + init_item_node = OpNode(Item, InitClass(), read_key_from="key") + event = Event(init_item_node, {"key": "fork", "price": 5}, None) + runtime.send(event) + + # Create another Item + spoon_item = Item("spoon", 3) + event = Event(init_item_node, {"key": "spoon", "price": 3}, None) + runtime.send(event, flush=True) + + collected_iterator: CloseableIterator = runtime.run(run_async=True, output="collect") + records = [] + + def wait_for_event_id(id: int) -> EventResult: + for record in collected_iterator: + records.append(record) + print(f"Collected record: {record}") + if record.event_id == id: + return record + + # Make sure the user & items are initialised + wait_for_event_id(event._id) + + # Have the User object buy the item + foo_user.buy_2_items(fork_item, spoon_item) + df = user_op.dataflows["buy_2_items"] + + # User with key "foo" buys item with key "fork" + user_buys_cutlery = Event(df.entry, {"user_key": "foo", "item1_key": "fork", "item2_key": "spoon"}, df) + runtime.send(user_buys_cutlery, flush=True) + + + # Check that we were able to buy the fork + buy_fork_result = wait_for_event_id(user_buys_cutlery._id) + assert buy_fork_result.result == True + + # Send an event to check if the balance was updated + user_get_balance_node = OpNode(User, InvokeMethod("get_balance"), read_key_from="key") + user_get_balance = Event(user_get_balance_node, {"key": "foo"}, None) + runtime.send(user_get_balance, flush=True) + + # See that the user's balance has gone down + get_balance = wait_for_event_id(user_get_balance._id) + assert get_balance.result == 92 + + collected_iterator.close() + print(records) \ No newline at end of file From 7d3677007c458da6ae4b91bbd4560bd533f2d38f Mon Sep 17 00:00:00 2001 From: lucasvanmol Date: Mon, 20 Jan 2025 12:05:54 +0100 Subject: [PATCH 03/12] remove key stack from select all operator --- src/cascade/dataflow/dataflow.py | 34 +- src/cascade/runtime/flink_runtime.py | 14 +- .../flink-runtime/test_select_all.py | 316 +++++++++--------- 3 files changed, 184 insertions(+), 180 deletions(-) diff --git a/src/cascade/dataflow/dataflow.py b/src/cascade/dataflow/dataflow.py index ee18005..2e12559 100644 --- a/src/cascade/dataflow/dataflow.py +++ b/src/cascade/dataflow/dataflow.py @@ -39,8 +39,8 @@ def __post_init__(self): class OpNode(Node): """A node in a `Dataflow` corresponding to a method call of a `StatefulOperator`. - A `Dataflow` may reference the same `StatefulOperator` multiple times. - The `StatefulOperator` that this node belongs to is referenced by `cls`.""" + A `Dataflow` may reference the same entity multiple times. + The `StatefulOperator` that this node belongs to is referenced by `entity`.""" entity: Type method_type: Union[InitClass, InvokeMethod, Filter] read_key_from: str @@ -58,7 +58,7 @@ class StatelessOpNode(Node): A `Dataflow` may reference the same `StatefulOperator` multiple times. The `StatefulOperator` that this node belongs to is referenced by `cls`.""" - dataflow: 'DataFlow' + operator: Operator # should be StatelessOperator but circular import! method_type: InvokeMethod """Which variable to take as the key for this StatefulOperator""" @@ -76,6 +76,7 @@ class SelectAllNode(Node): Think of this as executing `SELECT * FROM cls`""" cls: Type collect_target: 'CollectNode' + assign_key_to: str @dataclass @@ -222,12 +223,10 @@ def __post_init__(self): self._id = Event._id_counter Event._id_counter += 1 - def propogate(self, result) -> Union['EventResult', list['Event']]: + def propogate(self, result, select_all_keys: Optional[list[str]]=None) -> Union['EventResult', list['Event']]: """Propogate this event through the Dataflow.""" - # TODO: keys should be structs containing Key and Opnode (as we need to know the entity (cls) and method to invoke for that particular key) - # the following method only works because we assume all the keys have the same entity and method - if self.dataflow is None:# or len(key_stack) == 0: + if self.dataflow is None: return EventResult(self._id, result) targets = self.dataflow.get_neighbors(self.target) @@ -235,17 +234,26 @@ def propogate(self, result) -> Union['EventResult', list['Event']]: if len(targets) == 0: return EventResult(self._id, result) else: - # keys = key_stack.pop() - # if not isinstance(keys, list): - # keys = [keys] collect_targets: list[Optional[CollectTarget]] # Events with SelectAllNodes need to be assigned a CollectTarget if isinstance(self.target, SelectAllNode): + assert select_all_keys + assert len(targets) == 1 + n = len(select_all_keys) collect_targets = [ - CollectTarget(self.target.collect_target, len(targets), i) - for i in range(len(targets)) + CollectTarget(self.target.collect_target, n, i) + for i in range(n) ] + return [Event( + targets[0], + self.variable_map | {self.target.assign_key_to: key}, + self.dataflow, + _id=self._id, + collect_target=ct) + + for ct, key in zip(collect_targets, select_all_keys)] + elif isinstance(self.target, OpNode) and self.target.collect_target is not None: collect_targets = [ self.target.collect_target for i in range(len(targets)) @@ -253,7 +261,7 @@ def propogate(self, result) -> Union['EventResult', list['Event']]: else: collect_targets = [self.collect_target for i in range(len(targets))] - if isinstance(self.target, OpNode) and self.target.is_conditional: + if (isinstance(self.target, OpNode) or isinstance(self.target, StatelessOpNode)) and self.target.is_conditional: # In this case there will be two targets depending on the condition edges = self.dataflow.nodes[self.target.id].outgoing_edges diff --git a/src/cascade/runtime/flink_runtime.py b/src/cascade/runtime/flink_runtime.py index 2a04b34..d9dfd33 100644 --- a/src/cascade/runtime/flink_runtime.py +++ b/src/cascade/runtime/flink_runtime.py @@ -111,21 +111,20 @@ def __init__(self, operator: StatelessOperator) -> None: def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): - key_stack = event.key_stack # should be handled by filters on this FlinkOperator assert(isinstance(event.target, StatelessOpNode)) logger.debug(f"FlinkStatelessOperator {self.operator.dataflow.name}[{event._id}]: Processing: {event.target.method_type}") if isinstance(event.target.method_type, InvokeMethod): - result = self.operator.handle_invoke_method(event.target.method_type, variable_map=event.variable_map, key_stack=key_stack) + result = self.operator.handle_invoke_method(event.target.method_type, variable_map=event.variable_map, key_stack=[]) else: raise Exception(f"A StatelessOperator cannot compute event type: {event.target.method_type}") if event.target.assign_result_to is not None: event.variable_map[event.target.assign_result_to] = result - new_events = event.propogate(key_stack, result) + new_events = event.propogate(result) if isinstance(new_events, EventResult): logger.debug(f"FlinkStatelessOperator {self.operator.dataflow.name}[{event._id}]: Returned {new_events}") yield new_events @@ -157,11 +156,13 @@ def process_element(self, event: Event, ctx: 'ProcessFunction.Context'): logger.debug(f"SelectAllOperator [{event.target.cls.__name__}]: Selecting all") # Yield all the keys we now about - event.key_stack.append(state) + # event.key_stack.append(state) + new_keys = state num_events = len(state) # Propogate the event to the next node - new_events = event.propogate(event.key_stack, None) + new_events = event.propogate(None, select_all_keys=new_keys) + print(len(new_events), num_events) assert num_events == len(new_events) logger.debug(f"SelectAllOperator [{event.target.cls.__name__}]: Propogated {num_events} events with target: {event.target.collect_target}") @@ -173,7 +174,6 @@ class FlinkCollectOperator(KeyedProcessFunction): """Flink implementation of a merge operator.""" def __init__(self): #, merge_node: MergeNode) -> None: self.collection: ValueState = None # type: ignore (expect state to be initialised on .open()) - #self.node = merge_node def open(self, runtime_context: RuntimeContext): descriptor = ValueStateDescriptor("merge_state", Types.PICKLED_BYTE_ARRAY()) @@ -466,7 +466,7 @@ def add_stateless_operator(self, flink_op: FlinkStatelessOperator): op_stream = ( self.stateless_op_stream - .filter(lambda e: e.target.dataflow.name == flink_op.operator.dataflow.name) + .filter(lambda e: e.target.operator.dataflow.name == flink_op.operator.dataflow.name) .process(flink_op) .name("STATELESS DATAFLOW: " + flink_op.operator.dataflow.name) ) diff --git a/tests/integration/flink-runtime/test_select_all.py b/tests/integration/flink-runtime/test_select_all.py index 2b4de65..9ade211 100644 --- a/tests/integration/flink-runtime/test_select_all.py +++ b/tests/integration/flink-runtime/test_select_all.py @@ -1,164 +1,160 @@ -# """ -# Basically we need a way to search through all state. -# """ -# import math -# import random -# from dataclasses import dataclass -# from typing import Any - -# from pyflink.datastream.data_stream import CloseableIterator - -# from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, Event, EventResult, Filter, InitClass, InvokeMethod, MergeNode, OpNode, SelectAllNode -# from cascade.dataflow.operator import StatefulOperator, StatelessOperator -# from cascade.runtime.flink_runtime import FlinkOperator, FlinkRuntime, FlinkStatelessOperator -# from confluent_kafka import Producer -# import time -# import pytest - -# @dataclass -# class Geo: -# x: int -# y: int - -# class Hotel: -# def __init__(self, name: str, loc: Geo): -# self.name = name -# self.loc = loc - -# def get_name(self) -> str: -# return self.name +""" +The select all operator is used to fetch all keys for a single entity +""" +import math +import random +from dataclasses import dataclass +from typing import Any + +from pyflink.datastream.data_stream import CloseableIterator + +from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, Event, EventResult, InitClass, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode +from cascade.dataflow.operator import StatefulOperator, StatelessOperator +from cascade.runtime.flink_runtime import FlinkOperator, FlinkRuntime, FlinkStatelessOperator +import time +import pytest + +@dataclass +class Geo: + x: int + y: int + +class Hotel: + def __init__(self, name: str, loc: Geo): + self.name = name + self.loc = loc + + def get_name(self) -> str: + return self.name -# def distance(self, loc: Geo) -> float: -# return math.sqrt((self.loc.x - loc.x) ** 2 + (self.loc.y - loc.y) ** 2) + def distance(self, loc: Geo) -> float: + return math.sqrt((self.loc.x - loc.x) ** 2 + (self.loc.y - loc.y) ** 2) -# def __repr__(self) -> str: -# return f"Hotel({self.name}, {self.loc})" - - -# def distance_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: -# key_stack.pop() -# loc = variable_map["loc"] -# return math.sqrt((state.loc.x - loc.x) ** 2 + (state.loc.y - loc.y) ** 2) - -# def get_name_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: -# key_stack.pop() -# return state.name - -# hotel_op = StatefulOperator(Hotel, -# {"distance": distance_compiled, -# "get_name": get_name_compiled}, {}) - - - -# def get_nearby(hotels: list[Hotel], loc: Geo, dist: float): -# return [hotel.get_name() for hotel in hotels if hotel.distance(loc) < dist] - - -# # We compile just the predicate, the select is implemented using a selectall node -# def get_nearby_predicate_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): -# # the top of the key_stack is already the right key, so in this case we don't need to do anything -# # loc = variable_map["loc"] -# # we need the hotel_key for later. (body_compiled_0) -# variable_map["hotel_key"] = key_stack[-1] -# pass - -# def get_nearby_predicate_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) -> bool: -# loc = variable_map["loc"] -# dist = variable_map["dist"] -# hotel_dist = variable_map["hotel_distance"] -# # key_stack.pop() # shouldn't pop because this function is stateless -# return hotel_dist < dist - -# def get_nearby_body_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): -# key_stack.append(variable_map["hotel_key"]) - -# def get_nearby_body_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) -> str: -# return variable_map["hotel_name"] - -# get_nearby_op = StatelessOperator({ -# "get_nearby_predicate_compiled_0": get_nearby_predicate_compiled_0, -# "get_nearby_predicate_compiled_1": get_nearby_predicate_compiled_1, -# "get_nearby_body_compiled_0": get_nearby_body_compiled_0, -# "get_nearby_body_compiled_1": get_nearby_body_compiled_1, -# }, None) - -# # dataflow for getting all hotels within region -# df = DataFlow("get_nearby") -# n7 = CollectNode("get_nearby_result", "get_nearby_body") -# n0 = SelectAllNode(Hotel, n7) -# n1 = OpNode(get_nearby_op, InvokeMethod("get_nearby_predicate_compiled_0")) -# n2 = OpNode(hotel_op, InvokeMethod("distance"), assign_result_to="hotel_distance") -# n3 = OpNode(get_nearby_op, InvokeMethod("get_nearby_predicate_compiled_1"), is_conditional=True) -# n4 = OpNode(get_nearby_op, InvokeMethod("get_nearby_body_compiled_0")) -# n5 = OpNode(hotel_op, InvokeMethod("get_name"), assign_result_to="hotel_name") -# n6 = OpNode(get_nearby_op, InvokeMethod("get_nearby_body_compiled_1"), assign_result_to="get_nearby_body") - -# df.add_edge(Edge(n0, n1)) -# df.add_edge(Edge(n1, n2)) -# df.add_edge(Edge(n2, n3)) -# df.add_edge(Edge(n3, n4, if_conditional=True)) -# df.add_edge(Edge(n3, n7, if_conditional=False)) -# df.add_edge(Edge(n4, n5)) -# df.add_edge(Edge(n5, n6)) -# df.add_edge(Edge(n6, n7)) -# get_nearby_op.dataflow = df - -# @pytest.mark.integration -# def test_nearby_hotels(): -# runtime = FlinkRuntime("test_nearby_hotels") -# runtime.init() -# runtime.add_operator(FlinkOperator(hotel_op)) -# runtime.add_stateless_operator(FlinkStatelessOperator(get_nearby_op)) - -# # Create Hotels -# hotels = [] -# init_hotel = OpNode(hotel_op, InitClass()) -# random.seed(42) -# for i in range(20): -# coord_x = random.randint(-10, 10) -# coord_y = random.randint(-10, 10) -# hotel = Hotel(f"h_{i}", Geo(coord_x, coord_y)) -# event = Event(init_hotel, [hotel.name], {"name": hotel.name, "loc": hotel.loc}, None) -# runtime.send(event) -# hotels.append(hotel) - -# collected_iterator: CloseableIterator = runtime.run(run_async=True, collect=True) -# records = [] -# def wait_for_event_id(id: int) -> EventResult: -# for record in collected_iterator: -# records.append(record) -# print(f"Collected record: {record}") -# if record.event_id == id: -# return record + def __repr__(self) -> str: + return f"Hotel({self.name}, {self.loc})" + + +def distance_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: + loc = variable_map["loc"] + return math.sqrt((state.loc.x - loc.x) ** 2 + (state.loc.y - loc.y) ** 2) + +def get_name_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: + return state.name + +hotel_op = StatefulOperator(Hotel, + {"distance": distance_compiled, + "get_name": get_name_compiled}, {}) + + + +def get_nearby(hotels: list[Hotel], loc: Geo, dist: float): + return [hotel.get_name() for hotel in hotels if hotel.distance(loc) < dist] + + +# We compile just the predicate, the select is implemented using a selectall node +def get_nearby_predicate_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): + # the top of the key_stack is already the right key, so in this case we don't need to do anything + # loc = variable_map["loc"] + # we need the hotel_key for later. (body_compiled_0) + # variable_map["hotel_key"] = key_stack[-1] + pass + +def get_nearby_predicate_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) -> bool: + loc = variable_map["loc"] + dist = variable_map["dist"] + hotel_dist = variable_map["hotel_distance"] + # key_stack.pop() # shouldn't pop because this function is stateless + return hotel_dist < dist + +def get_nearby_body_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): + pass + +def get_nearby_body_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) -> str: + return variable_map["hotel_name"] + +get_nearby_op = StatelessOperator({ + "get_nearby_predicate_compiled_0": get_nearby_predicate_compiled_0, + "get_nearby_predicate_compiled_1": get_nearby_predicate_compiled_1, + "get_nearby_body_compiled_0": get_nearby_body_compiled_0, + "get_nearby_body_compiled_1": get_nearby_body_compiled_1, +}, None) + +# dataflow for getting all hotels within region +df = DataFlow("get_nearby") +n7 = CollectNode("get_nearby_result", "get_nearby_body") +n0 = SelectAllNode(Hotel, n7, assign_key_to="hotel_key") +n1 = StatelessOpNode(get_nearby_op, InvokeMethod("get_nearby_predicate_compiled_0")) +n2 = OpNode(Hotel, InvokeMethod("distance"), assign_result_to="hotel_distance", read_key_from="hotel_key") +n3 = StatelessOpNode(get_nearby_op, InvokeMethod("get_nearby_predicate_compiled_1"), is_conditional=True) +n4 = StatelessOpNode(get_nearby_op, InvokeMethod("get_nearby_body_compiled_0")) +n5 = OpNode(Hotel, InvokeMethod("get_name"), assign_result_to="hotel_name", read_key_from="hotel_key") +n6 = StatelessOpNode(get_nearby_op, InvokeMethod("get_nearby_body_compiled_1"), assign_result_to="get_nearby_body") + +df.add_edge(Edge(n0, n1)) +df.add_edge(Edge(n1, n2)) +df.add_edge(Edge(n2, n3)) +df.add_edge(Edge(n3, n4, if_conditional=True)) +df.add_edge(Edge(n3, n7, if_conditional=False)) +df.add_edge(Edge(n4, n5)) +df.add_edge(Edge(n5, n6)) +df.add_edge(Edge(n6, n7)) +get_nearby_op.dataflow = df + +@pytest.mark.integration +def test_nearby_hotels(): + runtime = FlinkRuntime("test_nearby_hotels") + runtime.init() + runtime.add_operator(FlinkOperator(hotel_op)) + runtime.add_stateless_operator(FlinkStatelessOperator(get_nearby_op)) + + # Create Hotels + hotels = [] + init_hotel = OpNode(Hotel, InitClass(), read_key_from="name") + random.seed(42) + for i in range(20): + coord_x = random.randint(-10, 10) + coord_y = random.randint(-10, 10) + hotel = Hotel(f"h_{i}", Geo(coord_x, coord_y)) + event = Event(init_hotel, {"name": hotel.name, "loc": hotel.loc}, None) + runtime.send(event) + hotels.append(hotel) + + collected_iterator: CloseableIterator = runtime.run(run_async=True, output='collect') + records = [] + def wait_for_event_id(id: int) -> EventResult: + for record in collected_iterator: + records.append(record) + print(f"Collected record: {record}") + if record.event_id == id: + return record -# def wait_for_n_records(num: int) -> list[EventResult]: -# i = 0 -# n_records = [] -# for record in collected_iterator: -# i += 1 -# records.append(record) -# n_records.append(record) -# print(f"Collected record: {record}") -# if i == num: -# return n_records - -# print("creating hotels") -# # Wait for hotels to be created -# wait_for_n_records(20) -# time.sleep(3) # wait for all hotels to be registered - -# dist = 5 -# loc = Geo(0, 0) -# # because of how the key stack works, we need to supply a key here -# event = Event(n0, ["workaround_key"], {"loc": loc, "dist": dist}, df) -# runtime.send(event, flush=True) + def wait_for_n_records(num: int) -> list[EventResult]: + i = 0 + n_records = [] + for record in collected_iterator: + i += 1 + records.append(record) + n_records.append(record) + print(f"Collected record: {record}") + if i == num: + return n_records + + print("creating hotels") + # Wait for hotels to be created + wait_for_n_records(20) + time.sleep(10) # wait for all hotels to be registered + + dist = 5 + loc = Geo(0, 0) + event = Event(n0, {"loc": loc, "dist": dist}, df) + runtime.send(event, flush=True) -# nearby = [] -# for hotel in hotels: -# if hotel.distance(loc) < dist: -# nearby.append(hotel.name) - -# event_result = wait_for_event_id(event._id) -# results = [r for r in event_result.result if r != None] -# print(nearby) -# assert set(results) == set(nearby) \ No newline at end of file + nearby = [] + for hotel in hotels: + if hotel.distance(loc) < dist: + nearby.append(hotel.name) + + event_result = wait_for_event_id(event._id) + results = [r for r in event_result.result if r != None] + print(nearby) + assert set(results) == set(nearby) \ No newline at end of file From 70b9bc8a47d247533d36ffb078312c0f61b6dff8 Mon Sep 17 00:00:00 2001 From: lucasvanmol Date: Mon, 20 Jan 2025 13:04:06 +0100 Subject: [PATCH 04/12] Cleanup propagation --- src/cascade/dataflow/dataflow.py | 160 ++++++++++++---------- src/cascade/runtime/flink_runtime.py | 8 +- tests/integration/flink-runtime/common.py | 6 - 3 files changed, 90 insertions(+), 84 deletions(-) diff --git a/src/cascade/dataflow/dataflow.py b/src/cascade/dataflow/dataflow.py index 2e12559..a788252 100644 --- a/src/cascade/dataflow/dataflow.py +++ b/src/cascade/dataflow/dataflow.py @@ -1,4 +1,4 @@ -from abc import ABC +from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any, Callable, List, Optional, Type, Union @@ -35,6 +35,10 @@ def __post_init__(self): self.id = Node._id_counter Node._id_counter += 1 + @abstractmethod + def propogate(self, event: 'Event', targets: list['Node'], result: Any, **kwargs) -> list['Event']: + pass + @dataclass class OpNode(Node): """A node in a `Dataflow` corresponding to a method call of a `StatefulOperator`. @@ -45,13 +49,57 @@ class OpNode(Node): method_type: Union[InitClass, InvokeMethod, Filter] read_key_from: str """Which variable to take as the key for this StatefulOperator""" + assign_result_to: Optional[str] = field(default=None) """What variable to assign the result of this node to, if any.""" is_conditional: bool = field(default=False) """Whether or not the boolean result of this node dictates the following path.""" collect_target: Optional['CollectTarget'] = field(default=None) """Whether the result of this node should go to a CollectNode.""" - + + def propogate(self, event: 'Event', targets: List[Node], result: Any) -> list['Event']: + return OpNode.propogate_opnode(self, event, targets, result) + + @staticmethod + def propogate_opnode(node: Union['OpNode', 'StatelessOpNode'], event: 'Event', targets: list[Node], result: Any) -> list['Event']: + if event.collect_target is not None: + # Assign new collect targets + collect_targets = [ + event.collect_target for i in range(len(targets)) + ] + else: + # Keep old collect targets + collect_targets = [node.collect_target for i in range(len(targets))] + + if node.is_conditional: + edges = event.dataflow.nodes[event.target.id].outgoing_edges + true_edges = [edge for edge in edges if edge.if_conditional] + false_edges = [edge for edge in edges if not edge.if_conditional] + if not (len(true_edges) == len(false_edges) == 1): + print(edges) + assert len(true_edges) == len(false_edges) == 1 + target_true = true_edges[0].to_node + target_false = false_edges[0].to_node + + + return [Event( + target_true if result else target_false, + event.variable_map, + event.dataflow, + _id=event._id, + collect_target=ct) + + for ct in collect_targets] + else: + return [Event( + target, + event.variable_map, + event.dataflow, + _id=event._id, + collect_target=ct) + + for target, ct in zip(targets, collect_targets)] + @dataclass class StatelessOpNode(Node): """A node in a `Dataflow` corresponding to a method call of a `StatelessOperator`. @@ -63,11 +111,15 @@ class StatelessOpNode(Node): """Which variable to take as the key for this StatefulOperator""" assign_result_to: Optional[str] = None + """What variable to assign the result of this node to, if any.""" is_conditional: bool = False """Whether or not the boolean result of this node dictates the following path.""" collect_target: Optional['CollectTarget'] = None """Whether the result of this node should go to a CollectNode.""" + def propogate(self, event: 'Event', targets: List[Node], result: Any) -> List['Event']: + return OpNode.propogate_opnode(self, event, targets, result) + @dataclass class SelectAllNode(Node): """A node type that will yield all items of an entity filtered by @@ -78,6 +130,21 @@ class SelectAllNode(Node): collect_target: 'CollectNode' assign_key_to: str + def propogate(self, event: 'Event', targets: List[Node], result: Any, keys: list[str]): + targets = event.dataflow.get_neighbors(event.target) + assert len(targets) == 1 + n = len(keys) + collect_targets = [ + CollectTarget(self.collect_target, n, i) + for i in range(n) + ] + return [Event( + targets[0], + event.variable_map | {self.assign_key_to: key}, + event.dataflow, + _id=event._id, + collect_target=ct) + for ct, key in zip(collect_targets, keys)] @dataclass class MergeNode(Node): @@ -98,6 +165,16 @@ class CollectNode(Node): read_results_from: str """The variable name in the variable map that the individual items put their result in.""" + def propogate(self, event: 'Event', targets: List[Node], result: Any, **kwargs) -> List['Event']: + collect_targets = [event.collect_target for i in range(len(targets))] + return [Event( + target, + event.variable_map, + event.dataflow, + _id=event._id, + collect_target=ct) + + for target, ct in zip(targets, collect_targets)] @dataclass class Edge(): @@ -234,82 +311,13 @@ def propogate(self, result, select_all_keys: Optional[list[str]]=None) -> Union[ if len(targets) == 0: return EventResult(self._id, result) else: - - collect_targets: list[Optional[CollectTarget]] - # Events with SelectAllNodes need to be assigned a CollectTarget - if isinstance(self.target, SelectAllNode): - assert select_all_keys - assert len(targets) == 1 - n = len(select_all_keys) - collect_targets = [ - CollectTarget(self.target.collect_target, n, i) - for i in range(n) - ] - return [Event( - targets[0], - self.variable_map | {self.target.assign_key_to: key}, - self.dataflow, - _id=self._id, - collect_target=ct) - - for ct, key in zip(collect_targets, select_all_keys)] - - elif isinstance(self.target, OpNode) and self.target.collect_target is not None: - collect_targets = [ - self.target.collect_target for i in range(len(targets)) - ] - else: - collect_targets = [self.collect_target for i in range(len(targets))] - - if (isinstance(self.target, OpNode) or isinstance(self.target, StatelessOpNode)) and self.target.is_conditional: - # In this case there will be two targets depending on the condition - - edges = self.dataflow.nodes[self.target.id].outgoing_edges - true_edges = [edge for edge in edges if edge.if_conditional] - false_edges = [edge for edge in edges if not edge.if_conditional] - if not (len(true_edges) == len(false_edges) == 1): - print(edges) - assert len(true_edges) == len(false_edges) == 1 - target_true = true_edges[0].to_node - target_false = false_edges[0].to_node - - - return [Event( - target_true if result else target_false, - # key_stack + [key], - self.variable_map, - self.dataflow, - _id=self._id, - collect_target=ct) + current_node = self.target - for ct in collect_targets] - - elif len(targets) == 1: - # We assume that all keys need to go to the same target - # this is only used for SelectAll propogation - - return [Event( - targets[0], - # key_stack + [key], - self.variable_map, - self.dataflow, - _id=self._id, - collect_target=ct) - - for ct in collect_targets] + if isinstance(current_node, SelectAllNode): + assert select_all_keys + return current_node.propogate(self, targets, result, select_all_keys) else: - # An event with multiple targets should have the same number of - # keys in a list on top of its key stack - # assert len(targets) == len(keys) - return [Event( - target, - # key_stack + [key], - self.variable_map, - self.dataflow, - _id=self._id, - collect_target=ct) - - for target, ct in zip(targets, collect_targets)] + return current_node.propogate(self, targets, result) @dataclass class EventResult(): diff --git a/src/cascade/runtime/flink_runtime.py b/src/cascade/runtime/flink_runtime.py index d9dfd33..975526c 100644 --- a/src/cascade/runtime/flink_runtime.py +++ b/src/cascade/runtime/flink_runtime.py @@ -3,7 +3,7 @@ import time import uuid import threading -from typing import Literal, Optional, Type, Union +from typing import Any, Literal, Optional, Type, Union from pyflink.common.typeinfo import Types, get_gateway from pyflink.common import Configuration, DeserializationSchema, SerializationSchema, WatermarkStrategy from pyflink.datastream.connectors import DeliveryGuarantee @@ -36,6 +36,10 @@ class FlinkRegisterKeyNode(Node): key: str cls: Type + def propogate(self, event: Event, targets: list[Node], result: Any, **kwargs) -> list[Event]: + """A key registration event does not propogate.""" + return [] + class FlinkOperator(KeyedProcessFunction): """Wraps an `cascade.dataflow.datflow.StatefulOperator` in a KeyedProcessFunction so that it can run in Flink. """ @@ -162,7 +166,7 @@ def process_element(self, event: Event, ctx: 'ProcessFunction.Context'): # Propogate the event to the next node new_events = event.propogate(None, select_all_keys=new_keys) - print(len(new_events), num_events) + assert isinstance(new_events, list), "SelectAll nodes shouldn't directly produce EventResults" assert num_events == len(new_events) logger.debug(f"SelectAllOperator [{event.target.cls.__name__}]: Propogated {num_events} events with target: {event.target.collect_target}") diff --git a/tests/integration/flink-runtime/common.py b/tests/integration/flink-runtime/common.py index 7a676e2..67baee6 100644 --- a/tests/integration/flink-runtime/common.py +++ b/tests/integration/flink-runtime/common.py @@ -40,25 +40,19 @@ def __repr__(self): return f"Item(key='{self.key}', price={self.price})" def update_balance_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - # key_stack.pop() # final function state.balance += variable_map["amount"] return state.balance >= 0 def get_balance_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - # key_stack.pop() # final function return state.balance def get_price_compiled(variable_map: dict[str, Any], state: Item, key_stack: list[str]) -> Any: - # key_stack.pop() # final function return state.price -# Items (or other operators) are passed by key always def buy_item_0_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - # key_stack.append(variable_map["item_key"]) return None def buy_item_1_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - # key_stack.pop() state.balance = state.balance - variable_map["item_price"] return state.balance >= 0 From 97545d7d53c52cd44feea192643aec4e5fbc710f Mon Sep 17 00:00:00 2001 From: lucasvanmol Date: Mon, 20 Jan 2025 14:01:31 +0100 Subject: [PATCH 05/12] Remove mention of key_stack in main lib --- src/cascade/dataflow/dataflow.py | 13 ++++--- src/cascade/dataflow/operator.py | 38 +++++++++---------- src/cascade/runtime/flink_runtime.py | 8 +--- tests/integration/flink-runtime/common.py | 14 +++---- .../flink-runtime/test_select_all.py | 17 +++------ 5 files changed, 40 insertions(+), 50 deletions(-) diff --git a/src/cascade/dataflow/dataflow.py b/src/cascade/dataflow/dataflow.py index a788252..960a865 100644 --- a/src/cascade/dataflow/dataflow.py +++ b/src/cascade/dataflow/dataflow.py @@ -1,6 +1,12 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any, Callable, List, Optional, Type, Union +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # Prevent circular imports + from cascade.dataflow.operator import StatelessOperator + class Operator(ABC): pass @@ -106,7 +112,7 @@ class StatelessOpNode(Node): A `Dataflow` may reference the same `StatefulOperator` multiple times. The `StatefulOperator` that this node belongs to is referenced by `cls`.""" - operator: Operator # should be StatelessOperator but circular import! + operator: 'StatelessOperator' method_type: InvokeMethod """Which variable to take as the key for this StatefulOperator""" @@ -273,11 +279,6 @@ class Event(): target: 'Node' """The Node that this Event wants to go to.""" - # key_stack: list[str] - """The keys this event is concerned with. - The top of the stack, i.e. `key_stack[-1]`, should always correspond to a key - on the StatefulOperator of `target.cls` if `target` is an `OpNode`.""" - variable_map: dict[str, Any] """A mapping of variable identifiers to values. If `target` is an `OpNode` this map should include the variables needed for that method.""" diff --git a/src/cascade/dataflow/operator.py b/src/cascade/dataflow/operator.py index 6fca4d6..56d3e45 100644 --- a/src/cascade/dataflow/operator.py +++ b/src/cascade/dataflow/operator.py @@ -10,20 +10,19 @@ class MethodCall(Generic[T], Protocol): It corresponds to functions with the following signature: ```py - def my_compiled_method(*args: Any, state: T, key_stack: list[str], **kwargs: Any) -> Any: + def my_compiled_method(variable_map: dict[str, Any], state: T) -> Any ... ``` - `T` corresponds to a Python class, which, if modified, should return as the 2nd item in the tuple. - - The first item in the returned tuple corresponds to the actual return value of the function. + The variable_map contains a mapping from identifiers (variables/keys) to + their values. + The state of type `T` corresponds to a Python class. - The third item in the tuple corresponds to the `key_stack` which should be updated accordingly. - Notably, a terminal function should pop a key off the `key_stack`, whereas a function that calls - other functions should push the correct key(s) onto the `key_stack`. + + The value returned corresponds to the value treturned by the function. """ - def __call__(self, variable_map: dict[str, Any], state: T, key_stack: list[str]) -> dict[str, Any]: ... + def __call__(self, variable_map: dict[str, Any], state: T) -> Any: ... """@private""" @@ -61,14 +60,13 @@ def buy_item(self, item: Item) -> bool: Here, the class could be turned into a StatefulOperator as follows: ```py - def user_get_balance(variable_map: dict[str, Any], state: User, key_stack: list[str]): - key_stack.pop() + def user_get_balance(variable_map: dict[str, Any], state: User): return state.balance - def user_buy_item_0(variable_map: dict[str, Any], state: User, key_stack: list[str]): - key_stack.append(variable_map['item_key']) + def user_buy_item_0(variable_map: dict[str, Any], state: User): + pass - def user_buy_item_1(variable_map: dict[str, Any], state: User, key_stack: list[str]): + def user_buy_item_1(variable_map: dict[str, Any], state: User): state.balance -= variable_map['item_get_price'] return state.balance >= 0 @@ -100,19 +98,19 @@ def handle_init_class(self, *args, **kwargs) -> T: """Create an instance of the underlying class. Equivalent to `T.__init__(*args, **kwargs)`.""" return self.entity(*args, **kwargs) - def handle_invoke_method(self, method: InvokeMethod, variable_map: dict[str, Any], state: T, key_stack: list[str]) -> dict[str, Any]: + def handle_invoke_method(self, method: InvokeMethod, variable_map: dict[str, Any], state: T) -> dict[str, Any]: """Invoke the method of the underlying class. The `cascade.dataflow.dataflow.InvokeMethod` object must contain a method identifier that exists on the underlying compiled class functions. - The state `T` and key_stack is passed along to the function, and may be modified. + The state `T` is passed along to the function, and may be modified. """ - return self._methods[method.method_name](variable_map=variable_map, state=state, key_stack=key_stack) + return self._methods[method.method_name](variable_map=variable_map, state=state) class StatelessMethodCall(Protocol): - def __call__(self, variable_map: dict[str, Any], key_stack: list[str]) -> Any: ... + def __call__(self, variable_map: dict[str, Any]) -> Any: ... """@private""" @@ -123,13 +121,13 @@ def __init__(self, methods: dict[str, StatelessMethodCall], dataflow: DataFlow) self._methods = methods self.dataflow = dataflow - def handle_invoke_method(self, method: InvokeMethod, variable_map: dict[str, Any], key_stack: list[str]) -> dict[str, Any]: + def handle_invoke_method(self, method: InvokeMethod, variable_map: dict[str, Any]) -> dict[str, Any]: """Invoke the method of the underlying class. The `cascade.dataflow.dataflow.InvokeMethod` object must contain a method identifier that exists on the underlying compiled class functions. - The state `T` and key_stack is passed along to the function, and may be modified. + The state `T` is passed along to the function, and may be modified. """ - return self._methods[method.method_name](variable_map=variable_map, key_stack=key_stack) + return self._methods[method.method_name](variable_map=variable_map) diff --git a/src/cascade/runtime/flink_runtime.py b/src/cascade/runtime/flink_runtime.py index 975526c..67cf194 100644 --- a/src/cascade/runtime/flink_runtime.py +++ b/src/cascade/runtime/flink_runtime.py @@ -53,7 +53,6 @@ def open(self, runtime_context: RuntimeContext): self.state: ValueState = runtime_context.get_state(descriptor) def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): - # key_stack = event.key_stack # should be handled by filters on this FlinkOperator assert(isinstance(event.target, OpNode)) @@ -70,7 +69,6 @@ def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): # Register the created key in FlinkSelectAllOperator register_key_event = Event( FlinkRegisterKeyNode(key, self.operator.entity), - # [], {}, None, _id = event._id @@ -79,11 +77,10 @@ def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): yield register_key_event # Pop this key from the key stack so that we exit - # key_stack.pop() self.state.update(pickle.dumps(result)) elif isinstance(event.target.method_type, InvokeMethod): state = pickle.loads(self.state.value()) - result = self.operator.handle_invoke_method(event.target.method_type, variable_map=event.variable_map, state=state, key_stack=[]) + result = self.operator.handle_invoke_method(event.target.method_type, variable_map=event.variable_map, state=state) # TODO: check if state actually needs to be updated if state is not None: @@ -121,7 +118,7 @@ def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): logger.debug(f"FlinkStatelessOperator {self.operator.dataflow.name}[{event._id}]: Processing: {event.target.method_type}") if isinstance(event.target.method_type, InvokeMethod): - result = self.operator.handle_invoke_method(event.target.method_type, variable_map=event.variable_map, key_stack=[]) + result = self.operator.handle_invoke_method(event.target.method_type, variable_map=event.variable_map) else: raise Exception(f"A StatelessOperator cannot compute event type: {event.target.method_type}") @@ -160,7 +157,6 @@ def process_element(self, event: Event, ctx: 'ProcessFunction.Context'): logger.debug(f"SelectAllOperator [{event.target.cls.__name__}]: Selecting all") # Yield all the keys we now about - # event.key_stack.append(state) new_keys = state num_events = len(state) diff --git a/tests/integration/flink-runtime/common.py b/tests/integration/flink-runtime/common.py index 67baee6..ccec426 100644 --- a/tests/integration/flink-runtime/common.py +++ b/tests/integration/flink-runtime/common.py @@ -39,28 +39,28 @@ def get_price(self) -> int: def __repr__(self): return f"Item(key='{self.key}', price={self.price})" -def update_balance_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: +def update_balance_compiled(variable_map: dict[str, Any], state: User) -> Any: state.balance += variable_map["amount"] return state.balance >= 0 -def get_balance_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: +def get_balance_compiled(variable_map: dict[str, Any], state: User) -> Any: return state.balance -def get_price_compiled(variable_map: dict[str, Any], state: Item, key_stack: list[str]) -> Any: +def get_price_compiled(variable_map: dict[str, Any], state: Item) -> Any: return state.price -def buy_item_0_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: +def buy_item_0_compiled(variable_map: dict[str, Any], state: User) -> Any: return None -def buy_item_1_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: +def buy_item_1_compiled(variable_map: dict[str, Any], state: User) -> Any: state.balance = state.balance - variable_map["item_price"] return state.balance >= 0 -def buy_2_items_0_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: +def buy_2_items_0_compiled(variable_map: dict[str, Any], state: User) -> Any: return None -def buy_2_items_1_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: +def buy_2_items_1_compiled(variable_map: dict[str, Any], state: User) -> Any: state.balance -= variable_map["item_prices"][0] + variable_map["item_prices"][1] return state.balance >= 0 diff --git a/tests/integration/flink-runtime/test_select_all.py b/tests/integration/flink-runtime/test_select_all.py index 9ade211..35c3265 100644 --- a/tests/integration/flink-runtime/test_select_all.py +++ b/tests/integration/flink-runtime/test_select_all.py @@ -34,11 +34,11 @@ def __repr__(self) -> str: return f"Hotel({self.name}, {self.loc})" -def distance_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: +def distance_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: loc = variable_map["loc"] return math.sqrt((state.loc.x - loc.x) ** 2 + (state.loc.y - loc.y) ** 2) -def get_name_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: +def get_name_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: return state.name hotel_op = StatefulOperator(Hotel, @@ -52,24 +52,19 @@ def get_nearby(hotels: list[Hotel], loc: Geo, dist: float): # We compile just the predicate, the select is implemented using a selectall node -def get_nearby_predicate_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): - # the top of the key_stack is already the right key, so in this case we don't need to do anything - # loc = variable_map["loc"] - # we need the hotel_key for later. (body_compiled_0) - # variable_map["hotel_key"] = key_stack[-1] +def get_nearby_predicate_compiled_0(variable_map: dict[str, Any]): pass -def get_nearby_predicate_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) -> bool: +def get_nearby_predicate_compiled_1(variable_map: dict[str, Any]) -> bool: loc = variable_map["loc"] dist = variable_map["dist"] hotel_dist = variable_map["hotel_distance"] - # key_stack.pop() # shouldn't pop because this function is stateless return hotel_dist < dist -def get_nearby_body_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): +def get_nearby_body_compiled_0(variable_map: dict[str, Any]): pass -def get_nearby_body_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) -> str: +def get_nearby_body_compiled_1(variable_map: dict[str, Any]) -> str: return variable_map["hotel_name"] get_nearby_op = StatelessOperator({ From 4f0679e0047fb90c43ab680daff57510454f2f97 Mon Sep 17 00:00:00 2001 From: lucasvanmol Date: Mon, 20 Jan 2025 15:15:18 +0100 Subject: [PATCH 06/12] Remove key stack from deathstar --- deathstar/demo.py | 29 +++++---- deathstar/entities/flight.py | 3 +- deathstar/entities/hotel.py | 11 ++-- deathstar/entities/recommendation.py | 60 ++++++++--------- deathstar/entities/search.py | 32 ++++------ deathstar/entities/user.py | 33 +++++----- deathstar/test_demo.py | 96 ++++++++++++++-------------- 7 files changed, 124 insertions(+), 140 deletions(-) diff --git a/deathstar/demo.py b/deathstar/demo.py index c42bdb7..68987a1 100644 --- a/deathstar/demo.py +++ b/deathstar/demo.py @@ -21,9 +21,9 @@ class DeathstarDemo(): def __init__(self, input_topic, output_topic): - self.init_user = OpNode(user_op, InitClass()) - self.init_hotel = OpNode(hotel_op, InitClass()) - self.init_flight = OpNode(flight_op, InitClass()) + self.init_user = OpNode(User, InitClass(), read_key_from="user_id") + self.init_hotel = OpNode(Hotel, InitClass(), read_key_from="key") + self.init_flight = OpNode(Flight, InitClass(), read_key_from="id") self.runtime = FlinkRuntime(input_topic, output_topic) def init_runtime(self): @@ -140,7 +140,7 @@ def populate(self): # populate users self.users = [User(f"Cornell_{i}", str(i) * 10) for i in range(501)] for user in self.users: - event = Event(self.init_user, [user.id], {"user_id": user.id, "password": user.password}, None) + event = Event(self.init_user, {"user_id": user.id, "password": user.password}, None) self.runtime.send(event) # populate hotels @@ -151,7 +151,7 @@ def populate(self): price = prices[i] hotel = Hotel(str(i), 10, geo, rate, price) self.hotels.append(hotel) - event = Event(self.init_hotel, [hotel.key], + event = Event(self.init_hotel, { "key": hotel.key, "cap": hotel.cap, @@ -164,13 +164,13 @@ def populate(self): # populate flights self.flights = [Flight(str(i), 10) for i in range(100)] for flight in self.flights[:-1]: - event = Event(self.init_flight, [flight.id], { + event = Event(self.init_flight, { "id": flight.id, "cap": flight.cap }, None) self.runtime.send(event) flight = self.flights[-1] - event = Event(self.init_flight, [flight.id], { + event = Event(self.init_flight, { "id": flight.id, "cap": flight.cap }, None) @@ -201,7 +201,7 @@ def search_hotel(self): lon = -122.095 + (random.randint(0, 325) - 157.0) / 1000.0 # We don't really use the in_date, out_date information - return Event(search_op.dataflow.entry, ["tempkey"], {"lat": lat, "lon": lon}, search_op.dataflow) + return Event(search_op.dataflow.entry, {"lat": lat, "lon": lon}, search_op.dataflow) def recommend(self, req_param=None): if req_param is None: @@ -214,13 +214,13 @@ def recommend(self, req_param=None): lat = 38.0235 + (random.randint(0, 481) - 240.5) / 1000.0 lon = -122.095 + (random.randint(0, 325) - 157.0) / 1000.0 - return Event(recommend_op.dataflow.entry, ["tempkey"], {"requirement": req_param, "lat": lat, "lon": lon}, recommend_op.dataflow) + return Event(recommend_op.dataflow.entry, {"requirement": req_param, "lat": lat, "lon": lon}, recommend_op.dataflow) def user_login(self): user_id = random.randint(0, 500) username = f"Cornell_{user_id}" password = str(user_id) * 10 - return Event(OpNode(user_op, InvokeMethod("login")), [username], {"password": password}, None) + return Event(OpNode(User, InvokeMethod("login"), read_key_from="user_key"), {"user_key": username, "password": password}, None) def reserve(self): hotel_id = random.randint(0, 99) @@ -230,7 +230,14 @@ def reserve(self): # user.order(flight, hotel) user_id = "Cornell_" + str(random.randint(0, 500)) - return Event(user_op.dataflows["order"].entry, [user_id], {"flight": str(flight_id), "hotel": str(hotel_id)}, user_op.dataflows["order"]) + return Event( + user_op.dataflows["order"].entry, + { + "user_key": user_id, + "flight_key": str(flight_id), + "hotel_key": str(hotel_id) + }, + user_op.dataflows["order"]) def deathstar_workload_generator(self): search_ratio = 0.6 diff --git a/deathstar/entities/flight.py b/deathstar/entities/flight.py index 60af68b..445ff9e 100644 --- a/deathstar/entities/flight.py +++ b/deathstar/entities/flight.py @@ -18,8 +18,7 @@ def reserve(self) -> bool: #### COMPILED FUNCTIONS (ORACLE) ##### -def reserve_compiled(variable_map: dict[str, Any], state: Flight, key_stack: list[str]) -> Any: - key_stack.pop() +def reserve_compiled(variable_map: dict[str, Any], state: Flight) -> Any: if state.cap <= 0: return False return True diff --git a/deathstar/entities/hotel.py b/deathstar/entities/hotel.py index 6689168..e57386d 100644 --- a/deathstar/entities/hotel.py +++ b/deathstar/entities/hotel.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Any, Optional +from typing import Any from cascade.dataflow.operator import StatefulOperator from geopy.distance import distance @@ -59,18 +59,15 @@ def __key__(self) -> int: #### COMPILED FUNCTIONS (ORACLE) ##### -def reserve_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: - key_stack.pop() +def reserve_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: if state.cap <= 0: return False return True -def get_geo_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: - key_stack.pop() +def get_geo_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: return state.geo -def get_price_compiled(variable_map: dict[str, Any], state: Hotel, key_stack: list[str]) -> Any: - key_stack.pop() +def get_price_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: return state.price hotel_op = StatefulOperator( diff --git a/deathstar/entities/recommendation.py b/deathstar/entities/recommendation.py index 7667210..99883ea 100644 --- a/deathstar/entities/recommendation.py +++ b/deathstar/entities/recommendation.py @@ -1,7 +1,7 @@ from typing import Any, Literal -from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode +from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode from cascade.dataflow.operator import StatelessOperator -from deathstar.entities.hotel import Geo, Hotel, hotel_op +from deathstar.entities.hotel import Geo, Hotel # Stateless class Recommendation(): @@ -23,51 +23,43 @@ def get_recommendations(requirement: Literal["distance", "price"], lat: float, l #### COMPILED FUNCTIONS (ORACLE) #### -def get_recs_if_cond(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_if_cond(variable_map: dict[str, Any]): return variable_map["requirement"] == "distance" # list comprehension entry -def get_recs_if_body_0(variable_map: dict[str, Any], key_stack: list[str]): - hotel_key = key_stack[-1] - # The body will need the hotel key (actually, couldn't we just take the top of the key stack again?) - variable_map["hotel_key"] = hotel_key - # The next node (Hotel.get_geo) will need the hotel key - key_stack.append(hotel_key) +def get_recs_if_body_0(variable_map: dict[str, Any]): + pass # list comprehension body -def get_recs_if_body_1(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_if_body_1(variable_map: dict[str, Any]): hotel_geo: Geo = variable_map["hotel_geo"] lat, lon = variable_map["lat"], variable_map["lon"] dist = hotel_geo.distance_km(lat, lon) return (dist, variable_map["hotel_key"]) # after list comprehension -def get_recs_if_body_2(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_if_body_2(variable_map: dict[str, Any]): distances = variable_map["distances"] min_dist = min(distances, key=lambda x: x[0])[0] variable_map["res"] = [hotel for dist, hotel in distances if dist == min_dist] -def get_recs_elif_cond(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_elif_cond(variable_map: dict[str, Any]): return variable_map["requirement"] == "price" # list comprehension entry -def get_recs_elif_body_0(variable_map: dict[str, Any], key_stack: list[str]): - hotel_key = key_stack[-1] - # The body will need the hotel key (actually, couldn't we just take the top of the key stack again?) - variable_map["hotel_key"] = hotel_key - # The next node (Hotel.get_geo) will need the hotel key - key_stack.append(hotel_key) +def get_recs_elif_body_0(variable_map: dict[str, Any]): + pass # list comprehension body -def get_recs_elif_body_1(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_elif_body_1(variable_map: dict[str, Any]): return (variable_map["hotel_price"], variable_map["hotel_key"]) # after list comprehension -def get_recs_elif_body_2(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_elif_body_2(variable_map: dict[str, Any]): prices = variable_map["prices"] min_price = min(prices, key=lambda x: x[0])[0] variable_map["res"] = [hotel for price, hotel in prices if price == min_price] @@ -76,7 +68,7 @@ def get_recs_elif_body_2(variable_map: dict[str, Any], key_stack: list[str]): # a future optimization might instead duplicate this piece of code over the two # branches, in order to reduce the number of splits by one -def get_recs_final(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_final(variable_map: dict[str, Any]): return variable_map["res"] @@ -93,24 +85,24 @@ def get_recs_final(variable_map: dict[str, Any], key_stack: list[str]): }, None) df = DataFlow("get_recommendations") -n1 = OpNode(recommend_op, InvokeMethod("get_recs_if_cond"), is_conditional=True) -n2 = OpNode(recommend_op, InvokeMethod("get_recs_if_body_0")) -n3 = OpNode(hotel_op, InvokeMethod("get_geo"), assign_result_to="hotel_geo") -n4 = OpNode(recommend_op, InvokeMethod("get_recs_if_body_1"), assign_result_to="distance") +n1 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_cond"), is_conditional=True) +n2 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_0")) +n3 = OpNode(Hotel, InvokeMethod("get_geo"), assign_result_to="hotel_geo", read_key_from="hotel_key") +n4 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_1"), assign_result_to="distance") n5 = CollectNode("distances", "distance") -n6 = OpNode(recommend_op, InvokeMethod("get_recs_if_body_2")) -ns1 = SelectAllNode(Hotel, n5) +n6 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_2")) +ns1 = SelectAllNode(Hotel, n5, assign_key_to="hotel_key") -n7 = OpNode(recommend_op, InvokeMethod("get_recs_elif_cond"), is_conditional=True) -n8 = OpNode(recommend_op, InvokeMethod("get_recs_elif_body_0")) -n9 = OpNode(hotel_op, InvokeMethod("get_price"), assign_result_to="hotel_price") -n10 = OpNode(recommend_op, InvokeMethod("get_recs_elif_body_1"), assign_result_to="price") +n7 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_cond"), is_conditional=True) +n8 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_0")) +n9 = OpNode(Hotel, InvokeMethod("get_price"), assign_result_to="hotel_price", read_key_from="hotel_key") +n10 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_1"), assign_result_to="price") n11 = CollectNode("prices", "price") -n12 = OpNode(recommend_op, InvokeMethod("get_recs_elif_body_2")) -ns2 = SelectAllNode(Hotel, n11) +n12 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_2")) +ns2 = SelectAllNode(Hotel, n11, assign_key_to="hotel_key") -n13 = OpNode(recommend_op, InvokeMethod("get_recs_final")) +n13 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_final")) df.add_edge(Edge(n1, ns1, if_conditional=True)) df.add_edge(Edge(n1, n7, if_conditional=False)) diff --git a/deathstar/entities/search.py b/deathstar/entities/search.py index a2782d2..0b508d3 100644 --- a/deathstar/entities/search.py +++ b/deathstar/entities/search.py @@ -1,5 +1,5 @@ from typing import Any -from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode +from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode from cascade.dataflow.operator import StatelessOperator from deathstar.entities.hotel import Geo, Hotel, hotel_op @@ -21,19 +21,11 @@ def nearby(lat: float, lon: float, in_date: int, out_date: int): # predicate 1 -def search_nearby_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): - # We assume that the top of the key stack is the hotel key. - # This assumption holds if the node before this one is a correctly - # configure SelectAllNode. - - hotel_key = key_stack[-1] - # The body will need the hotel key (actually, couldn't we just take the top of the key stack again?) - variable_map["hotel_key"] = hotel_key - # The next node (Hotel.get_geo) will need the hotel key - key_stack.append(hotel_key) +def search_nearby_compiled_0(variable_map: dict[str, Any]): + pass # predicate 2 -def search_nearby_compiled_1(variable_map: dict[str, Any], key_stack: list[str]): +def search_nearby_compiled_1(variable_map: dict[str, Any]): hotel_geo: Geo = variable_map["hotel_geo"] lat, lon = variable_map["lat"], variable_map["lon"] dist = hotel_geo.distance_km(lat, lon) @@ -42,11 +34,11 @@ def search_nearby_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) # body -def search_nearby_compiled_2(variable_map: dict[str, Any], key_stack: list[str]): +def search_nearby_compiled_2(variable_map: dict[str, Any]): return (variable_map["dist"], variable_map["hotel_key"]) # next line -def search_nearby_compiled_3(variable_map: dict[str, Any], key_stack: list[str]): +def search_nearby_compiled_3(variable_map: dict[str, Any]): distances = variable_map["distances"] hotels = [hotel for dist, hotel in sorted(distances)[:5]] return hotels @@ -60,14 +52,14 @@ def search_nearby_compiled_3(variable_map: dict[str, Any], key_stack: list[str]) }, None) df = DataFlow("search_nearby") -n1 = OpNode(search_op, InvokeMethod("search_nearby_compiled_0")) -n2 = OpNode(hotel_op, InvokeMethod("get_geo"), assign_result_to="hotel_geo") -n3 = OpNode(search_op, InvokeMethod("search_nearby_compiled_1"), is_conditional=True) -n4 = OpNode(search_op, InvokeMethod("search_nearby_compiled_2"), assign_result_to="search_body") +n1 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_0")) +n2 = OpNode(Hotel, InvokeMethod("get_geo"), assign_result_to="hotel_geo", read_key_from="hotel_key") +n3 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_1"), is_conditional=True) +n4 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_2"), assign_result_to="search_body") n5 = CollectNode("distances", "search_body") -n0 = SelectAllNode(Hotel, n5) +n0 = SelectAllNode(Hotel, n5, assign_key_to="hotel_key") -n6 = OpNode(search_op, InvokeMethod("search_nearby_compiled_3")) +n6 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_3")) df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n1, n2)) diff --git a/deathstar/entities/user.py b/deathstar/entities/user.py index 0234e91..95b135f 100644 --- a/deathstar/entities/user.py +++ b/deathstar/entities/user.py @@ -21,25 +21,22 @@ def order(self, flight: Flight, hotel: Hotel): #### COMPILED FUNCTIONS (ORACLE) ##### -def check_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() +def check_compiled(variable_map: dict[str, Any], state: User) -> Any: return state.password == variable_map["password"] -def order_compiled_entry_0(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append(variable_map["hotel"]) +def order_compiled_entry_0(variable_map: dict[str, Any], state: User) -> Any: + pass -def order_compiled_entry_1(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append(variable_map["flight"]) +def order_compiled_entry_1(variable_map: dict[str, Any], state: User) -> Any: + pass -def order_compiled_if_cond(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: +def order_compiled_if_cond(variable_map: dict[str, Any], state: User) -> Any: return variable_map["hotel_reserve"] and variable_map["flight_reserve"] -def order_compiled_if_body(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() +def order_compiled_if_body(variable_map: dict[str, Any], state: User) -> Any: return True -def order_compiled_else_body(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() +def order_compiled_else_body(variable_map: dict[str, Any], state: User) -> Any: return False user_op = StatefulOperator( @@ -59,13 +56,13 @@ def order_compiled_else_body(variable_map: dict[str, Any], state: User, key_stac # will try to automatically parallelize this. # There is also no user entry (this could also be an optimization) df = DataFlow("user_order") -n0 = OpNode(user_op, InvokeMethod("order_compiled_entry_0")) -n1 = OpNode(hotel_op, InvokeMethod("reserve"), assign_result_to="hotel_reserve") -n2 = OpNode(user_op, InvokeMethod("order_compiled_entry_1")) -n3 = OpNode(flight_op, InvokeMethod("reserve"), assign_result_to="flight_reserve") -n4 = OpNode(user_op, InvokeMethod("order_compiled_if_cond"), is_conditional=True) -n5 = OpNode(user_op, InvokeMethod("order_compiled_if_body")) -n6 = OpNode(user_op, InvokeMethod("order_compiled_else_body")) +n0 = OpNode(User, InvokeMethod("order_compiled_entry_0"), read_key_from="user_key") +n1 = OpNode(Hotel, InvokeMethod("reserve"), assign_result_to="hotel_reserve", read_key_from="hotel_key") +n2 = OpNode(User, InvokeMethod("order_compiled_entry_1"), read_key_from="user_key") +n3 = OpNode(Flight, InvokeMethod("reserve"), assign_result_to="flight_reserve", read_key_from="flight_key") +n4 = OpNode(User, InvokeMethod("order_compiled_if_cond"), is_conditional=True, read_key_from="user_key") +n5 = OpNode(User, InvokeMethod("order_compiled_if_body"), read_key_from="user_key") +n6 = OpNode(User, InvokeMethod("order_compiled_else_body"), read_key_from="user_key") df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n1, n2)) diff --git a/deathstar/test_demo.py b/deathstar/test_demo.py index f709948..a7e674e 100644 --- a/deathstar/test_demo.py +++ b/deathstar/test_demo.py @@ -1,49 +1,49 @@ -# from deathstar.demo import DeathstarDemo, DeathstarClient -# import time -# import pytest - -# @pytest.mark.integration -# def test_deathstar_demo(): -# ds = DeathstarDemo("deathstardemo-test", "dsd-out") -# ds.init_runtime() -# ds.runtime.run(run_async=True) -# print("Populating, press enter to go to the next step when done") -# ds.populate() - -# client = DeathstarClient("deathstardemo-test", "dsd-out") -# input() -# print("testing user login") -# event = client.user_login() -# client.send(event) - -# input() -# print("testing reserve") -# event = client.reserve() -# client.send(event) - -# input() -# print("testing search") -# event = client.search_hotel() -# client.send(event) - -# input() -# print("testing recommend (distance)") -# time.sleep(0.5) -# event = client.recommend(req_param="distance") -# client.send(event) - -# input() -# print("testing recommend (price)") -# time.sleep(0.5) -# event = client.recommend(req_param="price") -# client.send(event) - -# print(client.client._futures) -# input() -# print("done!") -# print(client.client._futures) - - -# if __name__ == "__main__": -# test_deathstar_demo() \ No newline at end of file +from deathstar.demo import DeathstarDemo, DeathstarClient +import time +import pytest + +@pytest.mark.integration +def test_deathstar_demo(): + ds = DeathstarDemo("deathstardemo-test", "dsd-out") + ds.init_runtime() + ds.runtime.run(run_async=True) + print("Populating, press enter to go to the next step when done") + ds.populate() + + client = DeathstarClient("deathstardemo-test", "dsd-out") + input() + print("testing user login") + event = client.user_login() + client.send(event) + + input() + print("testing reserve") + event = client.reserve() + client.send(event) + + input() + print("testing search") + event = client.search_hotel() + client.send(event) + + input() + print("testing recommend (distance)") + time.sleep(0.5) + event = client.recommend(req_param="distance") + client.send(event) + + input() + print("testing recommend (price)") + time.sleep(0.5) + event = client.recommend(req_param="price") + client.send(event) + + print(client.client._futures) + input() + print("done!") + print(client.client._futures) + + +if __name__ == "__main__": + test_deathstar_demo() \ No newline at end of file From fa6049727622c5e0466e967e9c8f8c6c0cb13f8b Mon Sep 17 00:00:00 2001 From: lucasvanmol Date: Mon, 20 Jan 2025 15:49:32 +0100 Subject: [PATCH 07/12] remove key_stack from test_programs --- test_programs/expected/checkout_item.py | 20 +++---- test_programs/expected/checkout_two_items.py | 20 +++---- .../expected/deathstar_recommendation.py | 60 ++++++++----------- test_programs/expected/deathstar_search.py | 35 ++++------- test_programs/expected/deathstar_user.py | 33 +++++----- 5 files changed, 71 insertions(+), 97 deletions(-) diff --git a/test_programs/expected/checkout_item.py b/test_programs/expected/checkout_item.py index fd256bf..75a32fa 100644 --- a/test_programs/expected/checkout_item.py +++ b/test_programs/expected/checkout_item.py @@ -1,29 +1,27 @@ from typing import Any -# from ..target.checkout_item import User, Item -# from cascade.dataflow.dataflow import DataFlow, OpNode, InvokeMethod, Edge -def buy_item_0_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append(variable_map['item_key']) +from cascade.dataflow.dataflow import DataFlow, Edge, InvokeMethod, OpNode +from test_programs.target.checkout_item import User, Item + +def buy_item_0_compiled(variable_map: dict[str, Any], state: User) -> Any: return None -def buy_item_1_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() +def buy_item_1_compiled(variable_map: dict[str, Any], state: User) -> Any: item_price_0 = variable_map['item_price_0'] state.balance -= item_price_0 return state.balance >= 0 -def get_price_0_compiled(variable_map: dict[str, Any], state: Item, key_stack: list[str]) -> Any: - key_stack.pop() +def get_price_0_compiled(variable_map: dict[str, Any], state: Item) -> Any: return state.price def user_buy_item_df(): df = DataFlow("user.buy_item") - n0 = OpNode(User, InvokeMethod("buy_item_0")) - n1 = OpNode(Item, InvokeMethod("get_price"), assign_result_to="item_price") - n2 = OpNode(User, InvokeMethod("buy_item_1")) + n0 = OpNode(User, InvokeMethod("buy_item_0"), read_key_from="user_key") + n1 = OpNode(Item, InvokeMethod("get_price"), assign_result_to="item_price", read_key_from="item_key") + n2 = OpNode(User, InvokeMethod("buy_item_1"), read_key_from="user_key") df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n1, n2)) df.entry = n0 diff --git a/test_programs/expected/checkout_two_items.py b/test_programs/expected/checkout_two_items.py index c3784bd..4784fa0 100644 --- a/test_programs/expected/checkout_two_items.py +++ b/test_programs/expected/checkout_two_items.py @@ -1,15 +1,12 @@ from typing import Any -# from ..target.checkout_item import User, Item -# from cascade.dataflow.dataflow import DataFlow, OpNode, InvokeMethod, Edge +from cascade.dataflow.dataflow import DataFlow, OpNode, InvokeMethod, Edge +from test_programs.target.checkout_two_items import User, Item -def buy_two_items_0_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append(variable_map['item_1_key']) - key_stack.append(variable_map['item_2_key']) +def buy_two_items_0_compiled(variable_map: dict[str, Any], state: User) -> Any: return None -def buy_two_items_1_compiled(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() +def buy_two_items_1_compiled(variable_map: dict[str, Any], state: User) -> Any: item_price_1_0 = variable_map['item_price_1_0'] item_price_2_0 = variable_map['item_price_2_0'] total_price_0 = item_price_1_0 + item_price_2_0 @@ -17,16 +14,15 @@ def buy_two_items_1_compiled(variable_map: dict[str, Any], state: User, key_stac return state.balance >= 0 -def get_price_0_compiled(variable_map: dict[str, Any], state: Item, key_stack: list[str]) -> Any: - key_stack.pop() +def get_price_0_compiled(variable_map: dict[str, Any], state: Item) -> Any: return state.price def user_buy_item_df(): df = DataFlow("user.buy_item") - n0 = OpNode(User, InvokeMethod("buy_item_0")) - n1 = OpNode(Item, InvokeMethod("get_price"), assign_result_to="item_price") - n2 = OpNode(User, InvokeMethod("buy_item_1")) + n0 = OpNode(User, InvokeMethod("buy_item_0"), read_key_from="user_key") + n1 = OpNode(Item, InvokeMethod("get_price"), assign_result_to="item_price", read_key_from="item_key") + n2 = OpNode(User, InvokeMethod("buy_item_1"), read_key_from="user_key") df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n1, n2)) df.entry = n0 diff --git a/test_programs/expected/deathstar_recommendation.py b/test_programs/expected/deathstar_recommendation.py index 436aa5d..8a8a727 100644 --- a/test_programs/expected/deathstar_recommendation.py +++ b/test_programs/expected/deathstar_recommendation.py @@ -1,53 +1,45 @@ from typing import Any, Literal -from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode +from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode from cascade.dataflow.operator import StatelessOperator -def get_recs_if_cond(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_if_cond(variable_map: dict[str, Any]): return variable_map["requirement"] == "distance" # list comprehension entry -def get_recs_if_body_0(variable_map: dict[str, Any], key_stack: list[str]): - hotel_key = key_stack[-1] - # The body will need the hotel key (actually, couldn't we just take the top of the key stack again?) - variable_map["hotel_key"] = hotel_key - # The next node (Hotel.get_geo) will need the hotel key - key_stack.append(hotel_key) +def get_recs_if_body_0(variable_map: dict[str, Any]): + pass # list comprehension body -def get_recs_if_body_1(variable_map: dict[str, Any], key_stack: list[str]): - hotel_geo: Geo = variable_map["hotel_geo"] +def get_recs_if_body_1(variable_map: dict[str, Any]): + hotel_geo = variable_map["hotel_geo"] lat, lon = variable_map["lat"], variable_map["lon"] dist = hotel_geo.distance_km(lat, lon) return (dist, variable_map["hotel_key"]) # after list comprehension -def get_recs_if_body_2(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_if_body_2(variable_map: dict[str, Any]): distances = variable_map["distances"] min_dist = min(distances, key=lambda x: x[0])[0] variable_map["res"] = [hotel for dist, hotel in distances if dist == min_dist] -def get_recs_elif_cond(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_elif_cond(variable_map: dict[str, Any]): return variable_map["requirement"] == "price" # list comprehension entry -def get_recs_elif_body_0(variable_map: dict[str, Any], key_stack: list[str]): - hotel_key = key_stack[-1] - # The body will need the hotel key (actually, couldn't we just take the top of the key stack again?) - variable_map["hotel_key"] = hotel_key - # The next node (Hotel.get_geo) will need the hotel key - key_stack.append(hotel_key) +def get_recs_elif_body_0(variable_map: dict[str, Any]): + pass # list comprehension body -def get_recs_elif_body_1(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_elif_body_1(variable_map: dict[str, Any]): return (variable_map["hotel_price"], variable_map["hotel_key"]) # after list comprehension -def get_recs_elif_body_2(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_elif_body_2(variable_map: dict[str, Any]): prices = variable_map["prices"] min_price = min(prices, key=lambda x: x[0])[0] variable_map["res"] = [hotel for price, hotel in prices if price == min_price] @@ -56,7 +48,7 @@ def get_recs_elif_body_2(variable_map: dict[str, Any], key_stack: list[str]): # a future optimization might instead duplicate this piece of code over the two # branches, in order to reduce the number of splits by one -def get_recs_final(variable_map: dict[str, Any], key_stack: list[str]): +def get_recs_final(variable_map: dict[str, Any]): return variable_map["res"] @@ -74,24 +66,24 @@ def get_recs_final(variable_map: dict[str, Any], key_stack: list[str]): def get_recommendations_df(): df = DataFlow("get_recommendations") - n1 = OpNode(recommend_op, InvokeMethod("get_recs_if_cond"), is_conditional=True) - n2 = OpNode(recommend_op, InvokeMethod("get_recs_if_body_0")) - n3 = OpNode(hotel_op, InvokeMethod("get_geo"), assign_result_to="hotel_geo") - n4 = OpNode(recommend_op, InvokeMethod("get_recs_if_body_1"), assign_result_to="distance") + n1 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_cond"), is_conditional=True) + n2 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_0")) + n3 = OpNode(Hotel, InvokeMethod("get_geo"), assign_result_to="hotel_geo", read_key_from="hotel_key") + n4 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_1"), assign_result_to="distance") n5 = CollectNode("distances", "distance") - n6 = OpNode(recommend_op, InvokeMethod("get_recs_if_body_2")) - ns1 = SelectAllNode(Hotel, n5) + n6 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_2")) + ns1 = SelectAllNode(Hotel, n5, assign_key_to="hotel_key") - n7 = OpNode(recommend_op, InvokeMethod("get_recs_elif_cond"), is_conditional=True) - n8 = OpNode(recommend_op, InvokeMethod("get_recs_elif_body_0")) - n9 = OpNode(hotel_op, InvokeMethod("get_price"), assign_result_to="hotel_price") - n10 = OpNode(recommend_op, InvokeMethod("get_recs_elif_body_1"), assign_result_to="price") + n7 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_cond"), is_conditional=True) + n8 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_0")) + n9 = OpNode(Hotel, InvokeMethod("get_price"), assign_result_to="hotel_price", read_key_from="hotel_key") + n10 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_1"), assign_result_to="price") n11 = CollectNode("prices", "price") - n12 = OpNode(recommend_op, InvokeMethod("get_recs_elif_body_2")) - ns2 = SelectAllNode(Hotel, n11) + n12 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_2")) + ns2 = SelectAllNode(Hotel, n11, assign_key_to="hotel_key") - n13 = OpNode(recommend_op, InvokeMethod("get_recs_final")) + n13 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_final")) df.add_edge(Edge(n1, ns1, if_conditional=True)) df.add_edge(Edge(n1, n7, if_conditional=False)) diff --git a/test_programs/expected/deathstar_search.py b/test_programs/expected/deathstar_search.py index 06cbec0..cd20593 100644 --- a/test_programs/expected/deathstar_search.py +++ b/test_programs/expected/deathstar_search.py @@ -1,24 +1,15 @@ from typing import Any -from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode +from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode from cascade.dataflow.operator import StatelessOperator - # predicate 1 -def search_nearby_compiled_0(variable_map: dict[str, Any], key_stack: list[str]): - # We assume that the top of the key stack is the hotel key. - # This assumption holds if the node before this one is a correctly - # configure SelectAllNode. - - hotel_key = key_stack[-1] - # The body will need the hotel key (actually, couldn't we just take the top of the key stack again?) - variable_map["hotel_key"] = hotel_key - # The next node (Hotel.get_geo) will need the hotel key - key_stack.append(hotel_key) +def search_nearby_compiled_0(variable_map: dict[str, Any]): + pass # predicate 2 -def search_nearby_compiled_1(variable_map: dict[str, Any], key_stack: list[str]): - hotel_geo = variable_map["hotel_geo"] +def search_nearby_compiled_1(variable_map: dict[str, Any]): + hotel_geo: Geo = variable_map["hotel_geo"] lat, lon = variable_map["lat"], variable_map["lon"] dist = hotel_geo.distance_km(lat, lon) variable_map["dist"] = dist @@ -26,11 +17,11 @@ def search_nearby_compiled_1(variable_map: dict[str, Any], key_stack: list[str]) # body -def search_nearby_compiled_2(variable_map: dict[str, Any], key_stack: list[str]): +def search_nearby_compiled_2(variable_map: dict[str, Any]): return (variable_map["dist"], variable_map["hotel_key"]) # next line -def search_nearby_compiled_3(variable_map: dict[str, Any], key_stack: list[str]): +def search_nearby_compiled_3(variable_map: dict[str, Any]): distances = variable_map["distances"] hotels = [hotel for dist, hotel in sorted(distances)[:5]] return hotels @@ -45,14 +36,14 @@ def search_nearby_compiled_3(variable_map: dict[str, Any], key_stack: list[str]) def search_nearby_df(): df = DataFlow("search_nearby") - n1 = OpNode(search_op, InvokeMethod("search_nearby_compiled_0")) - n2 = OpNode(hotel_op, InvokeMethod("get_geo"), assign_result_to="hotel_geo") - n3 = OpNode(search_op, InvokeMethod("search_nearby_compiled_1"), is_conditional=True) - n4 = OpNode(search_op, InvokeMethod("search_nearby_compiled_2"), assign_result_to="search_body") + n1 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_0")) + n2 = OpNode(Hotel, InvokeMethod("get_geo"), assign_result_to="hotel_geo", read_key_from="hotel_key") + n3 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_1"), is_conditional=True) + n4 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_2"), assign_result_to="search_body") n5 = CollectNode("distances", "search_body") - n0 = SelectAllNode(Hotel, n5) + n0 = SelectAllNode(Hotel, n5, assign_key_to="hotel_key") - n6 = OpNode(search_op, InvokeMethod("search_nearby_compiled_3")) + n6 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_3")) df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n1, n2)) diff --git a/test_programs/expected/deathstar_user.py b/test_programs/expected/deathstar_user.py index 5aea434..64985ea 100644 --- a/test_programs/expected/deathstar_user.py +++ b/test_programs/expected/deathstar_user.py @@ -2,24 +2,21 @@ from cascade.dataflow.dataflow import DataFlow, Edge, InvokeMethod, OpNode from cascade.dataflow.operator import StatefulOperator -def order_compiled_entry_0(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append(variable_map["hotel"]) +def order_compiled_entry_0(variable_map: dict[str, Any], state: User) -> Any: + pass -def order_compiled_entry_1(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.append(variable_map["flight"]) +def order_compiled_entry_1(variable_map: dict[str, Any], state: User) -> Any: + pass -def order_compiled_if_cond(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: +def order_compiled_if_cond(variable_map: dict[str, Any], state: User) -> Any: return variable_map["hotel_reserve"] and variable_map["flight_reserve"] -def order_compiled_if_body(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() +def order_compiled_if_body(variable_map: dict[str, Any], state: User) -> Any: return True -def order_compiled_else_body(variable_map: dict[str, Any], state: User, key_stack: list[str]) -> Any: - key_stack.pop() +def order_compiled_else_body(variable_map: dict[str, Any], state: User) -> Any: return False - user_op = StatefulOperator( User, { @@ -29,7 +26,7 @@ def order_compiled_else_body(variable_map: dict[str, Any], state: User, key_stac "order_compiled_if_body": order_compiled_if_body, "order_compiled_else_body": order_compiled_else_body }, - {} # dataflows (filled later) + {} ) # For now, the dataflow will be serial instead of parallel (calling hotel, then @@ -39,13 +36,13 @@ def order_compiled_else_body(variable_map: dict[str, Any], state: User, key_stac # before the first entity call). def user_order_df(): df = DataFlow("user_order") - n0 = OpNode(user_op, InvokeMethod("order_compiled_entry_0")) - n1 = OpNode(hotel_op, InvokeMethod("reserve"), assign_result_to="hotel_reserve") - n2 = OpNode(user_op, InvokeMethod("order_compiled_entry_1")) - n3 = OpNode(flight_op, InvokeMethod("reserve"), assign_result_to="flight_reserve") - n4 = OpNode(user_op, InvokeMethod("order_compiled_if_cond"), is_conditional=True) - n5 = OpNode(user_op, InvokeMethod("order_compiled_if_body")) - n6 = OpNode(user_op, InvokeMethod("order_compiled_else_body")) + n0 = OpNode(User, InvokeMethod("order_compiled_entry_0"), read_key_from="user_key") + n1 = OpNode(Hotel, InvokeMethod("reserve"), assign_result_to="hotel_reserve", read_key_from="hotel_key") + n2 = OpNode(User, InvokeMethod("order_compiled_entry_1"), read_key_from="user_key") + n3 = OpNode(Flight, InvokeMethod("reserve"), assign_result_to="flight_reserve", read_key_from="flight_key") + n4 = OpNode(User, InvokeMethod("order_compiled_if_cond"), is_conditional=True, read_key_from="user_key") + n5 = OpNode(User, InvokeMethod("order_compiled_if_body"), read_key_from="user_key") + n6 = OpNode(User, InvokeMethod("order_compiled_else_body"), read_key_from="user_key") df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n1, n2)) From 438f1da4e42fdabf755ee8d9e40b7e1e33b39b6a Mon Sep 17 00:00:00 2001 From: lucasvanmol Date: Mon, 20 Jan 2025 16:36:51 +0100 Subject: [PATCH 08/12] Fix tests --- src/cascade/runtime/python_runtime.py | 36 ++++++++----------- .../flink-runtime/test_collect_operator.py | 4 +-- .../flink-runtime/test_select_all.py | 4 +-- .../flink-runtime/test_two_entities.py | 4 +-- 4 files changed, 21 insertions(+), 27 deletions(-) diff --git a/src/cascade/runtime/python_runtime.py b/src/cascade/runtime/python_runtime.py index cf936f3..8743014 100644 --- a/src/cascade/runtime/python_runtime.py +++ b/src/cascade/runtime/python_runtime.py @@ -1,7 +1,8 @@ from logging import Filter import threading +from typing import Type from cascade.dataflow.operator import StatefulOperator, StatelessOperator -from cascade.dataflow.dataflow import CollectNode, Event, EventResult, InitClass, InvokeMethod, OpNode, SelectAllNode +from cascade.dataflow.dataflow import CollectNode, Event, EventResult, InitClass, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode from queue import Empty, Queue class PythonStatefulOperator(): @@ -11,17 +12,15 @@ def __init__(self, operator: StatefulOperator): def process(self, event: Event): assert(isinstance(event.target, OpNode)) - assert(isinstance(event.target.operator, StatefulOperator)) - assert(event.target.operator.entity == self.operator.entity) - key_stack = event.key_stack - key = key_stack[-1] + assert(event.target.entity == self.operator.entity) + + key = event.variable_map[event.target.read_key_from] print(f"PythonStatefulOperator: {event}") if isinstance(event.target.method_type, InitClass): result = self.operator.handle_init_class(*event.variable_map.values()) self.states[key] = result - key_stack.pop() elif isinstance(event.target.method_type, InvokeMethod): state = self.states[key] @@ -29,7 +28,6 @@ def process(self, event: Event): event.target.method_type, variable_map=event.variable_map, state=state, - key_stack=key_stack ) self.states[key] = state @@ -39,7 +37,7 @@ def process(self, event: Event): if event.target.assign_result_to is not None: event.variable_map[event.target.assign_result_to] = result - new_events = event.propogate(key_stack, result) + new_events = event.propogate(result) if isinstance(new_events, EventResult): yield new_events else: @@ -50,17 +48,14 @@ def __init__(self, operator: StatelessOperator): self.operator = operator def process(self, event: Event): - assert(isinstance(event.target, OpNode)) - assert(isinstance(event.target.operator, StatelessOperator)) + assert(isinstance(event.target, StatelessOpNode)) - key_stack = event.key_stack if isinstance(event.target.method_type, InvokeMethod): result = self.operator.handle_invoke_method( event.target.method_type, variable_map=event.variable_map, - key_stack=key_stack ) else: raise Exception(f"A StatelessOperator cannot compute event type: {event.target.method_type}") @@ -68,7 +63,7 @@ def process(self, event: Event): if event.target.assign_result_to is not None: event.variable_map[event.target.assign_result_to] = result - new_events = event.propogate(key_stack, result) + new_events = event.propogate(result) if isinstance(new_events, EventResult): yield new_events else: @@ -81,8 +76,8 @@ def __init__(self): self.events = Queue() self.results = Queue() self.running = False - self.statefuloperators: dict[StatefulOperator, PythonStatefulOperator] = {} - self.statelessoperators: dict[StatelessOperator, PythonStatelessOperator] = {} + self.statefuloperators: dict[Type, PythonStatefulOperator] = {} + self.statelessoperators: dict[str, PythonStatelessOperator] = {} def init(self): pass @@ -91,10 +86,9 @@ def _consume_events(self): self.running = True def consume_event(event: Event): if isinstance(event.target, OpNode): - if isinstance(event.target.operator, StatefulOperator): - yield from self.statefuloperators[event.target.operator].process(event) - elif isinstance(event.target.operator, StatelessOperator): - yield from self.statelessoperators[event.target.operator].process(event) + yield from self.statefuloperators[event.target.entity].process(event) + elif isinstance(event.target, StatelessOpNode): + yield from self.statelessoperators[event.target.operator.dataflow.name].process(event) elif isinstance(event.target, SelectAllNode): raise NotImplementedError() @@ -121,11 +115,11 @@ def consume_event(event: Event): def add_operator(self, op: StatefulOperator): """Add a `StatefulOperator` to the datastream.""" - self.statefuloperators[op] = PythonStatefulOperator(op) + self.statefuloperators[op.entity] = PythonStatefulOperator(op) def add_stateless_operator(self, op: StatelessOperator): """Add a `StatelessOperator` to the datastream.""" - self.statelessoperators[op] = PythonStatelessOperator(op) + self.statelessoperators[op.dataflow.name] = PythonStatelessOperator(op) def send(self, event: Event, flush=None): self.events.put(event) diff --git a/tests/integration/flink-runtime/test_collect_operator.py b/tests/integration/flink-runtime/test_collect_operator.py index 574c739..d14418f 100644 --- a/tests/integration/flink-runtime/test_collect_operator.py +++ b/tests/integration/flink-runtime/test_collect_operator.py @@ -10,8 +10,8 @@ def test_merge_operator(): runtime = FlinkRuntime("test_collect_operator") runtime.init() - runtime.add_operator(FlinkOperator(item_op)) - runtime.add_operator(FlinkOperator(user_op)) + runtime.add_operator(item_op) + runtime.add_operator(user_op) # Create a User object diff --git a/tests/integration/flink-runtime/test_select_all.py b/tests/integration/flink-runtime/test_select_all.py index 35c3265..602858d 100644 --- a/tests/integration/flink-runtime/test_select_all.py +++ b/tests/integration/flink-runtime/test_select_all.py @@ -99,8 +99,8 @@ def get_nearby_body_compiled_1(variable_map: dict[str, Any]) -> str: def test_nearby_hotels(): runtime = FlinkRuntime("test_nearby_hotels") runtime.init() - runtime.add_operator(FlinkOperator(hotel_op)) - runtime.add_stateless_operator(FlinkStatelessOperator(get_nearby_op)) + runtime.add_operator(hotel_op) + runtime.add_stateless_operator(get_nearby_op) # Create Hotels hotels = [] diff --git a/tests/integration/flink-runtime/test_two_entities.py b/tests/integration/flink-runtime/test_two_entities.py index 54309fa..3d89bd2 100644 --- a/tests/integration/flink-runtime/test_two_entities.py +++ b/tests/integration/flink-runtime/test_two_entities.py @@ -10,8 +10,8 @@ def test_two_entities(): runtime = FlinkRuntime("test_two_entities") runtime.init() - runtime.add_operator(FlinkOperator(item_op)) - runtime.add_operator(FlinkOperator(user_op)) + runtime.add_operator(item_op) + runtime.add_operator(user_op) # Create a User object foo_user = User("foo", 100) From 610104c3ee6719b51117b3adb228818c25e462de Mon Sep 17 00:00:00 2001 From: lucasvanmol Date: Tue, 21 Jan 2025 12:00:05 +0100 Subject: [PATCH 09/12] Fix dataflow test --- src/cascade/dataflow/test_dataflow.py | 68 +++++++++++++++------------ 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/src/cascade/dataflow/test_dataflow.py b/src/cascade/dataflow/test_dataflow.py index 1e29aad..a5b42af 100644 --- a/src/cascade/dataflow/test_dataflow.py +++ b/src/cascade/dataflow/test_dataflow.py @@ -12,14 +12,12 @@ def buy_item(self, item: 'DummyItem') -> bool: self.balance -= item_price return self.balance >= 0 -def buy_item_0_compiled(variable_map: dict[str, Any], state: DummyUser, key_stack: list[str]) -> dict[str, Any]: - key_stack.append(variable_map["item_key"]) +def buy_item_0_compiled(variable_map: dict[str, Any], state: DummyUser): return -def buy_item_1_compiled(variable_map: dict[str, Any], state: DummyUser, key_stack: list[str]) -> dict[str, Any]: - key_stack.pop() +def buy_item_1_compiled(variable_map: dict[str, Any], state: DummyUser): state.balance -= variable_map["item_price"] - return {"user_postive_balance": state.balance >= 0} + return state.balance >= 0 class DummyItem: def __init__(self, key: str, price: int): @@ -29,10 +27,8 @@ def __init__(self, key: str, price: int): def get_price(self) -> int: return self.price -def get_price_compiled(variable_map: dict[str, Any], state: DummyItem, key_stack: list[str]) -> dict[str, Any]: - key_stack.pop() # final function - variable_map["item_price"] = state.price - # return {"item_price": state.price} +def get_price_compiled(variable_map: dict[str, Any], state: DummyItem): + return state.price ################## TESTS ####################### @@ -46,53 +42,60 @@ def get_price_compiled(variable_map: dict[str, Any], state: DummyItem, key_stack def test_simple_df_propogation(): df = DataFlow("user.buy_item") - n1 = OpNode(DummyUser, InvokeMethod("buy_item_0_compiled")) - n2 = OpNode(DummyItem, InvokeMethod("get_price")) - n3 = OpNode(DummyUser, InvokeMethod("buy_item_1")) + n1 = OpNode(DummyUser, InvokeMethod("buy_item_0_compiled"), read_key_from="user_key") + n2 = OpNode(DummyItem, InvokeMethod("get_price"), read_key_from="item_key", assign_result_to="item_price") + n3 = OpNode(DummyUser, InvokeMethod("buy_item_1"), read_key_from="user_key") df.add_edge(Edge(n1, n2)) df.add_edge(Edge(n2, n3)) user.buy_item(item) - event = Event(n1, ["user"], {"item_key":"fork"}, df) + event = Event(n1, {"user_key": "user", "item_key":"fork"}, df) # Manually propogate - item_key = buy_item_0_compiled(event.variable_map, state=user, key_stack=event.key_stack) - next_event = event.propogate(event.key_stack, item_key) + item_key = buy_item_0_compiled(event.variable_map, state=user) + next_event = event.propogate(event, item_key) + assert isinstance(next_event, list) assert len(next_event) == 1 assert next_event[0].target == n2 - assert next_event[0].key_stack == ["user", "fork"] event = next_event[0] - item_price = get_price_compiled(event.variable_map, state=item, key_stack=event.key_stack) - next_event = event.propogate(event.key_stack, item_price) + # manually add the price to the variable map + item_price = get_price_compiled(event.variable_map, state=item) + assert n2.assign_result_to + event.variable_map[n2.assign_result_to] = item_price + next_event = event.propogate(item_price) + + assert isinstance(next_event, list) assert len(next_event) == 1 assert next_event[0].target == n3 event = next_event[0] - positive_balance = buy_item_1_compiled(event.variable_map, state=user, key_stack=event.key_stack) - next_event = event.propogate(event.key_stack, None) + positive_balance = buy_item_1_compiled(event.variable_map, state=user) + next_event = event.propogate(None) assert isinstance(next_event, EventResult) def test_merge_df_propogation(): df = DataFlow("user.buy_2_items") - n0 = OpNode(DummyUser, InvokeMethod("buy_2_items_0")) + n0 = OpNode(DummyUser, InvokeMethod("buy_2_items_0"), read_key_from="user_key") n3 = CollectNode(assign_result_to="item_prices", read_results_from="item_price") n1 = OpNode( DummyItem, InvokeMethod("get_price"), assign_result_to="item_price", - collect_target=CollectTarget(n3, 2, 0) + collect_target=CollectTarget(n3, 2, 0), + read_key_from="item_1_key" ) n2 = OpNode( DummyItem, InvokeMethod("get_price"), assign_result_to="item_price", - collect_target=CollectTarget(n3, 2, 1) + collect_target=CollectTarget(n3, 2, 1), + read_key_from="item_2_key" ) - n4 = OpNode(DummyUser, InvokeMethod("buy_2_items_1")) + n4 = OpNode(DummyUser, InvokeMethod("buy_2_items_1"), read_key_from="user_key") df.add_edge(Edge(n0, n1)) df.add_edge(Edge(n0, n2)) df.add_edge(Edge(n1, n3)) @@ -100,25 +103,30 @@ def test_merge_df_propogation(): df.add_edge(Edge(n3, n4)) # User with key "foo" buys items with keys "fork" and "spoon" - event = Event(n0, ["foo"], {"item_1_key": "fork", "item_2_key": "spoon"}, df) + event = Event(n0, {"user_key": "foo", "item_1_key": "fork", "item_2_key": "spoon"}, df) # Propogate the event (without actually doing any calculation) # Normally, the key_stack should've been updated by the runtime here: - key_stack = ["foo", ["fork", "spoon"]] - next_event = event.propogate(key_stack, None) + next_event = event.propogate(None) + assert isinstance(next_event, list) assert len(next_event) == 2 assert next_event[0].target == n1 assert next_event[1].target == n2 event1, event2 = next_event - next_event = event1.propogate(event1.key_stack, None) + next_event = event1.propogate(None) + + assert isinstance(next_event, list) assert len(next_event) == 1 assert next_event[0].target == n3 - next_event = event2.propogate(event2.key_stack, None) + next_event = event2.propogate(None) + + assert isinstance(next_event, list) assert len(next_event) == 1 assert next_event[0].target == n3 - final_event = next_event[0].propogate(next_event[0].key_stack, None) + final_event = next_event[0].propogate(None) + assert isinstance(final_event, list) assert final_event[0].target == n4 From d465bf722ec7677df6d67ea70340f72902035339 Mon Sep 17 00:00:00 2001 From: lucasvanmol Date: Thu, 23 Jan 2025 16:41:21 +0100 Subject: [PATCH 10/12] Add dead node optimization --- deathstar/test_demo.py | 2 +- src/cascade/dataflow/dataflow.py | 78 ++++++++++++- src/cascade/dataflow/optimization/__init__.py | 0 .../dataflow/optimization/dead_node_elim.py | 44 ++++++++ .../optimization/test_dead_node_elim.py | 103 ++++++++++++++++++ 5 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 src/cascade/dataflow/optimization/__init__.py create mode 100644 src/cascade/dataflow/optimization/dead_node_elim.py create mode 100644 src/cascade/dataflow/optimization/test_dead_node_elim.py diff --git a/deathstar/test_demo.py b/deathstar/test_demo.py index a3afd4a..751c9ed 100644 --- a/deathstar/test_demo.py +++ b/deathstar/test_demo.py @@ -59,7 +59,7 @@ def test_deathstar_demo_python(): print("Populating, press enter to go to the next step when done") ds.populate() - time.sleep(2) + time.sleep(0.1) client = PythonClientSync(ds.runtime) print("testing user login") diff --git a/src/cascade/dataflow/dataflow.py b/src/cascade/dataflow/dataflow.py index 32c79a3..9f78fe2 100644 --- a/src/cascade/dataflow/dataflow.py +++ b/src/cascade/dataflow/dataflow.py @@ -21,6 +21,9 @@ class InvokeMethod: """A method invocation of the underlying method indentifier.""" method_name: str + def __repr__(self) -> str: + return f"{self.__class__.__name__}('{self.method_name}')" + @dataclass class Filter: """Filter by this function""" @@ -105,6 +108,9 @@ def propogate_opnode(node: Union['OpNode', 'StatelessOpNode'], event: 'Event', t collect_target=ct) for target, ct in zip(targets, collect_targets)] + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.entity.__name__}, {self.method_type})" @dataclass class StatelessOpNode(Node): @@ -237,13 +243,81 @@ def add_edge(self, edge: Edge): """Add an edge to the Dataflow graph. Nodes that don't exist will be added to the graph automatically.""" self.add_node(edge.from_node) self.add_node(edge.to_node) - self.adjacency_list[edge.from_node.id].append(edge.to_node.id) - edge.from_node.outgoing_edges.append(edge) + if edge.to_node.id not in self.adjacency_list[edge.from_node.id]: + self.adjacency_list[edge.from_node.id].append(edge.to_node.id) + edge.from_node.outgoing_edges.append(edge) + + + def remove_edge(self, from_node: Node, to_node: Node): + """Remove an edge from the Dataflow graph.""" + if from_node.id in self.adjacency_list and to_node.id in self.adjacency_list[from_node.id]: + # Remove from adjacency list + self.adjacency_list[from_node.id].remove(to_node.id) + # Remove from outgoing_edges + from_node.outgoing_edges = [ + edge for edge in from_node.outgoing_edges if edge.to_node.id != to_node.id + ] + + def remove_node(self, node: Node): + """Remove a node from the DataFlow graph and reconnect its parents to its children.""" + if node.id not in self.nodes: + return # Node doesn't exist in the graph + + + if isinstance(node, OpNode) or isinstance(node, StatelessOpNode): + assert not node.is_conditional, "there's no clear way to remove a conditional node" + assert not node.assign_result_to, "can't delete node whose result is used" + assert not node.collect_target, "can't delete node which has a collect_target" + + # Find parents (nodes that have edges pointing to this node) + parents = [parent_id for parent_id, children in self.adjacency_list.items() if node.id in children] + + # Find children (nodes that this node points to) + children = self.adjacency_list[node.id] + + # Connect each parent to each child + for parent_id in parents: + parent_node = self.nodes[parent_id] + for child_id in children: + child_node = self.nodes[child_id] + new_edge = Edge(parent_node, child_node) + self.add_edge(new_edge) + + # Remove edges from parents to the node + for parent_id in parents: + parent_node = self.nodes[parent_id] + self.remove_edge(parent_node, node) + + # Remove outgoing edges from the node + for child_id in children: + child_node = self.nodes[child_id] + self.remove_edge(node, child_node) + + # Remove the node from the adjacency list and nodes dictionary + del self.adjacency_list[node.id] + del self.nodes[node.id] + def get_neighbors(self, node: Node) -> List[Node]: """Get the outgoing neighbors of this `Node`""" return [self.nodes[id] for id in self.adjacency_list.get(node.id, [])] + def to_dot(self) -> str: + """Output the DataFlow graph in DOT (Graphviz) format.""" + lines = [f"digraph {self.name} {{"] + + # Add nodes + for node in self.nodes.values(): + lines.append(f' {node.id} [label="{node}"];') + + # Add edges + for from_id, to_ids in self.adjacency_list.items(): + for to_id in to_ids: + lines.append(f" {from_id} -> {to_id};") + + lines.append("}") + return "\n".join(lines) + class Result(ABC): pass diff --git a/src/cascade/dataflow/optimization/__init__.py b/src/cascade/dataflow/optimization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cascade/dataflow/optimization/dead_node_elim.py b/src/cascade/dataflow/optimization/dead_node_elim.py new file mode 100644 index 0000000..a62ac37 --- /dev/null +++ b/src/cascade/dataflow/optimization/dead_node_elim.py @@ -0,0 +1,44 @@ +from cascade.dataflow.dataflow import DataFlow, InvokeMethod +from cascade.dataflow.operator import StatefulOperator, StatelessOperator +import inspect + +def is_no_op(func): + # Get the source code of the function + source = inspect.getsource(func).strip() + + # Extract the function body (skip the signature) + lines = source.splitlines() + if len(lines) < 2: + # A function with only a signature can't have a body + return False + + # Check the body of the function + body = lines[1].strip() + # A valid no-op function body is either 'pass' or 'return' + return body in ("pass", "return") + + +def dead_node_elimination(stateful_ops: list[StatefulOperator], stateless_ops: list[StatelessOperator]): + # Find dead functions + dead_func_names = set() + for op in stateful_ops: + for method in op._methods.values(): + if is_no_op(method): + dead_func_names.add(method.__qualname__) + + # Remove them from dataflows + for op in stateful_ops: + for dataflow in op.dataflows.values(): + to_remove = [] + for node in dataflow.nodes.values(): + if hasattr(node, "method_type") and isinstance(node.method_type, InvokeMethod): + im: InvokeMethod = node.method_type + if im.method_name in dead_func_names: + to_remove.append(node) + + for node in to_remove: + print(node) + dataflow.remove_node(node) + print(dataflow.to_dot()) + + diff --git a/src/cascade/dataflow/optimization/test_dead_node_elim.py b/src/cascade/dataflow/optimization/test_dead_node_elim.py new file mode 100644 index 0000000..94b30af --- /dev/null +++ b/src/cascade/dataflow/optimization/test_dead_node_elim.py @@ -0,0 +1,103 @@ +from typing import Any + +from cascade.dataflow.dataflow import DataFlow, Edge, InvokeMethod, OpNode +from cascade.dataflow.operator import StatefulOperator +from cascade.dataflow.optimization.dead_node_elim import dead_node_elimination +from cascade.dataflow.optimization.dead_node_elim import is_no_op + +class User: + pass + +class Hotel: + pass + +class Flight: + pass + +def order_compiled_entry_0(variable_map: dict[str, Any], state: User) -> Any: + pass + +def order_compiled_entry_1(variable_map: dict[str, Any], state: User) -> Any: + pass + +def order_compiled_if_cond(variable_map: dict[str, Any], state: User) -> Any: + return variable_map["hotel_reserve"] and variable_map["flight_reserve"] + +def order_compiled_if_body(variable_map: dict[str, Any], state: User) -> Any: + return True + +def order_compiled_else_body(variable_map: dict[str, Any], state: User) -> Any: + return False + +user_op = StatefulOperator( + User, + { + "order_compiled_entry_0": order_compiled_entry_0, + "order_compiled_entry_1": order_compiled_entry_1, + "order_compiled_if_cond": order_compiled_if_cond, + "order_compiled_if_body": order_compiled_if_body, + "order_compiled_else_body": order_compiled_else_body + }, + {} +) + +# For now, the dataflow will be serial instead of parallel (calling hotel, then +# flight). Future optimizations could try to automatically parallelize this. +# There could definetly be some slight changes to this dataflow depending on +# other optimizations aswell. (A naive system could have an empty first entry +# before the first entity call). +def user_order_df(): + df = DataFlow("user_order") + n0 = OpNode(User, InvokeMethod("order_compiled_entry_0"), read_key_from="user_key") + n1 = OpNode(Hotel, InvokeMethod("reserve"), assign_result_to="hotel_reserve", read_key_from="hotel_key") + n2 = OpNode(User, InvokeMethod("order_compiled_entry_1"), read_key_from="user_key") + n3 = OpNode(Flight, InvokeMethod("reserve"), assign_result_to="flight_reserve", read_key_from="flight_key") + n4 = OpNode(User, InvokeMethod("order_compiled_if_cond"), is_conditional=True, read_key_from="user_key") + n5 = OpNode(User, InvokeMethod("order_compiled_if_body"), read_key_from="user_key") + n6 = OpNode(User, InvokeMethod("order_compiled_else_body"), read_key_from="user_key") + + df.add_edge(Edge(n0, n1)) + df.add_edge(Edge(n1, n2)) + df.add_edge(Edge(n2, n3)) + df.add_edge(Edge(n3, n4)) + df.add_edge(Edge(n4, n5, if_conditional=True)) + df.add_edge(Edge(n4, n6, if_conditional=False)) + + df.entry = n0 + return df + +df = user_order_df() +user_op.dataflows[df.name] = df + +def test_dead_node_elim(): + print(user_op.dataflows[df.name].to_dot()) + + dead_node_elimination([user_op], []) + + print(user_op.dataflows[df.name].to_dot()) + + + +### TEST NO OP DETECTION ### + +def c1(variable_map: dict[str, Any]): + return (variable_map["dist"], variable_map["hotel_key"]) + +def c2(variable_map: dict[str, Any]): + return None + +def c3(variable_map: dict[str, Any]): + return + +def c4(variable_map: dict[str, Any]): + pass + +def c5(variable_map: dict[str, Any]): + return True + +def test_no_op_detect(): + assert not is_no_op(c1) + assert not is_no_op(c2) + assert is_no_op(c3) + assert is_no_op(c4) + assert not is_no_op(c5) From 35bbf83fa350c530e09b44ba38bfdcd9b48a56e6 Mon Sep 17 00:00:00 2001 From: Lucas Van Mol Date: Tue, 4 Feb 2025 14:03:43 +0100 Subject: [PATCH 11/12] initial results --- deathstar/demo.py | 102 +- deathstar/entities/user.py | 43 +- display_results.ipynb | 2914 +++++++++++++++++ login_10mps.png | Bin 0 -> 61636 bytes reserve_10mps.png | Bin 0 -> 36356 bytes reserve_10mps_parallel.png | Bin 0 -> 34048 bytes reserve_1mps.png | Bin 0 -> 14193 bytes reserve_1mps_parallel.png | Bin 0 -> 28572 bytes src/cascade/dataflow/dataflow.py | 29 +- .../dataflow/optimization/parallelization.py | 191 ++ src/cascade/runtime/flink_runtime.py | 40 +- 11 files changed, 3252 insertions(+), 67 deletions(-) create mode 100644 display_results.ipynb create mode 100644 login_10mps.png create mode 100644 reserve_10mps.png create mode 100644 reserve_10mps_parallel.png create mode 100644 reserve_1mps.png create mode 100644 reserve_1mps_parallel.png create mode 100644 src/cascade/dataflow/optimization/parallelization.py diff --git a/deathstar/demo.py b/deathstar/demo.py index 37cd9d9..5c7a6a1 100644 --- a/deathstar/demo.py +++ b/deathstar/demo.py @@ -9,13 +9,14 @@ # import cascade sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) -from cascade.dataflow.dataflow import Event, InitClass, InvokeMethod, OpNode +from cascade.dataflow.dataflow import Event, EventResult, InitClass, InvokeMethod, OpNode from cascade.runtime.flink_runtime import FlinkClientSync, FlinkOperator, FlinkRuntime, FlinkStatelessOperator from deathstar.entities.flight import Flight, flight_op from deathstar.entities.hotel import Geo, Hotel, Rate, hotel_op from deathstar.entities.recommendation import Recommendation, recommend_op from deathstar.entities.search import Search, search_op from deathstar.entities.user import User, user_op +import pandas as pd class DeathstarDemo(): @@ -219,8 +220,6 @@ def reserve(): hotel_id = random.randint(0, 99) flight_id = random.randint(0, 99) - # user = User("user1", "pass") - # user.order(flight, hotel) user_id = "Cornell_" + str(random.randint(0, 500)) return Event( @@ -250,43 +249,52 @@ def deathstar_workload_generator(): yield reserve() c += 1 +def reserve_workload_generator(): + while True: + yield reserve() + +def user_login_workload_generator(): + while True: + yield user_login() + threads = 1 -messages_per_second = 10 -sleeps_per_second = 10 +messages_per_burst = 1 +sleeps_per_burst = 1 sleep_time = 0.0085 -seconds = 10 +seconds_per_burst = 1 +bursts = 100 def benchmark_runner(proc_num) -> dict[int, dict]: print(f'Generator: {proc_num} starting') client = FlinkClientSync("deathstar", "ds-out", "localhost:9092", True) - deathstar_generator = deathstar_workload_generator() - # futures: dict[int, dict] = {} + deathstar_generator = reserve_workload_generator() start = timer() - for _ in range(seconds): + + for _ in range(bursts): sec_start = timer() - for i in range(messages_per_second): - if i % (messages_per_second // sleeps_per_second) == 0: + + # send burst of messages + for i in range(messages_per_burst): + + # sleep sometimes between messages + if i % (messages_per_burst // sleeps_per_burst) == 0: time.sleep(sleep_time) event = next(deathstar_generator) - # func_name = event.dataflow.name if event.dataflow is not None else "login" # only login has no dataflow - # key = event.key_stack[0] - # params = event.variable_map client.send(event) - # futures[event._id] = {"event": f'{func_name} {key}->{params}'} client.flush() sec_end = timer() + + # wait out the second lps = sec_end - sec_start - if lps < 1: + if lps < seconds_per_burst: time.sleep(1 - lps) sec_end2 = timer() - print(f'Latency per second: {sec_end2 - sec_start}') + print(f'Latency per burst: {sec_end2 - sec_start} ({seconds_per_burst})') + end = timer() - print(f'Average latency per second: {(end - start) / seconds}') - # styx.close() - # for key, metadata in styx.delivery_timestamps.items(): - # timestamp_futures[key]["timestamp"] = metadata + print(f'Average latency per burst: {(end - start) / bursts} ({seconds_per_burst})') done = False while not done: @@ -302,36 +310,36 @@ def benchmark_runner(proc_num) -> dict[int, dict]: return futures -def write_dict_to_csv(futures_dict, filename): +def write_dict_to_pkl(futures_dict, filename): """ - Writes a dictionary of event data to a CSV file. + Writes a dictionary of event data to a pickle file. Args: futures_dict (dict): A dictionary where each key is an event ID and the value is another dict. - filename (str): The name of the CSV file to write to. + filename (str): The name of the pickle file to write to. """ - # Define the column headers - headers = ["event_id", "sent", "sent_t", "ret", "ret_t", "latency"] - - # Open the file for writing - with open(filename, mode='w', newline='', encoding='utf-8') as csvfile: - writer = csv.DictWriter(csvfile, fieldnames=headers) - - # Write the headers - writer.writeheader() - - # Write the data rows - for event_id, event_data in futures_dict.items(): - # Prepare a row where the 'event_id' is the first column - row = { - "event_id": event_id, - "sent": event_data.get("sent"), - "sent_t": event_data.get("sent_t"), - "ret": event_data.get("ret"), - "ret_t": event_data.get("ret_t"), - "latency": event_data["ret_t"][1] - event_data["sent_t"][1] - } - writer.writerow(row) + + # Prepare the data for the DataFrame + data = [] + for event_id, event_data in futures_dict.items(): + ret: EventResult = event_data.get("ret") + row = { + "event_id": event_id, + "sent": str(event_data.get("sent")), + "sent_t": event_data.get("sent_t"), + "ret": str(event_data.get("ret")), + "ret_t": event_data.get("ret_t"), + "roundtrip": ret.metadata["roundtrip"] if ret else None, + "flink_time": ret.metadata["flink_time"] if ret else None, + "deser_times": ret.metadata["deser_times"] if ret else None, + "loops": ret.metadata["loops"] if ret else None, + "latency": event_data["ret_t"][1] - event_data["sent_t"][1] if ret else None + } + data.append(row) + + # Create a DataFrame and save it as a pickle file + df = pd.DataFrame(data) + df.to_pickle(filename) def main(): ds = DeathstarDemo() @@ -361,7 +369,7 @@ def main(): print(result) r += 1 print(f"{r}/{t} results recieved.") - write_dict_to_csv(results, "test2.csv") + write_dict_to_pkl(results, "test2.pkl") if __name__ == "__main__": main() \ No newline at end of file diff --git a/deathstar/entities/user.py b/deathstar/entities/user.py index 95b135f..fb620d7 100644 --- a/deathstar/entities/user.py +++ b/deathstar/entities/user.py @@ -1,5 +1,5 @@ from typing import Any -from cascade.dataflow.dataflow import DataFlow, Edge, InvokeMethod, OpNode +from cascade.dataflow.dataflow import CollectNode, CollectTarget, DataFlow, Edge, InvokeMethod, OpNode from cascade.dataflow.operator import StatefulOperator from deathstar.entities.flight import Flight, flight_op from deathstar.entities.hotel import Hotel, hotel_op @@ -33,6 +33,9 @@ def order_compiled_entry_1(variable_map: dict[str, Any], state: User) -> Any: def order_compiled_if_cond(variable_map: dict[str, Any], state: User) -> Any: return variable_map["hotel_reserve"] and variable_map["flight_reserve"] +def order_compiled_if_cond_parallel(variable_map: dict[str, Any], state: User) -> Any: + return variable_map["reserves"][0] and variable_map["reserves"][1] + def order_compiled_if_body(variable_map: dict[str, Any], state: User) -> Any: return True @@ -45,7 +48,8 @@ def order_compiled_else_body(variable_map: dict[str, Any], state: User) -> Any: "login": check_compiled, "order_compiled_entry_0": order_compiled_entry_0, "order_compiled_entry_1": order_compiled_entry_1, - "order_compiled_if_cond": order_compiled_if_cond, + # "order_compiled_if_cond": order_compiled_if_cond, + "order_compiled_if_cond": order_compiled_if_cond_parallel, "order_compiled_if_body": order_compiled_if_body, "order_compiled_else_body": order_compiled_else_body }, @@ -55,19 +59,42 @@ def order_compiled_else_body(variable_map: dict[str, Any], state: User) -> Any: # For now, the dataflow will be serial instead of parallel. Future optimizations # will try to automatically parallelize this. # There is also no user entry (this could also be an optimization) +# df = DataFlow("user_order") +# n0 = OpNode(User, InvokeMethod("order_compiled_entry_0"), read_key_from="user_key") +# n1 = OpNode(Hotel, InvokeMethod("reserve"), assign_result_to="hotel_reserve", read_key_from="hotel_key") +# n2 = OpNode(User, InvokeMethod("order_compiled_entry_1"), read_key_from="user_key") +# n3 = OpNode(Flight, InvokeMethod("reserve"), assign_result_to="flight_reserve", read_key_from="flight_key") +# n4 = OpNode(User, InvokeMethod("order_compiled_if_cond"), is_conditional=True, read_key_from="user_key") +# n5 = OpNode(User, InvokeMethod("order_compiled_if_body"), read_key_from="user_key") +# n6 = OpNode(User, InvokeMethod("order_compiled_else_body"), read_key_from="user_key") + +# df.add_edge(Edge(n0, n1)) +# df.add_edge(Edge(n1, n2)) +# df.add_edge(Edge(n2, n3)) +# df.add_edge(Edge(n3, n4)) +# df.add_edge(Edge(n4, n5, if_conditional=True)) +# df.add_edge(Edge(n4, n6, if_conditional=False)) + +# df.entry = n0 + +# user_op.dataflows["order"] = df + + +# PARALEL DATAFLOW df = DataFlow("user_order") n0 = OpNode(User, InvokeMethod("order_compiled_entry_0"), read_key_from="user_key") -n1 = OpNode(Hotel, InvokeMethod("reserve"), assign_result_to="hotel_reserve", read_key_from="hotel_key") -n2 = OpNode(User, InvokeMethod("order_compiled_entry_1"), read_key_from="user_key") -n3 = OpNode(Flight, InvokeMethod("reserve"), assign_result_to="flight_reserve", read_key_from="flight_key") +ct = CollectNode(assign_result_to="reserves", read_results_from="reserve") +n1 = OpNode(Hotel, InvokeMethod("reserve"), assign_result_to="reserve", read_key_from="hotel_key", collect_target=CollectTarget(ct, 2, 0)) +n3 = OpNode(Flight, InvokeMethod("reserve"), assign_result_to="reserve", read_key_from="flight_key", collect_target=CollectTarget(ct, 2, 1)) n4 = OpNode(User, InvokeMethod("order_compiled_if_cond"), is_conditional=True, read_key_from="user_key") n5 = OpNode(User, InvokeMethod("order_compiled_if_body"), read_key_from="user_key") n6 = OpNode(User, InvokeMethod("order_compiled_else_body"), read_key_from="user_key") df.add_edge(Edge(n0, n1)) -df.add_edge(Edge(n1, n2)) -df.add_edge(Edge(n2, n3)) -df.add_edge(Edge(n3, n4)) +df.add_edge(Edge(n0, n3)) +df.add_edge(Edge(n1, ct)) +df.add_edge(Edge(n3, ct)) +df.add_edge(Edge(ct, n4)) df.add_edge(Edge(n4, n5, if_conditional=True)) df.add_edge(Edge(n4, n6, if_conditional=False)) diff --git a/display_results.ipynb b/display_results.ipynb new file mode 100644 index 0000000..fbe5cf4 --- /dev/null +++ b/display_results.ipynb @@ -0,0 +1,2914 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 77, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " event_id sent \\\n", + "0 701 Event(target=OpNode(User, InvokeMethod('order_... \n", + "1 702 Event(target=OpNode(User, InvokeMethod('order_... \n", + "2 703 Event(target=OpNode(User, InvokeMethod('order_... \n", + "3 704 Event(target=OpNode(User, InvokeMethod('order_... \n", + "4 705 Event(target=OpNode(User, InvokeMethod('order_... \n", + "\n", + " sent_t ret \\\n", + "0 (2, 1738342392523) EventResult(event_id=701, result=True, metadat... \n", + "1 (2, 1738342392523) EventResult(event_id=702, result=True, metadat... \n", + "2 (2, 1738342392523) EventResult(event_id=703, result=True, metadat... \n", + "3 (2, 1738342392523) EventResult(event_id=704, result=True, metadat... \n", + "4 (2, 1738342392523) EventResult(event_id=705, result=True, metadat... \n", + "\n", + " ret_t roundtrip flink_time \\\n", + "0 (2, 1738342395748) 3.218930 1.794561 \n", + "1 (2, 1738342394543) 2.013558 1.205442 \n", + "2 (2, 1738342395450) 2.919337 1.445957 \n", + "3 (2, 1738342395450) 2.919065 1.537150 \n", + "4 (2, 1738342395650) 3.012420 1.574120 \n", + "\n", + " deser_times loops latency \n", + "0 [0.00010704994201660156, 3.4332275390625e-05, ... 5 3225 \n", + "1 [3.7670135498046875e-05, 6.389617919921875e-05... 5 2020 \n", + "2 [2.956390380859375e-05, 3.0994415283203125e-05... 5 2927 \n", + "3 [2.6941299438476562e-05, 5.507469177246094e-05... 5 2927 \n", + "4 [2.6941299438476562e-05, 2.7418136596679688e-0... 5 3127 \n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# Read the CSV file\n", + "df = pd.read_pickle('test2.pkl')\n", + "\n", + "# Display the first few rows of the dataframe to understand its structure\n", + "print(df.head())" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + " event_id sent \\\n", + "0 701 Event(target=OpNode(User, InvokeMethod('order_... \n", + "1 702 Event(target=OpNode(User, InvokeMethod('order_... \n", + "2 703 Event(target=OpNode(User, InvokeMethod('order_... \n", + "3 704 Event(target=OpNode(User, InvokeMethod('order_... \n", + "4 705 Event(target=OpNode(User, InvokeMethod('order_... \n", + "\n", + " sent_t ret \\\n", + "0 (2, 1738342392523) EventResult(event_id=701, result=True, metadat... \n", + "1 (2, 1738342392523) EventResult(event_id=702, result=True, metadat... \n", + "2 (2, 1738342392523) EventResult(event_id=703, result=True, metadat... \n", + "3 (2, 1738342392523) EventResult(event_id=704, result=True, metadat... \n", + "4 (2, 1738342392523) EventResult(event_id=705, result=True, metadat... \n", + "\n", + " ret_t roundtrip flink_time \\\n", + "0 (2, 1738342395748) 3.218930 1.794561 \n", + "1 (2, 1738342394543) 2.013558 1.205442 \n", + "2 (2, 1738342395450) 2.919337 1.445957 \n", + "3 (2, 1738342395450) 2.919065 1.537150 \n", + "4 (2, 1738342395650) 3.012420 1.574120 \n", + "\n", + " deser_times loops latency \\\n", + "0 [0.00010704994201660156, 3.4332275390625e-05, ... 5 3225 \n", + "1 [3.7670135498046875e-05, 6.389617919921875e-05... 5 2020 \n", + "2 [2.956390380859375e-05, 3.0994415283203125e-05... 5 2927 \n", + "3 [2.6941299438476562e-05, 5.507469177246094e-05... 5 2927 \n", + "4 [2.6941299438476562e-05, 2.7418136596679688e-0... 5 3127 \n", + "\n", + " flink_time_ms deser_times_ms \n", + "0 1794.560671 0.334024 \n", + "1 1205.441713 0.262737 \n", + "2 1445.957422 0.235319 \n", + "3 1537.150145 0.244617 \n", + "4 1574.119806 0.237703 \n" + ] + }, + { + "data": { + "text/plain": [ + "Text(0, 0.5, 'latency (ms)')" + ] + }, + "execution_count": 78, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2025-01-31T17:55:19.053555\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.9.0, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "df['flink_time_ms'] = df['flink_time'] * 1000\n", + "df[\"deser_times_ms\"] = df[\"deser_times\"].apply(lambda x: sum(x)) * 1000\n", + "print(type(df['deser_times']))\n", + "print(df.head())\n", + "df.plot(x='event_id', y=['latency', 'flink_time_ms', 'deser_times_ms'], kind='line')\n", + "plt.ylim(bottom=0)\n", + "plt.ylabel('latency (ms)')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/login_10mps.png b/login_10mps.png new file mode 100644 index 0000000000000000000000000000000000000000..924958a209c412d86ec003ee8514cdd3052bf670 GIT binary patch literal 61636 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 VY3Nj5?gasV7N%C`DvjOm{txQDTrL0r literal 0 HcmV?d00001 diff --git a/reserve_10mps.png b/reserve_10mps.png new file mode 100644 index 0000000000000000000000000000000000000000..20fdacfbc3e84f49816690055b9c53379034f66f GIT binary patch literal 36356 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 Qf&mCTUHx3vIVCg!04XBsD*ylh literal 0 HcmV?d00001 diff --git a/reserve_10mps_parallel.png b/reserve_10mps_parallel.png new file mode 100644 index 0000000000000000000000000000000000000000..7d22c450d688d7f7f2bece2a7dfc365be690c552 GIT binary patch literal 34048 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 literal 0 HcmV?d00001 diff --git a/reserve_1mps.png b/reserve_1mps.png new file mode 100644 index 0000000000000000000000000000000000000000..85ab9a5e840c875f740faad3710aa35e4e6a7c1b GIT binary patch literal 14193 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 R;4?YUc|(hHWd_&o|1UU@@@fD8 literal 0 HcmV?d00001 diff --git a/reserve_1mps_parallel.png b/reserve_1mps_parallel.png new file mode 100644 index 0000000000000000000000000000000000000000..d111ec8adc5571ad410fccc5e5fcb2a1515d70b8 GIT binary patch literal 28572 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%0A**R^|Cj2zM?|tOZnVS}y&8 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 dict: + return { + "in_t": None, + "deser_times": [], + "flink_time": 0 + } + @dataclass class Event(): """An Event is an object that travels through the Dataflow graph.""" @@ -360,6 +371,9 @@ class Event(): """Tells each mergenode (key) how many events to merge on""" _id_counter: int = field(init=False, default=0, repr=False) + + metadata: dict = field(default_factory=metadata_dict) + """Event metadata containing, for example, timestamps for benchmarking""" def __post_init__(self): if self._id is None: @@ -371,12 +385,12 @@ def propogate(self, result, select_all_keys: Optional[list[str]]=None) -> Union[ """Propogate this event through the Dataflow.""" if self.dataflow is None: - return EventResult(self._id, result) + return EventResult(self._id, result, self.metadata) targets = self.dataflow.get_neighbors(self.target) if len(targets) == 0: - return EventResult(self._id, result) + return EventResult(self._id, result, self.metadata) else: current_node = self.target @@ -389,4 +403,5 @@ def propogate(self, result, select_all_keys: Optional[list[str]]=None) -> Union[ @dataclass class EventResult(): event_id: int - result: Any \ No newline at end of file + result: Any + metadata: dict \ No newline at end of file diff --git a/src/cascade/dataflow/optimization/parallelization.py b/src/cascade/dataflow/optimization/parallelization.py new file mode 100644 index 0000000..79e3ea4 --- /dev/null +++ b/src/cascade/dataflow/optimization/parallelization.py @@ -0,0 +1,191 @@ +""" +When is it safe to parallize nodes? + +-> When they don't affect each other +-> The simpelest way of doing it could be to run individual dataflows in parallel +(e.g. item.get_price() can run in parallel) +-> must convey that we assume no side-affects, so the actual order of execution +does not matter. could go deeper and give a spec. +-> some instructions from the same dataflow could also be completed in parallel? +maybe? like ILP. but might need to think of more contrived examples/do more +advanced program analyis. + +From Control Flow to Dataflow +3. Parallelizing Memory Operations +- operations on different memory locatiosn need not be sequentialized +- circulate a set of access tokens for each variable (=split function?) + - assume that every variable denotes a unique memory location (no aliasing) + +We have to be careful about certain types of parallelization. Consider the example: + +``` +# Calculate the average item price in basket: List[Item] +n = 0 +p = 0 +for item in basket: + n += 1 + p += item.price() +return p / n +``` + +In this example we would want to parallelize the calls to item.price(). +But we have to make sure the calls to `n += 1` remains bounded to the number of +items, even though there is no explicit data dependency. + + +---- + + +There is another type of optimization we could look at. +Suppose the following: + +``` +n = self.basket_size + +prices = [item.price() for item in self.basket] +total_price = sum(prices) + +return total_price / n +``` + +In this case, the variable n is not needed in the list comprehension - unoptimized +versions would generate an extra function instead of having the line be re-ordered +into the bottom function. Instead, analyis of the variables each function needs +access to would be a way to optimize these parts! + +--> Ask Soham about this! + +from "From control flow to dataflow" + +Consider the portion of control-flow graph between a node N and its *immediate +postdominator* P. Every control-flow path starting at N ultimately ends up at P. +Suppose that there is no reference to a variable x in any node on any path between +N and P. It is clear that an access token for x that enters N may bypass this +region of the graph altogether and go directly to P. + + +---- + +"Dataflow-Based Parallelization of Control-Flow Algorithms" + +loop invariant hoisting + +``` +i = 0 +while i < n: + x = y + z + a[i] = 6 * i + x * x + i += 1 +``` + +can be transformed in + +``` +i = 0 +if i < n: + x = y + z # loop invariant 1 + t1 = x * x # loop invariant 2 + do { # do while loop needed in case the conditional has side effects + a[i] = 6 * i + t1 + i += 1 + } while i < n +``` + +this is achieved using reaching definitions analysis. In the paper: +"It is a common optimization to pull those parts of a loop body +that depend on only static datasets outside of the loop, and thus +execute these parts only once [7 , 13 , 15 , 32 ]. However, launching +new dataflow jobs for every iteration step prevents this optimiza- +tion in the case of such binary operators where only one input is +static. For example, if a static dataset is used as the build-side of +a hash join, then the system should not rebuild the hash table at +every iteration step. Labyrinth operators can keep such a hash +table in their internal states between iteration steps. This is made +possible by implementing iterations as a single cyclic dataflow +job, where the lifetimes of operators span all the steps." +Is there a similair example we could leverage for cascade? one with a "static dataset" as loop invariant? +in spark, it's up to the programmer to .cache it + + +In this paper, they also use an intermediate representation of one "basic block" per node. +A "basic block" is a sequence of instructions that always execute one after the other, +in other words contains no control flow. Control flow is defined by the edges in the +dataflow graph that connect the nodes. + +There's also a slightly different focus of this paper. The focus is not on stateful +dataflows, and obviously the application is still focused on bigdata-like applications, +not ones were latency is key issue. + + +Basic Blocks - Aho, A. V., Sethi, R., and Ullman, J. D. Compilers: principles, techniques, and +tools, vol. 2. Addison-wesley Reading, 2007. +SSA - Rastello, F. SSA-based Compiler Design. Springer Publishing Company, +Incorporated, 2016. + + +---- + +ideas from "optimization of dataflows with UDFs:" + +we are basically making a DSL (integrated with python) which would allow for optimization +of UDFs!! this optimization is inside the intermediate representation, and not directly in +the target machine (similair to Emma, which uses a functional style *but* is a DSL (does it +allow for arbitrary scala code?)) + +--- + +our program is essentially a compiler. this allows to take inspiration from existing +works on compilation (which has existed for much longer than work on dataflows (?) - +actually, dataflows were more popular initially when people didn't settle on the von Neumann architecture yet, +see e.g. Monsoon (1990s) or the original control flow to dataflow paper. the popularisation and efficiency of tools +such as drayadlinq, apache spark, apache flink has reinvigorated the attention towards dataflows). +BUT compilers are often have hardware specific optimizations, based on the hardware instruction sets, or hardware-specifics +such as optimization of register allocation, cache line considerations etc etc. +The compiler in Cascade/other cf to df systems do not necessarily have the same considerations. This is because the backend +is software rather than hardware (e.g. we use flink + kafka). Since software is generally a lot more flexible than hardware, +we can instead impose certain considerations on the execution engine (which is now software, instead of a chip) rather than +the other way around (e.g. SIMD introduced --> compiler optimizations introduced). (to be fair, compiler design has had major influences [citation needed] on CPU design, but the point is that hardware iteration +is generally slower and more expensive than software iteration). + + +--- + +for certain optimizations, cascade assumes order of any side effects (such as file IO) does not matter. +otherwise a lot of parallelization operations would become much more costly due to the necessary synchronization issues. + +--- + +other optimization: code duplication + +this would remove nodes (assumption that less nodes = faster) at the cost of more computation per node. +a common example is something like this: + +``` +cost = item.price() +if cost > 30: + shipping_discount = discount_service.get_shipping_discount() + price = cost * shipping_discount +else: + price = cost + +return price +``` + +in this case the "return price" could be duplicated accross the two branches, +such that they don't need to return back to the function body. + +--- + +other ideas: + https://en.wikipedia.org/wiki/Optimizing_compiler#Specific_techniques +""" + +from cascade.dataflow.operator import StatefulOperator, StatelessOperator + + +def node_parallelization(stateful_ops: list[StatefulOperator], stateless_ops: list[StatelessOperator]): + # Find parallelizable nodes + for op in stateful_ops: + for dataflow in op.dataflows.values(): + pass + # Parallize them \ No newline at end of file diff --git a/src/cascade/runtime/flink_runtime.py b/src/cascade/runtime/flink_runtime.py index 1a230ef..e0f14c0 100644 --- a/src/cascade/runtime/flink_runtime.py +++ b/src/cascade/runtime/flink_runtime.py @@ -285,6 +285,31 @@ def __init__(self): self, j_deserialization_schema=j_byte_string_schema ) +def deserialize_and_timestamp(x) -> Event: + t1 = time.time() + e: Event = pickle.loads(x) + t2 = time.time() + if e.metadata["in_t"] is None: + e.metadata["in_t"] = t1 + e.metadata["current_in_t"] = t1 + e.metadata["deser_times"].append(t2 - t1) + return e + +def timestamp_event(e: Event) -> Event: + t1 = time.time() + try: + e.metadata["flink_time"] += t1 - e.metadata["current_in_t"] + except KeyError: + pass + return e + +def timestamp_result(e: EventResult) -> EventResult: + t1 = time.time() + e.metadata["out_t"] = t1 + e.metadata["flink_time"] += t1 - e.metadata["current_in_t"] + e.metadata["loops"] = len(e.metadata["deser_times"]) + e.metadata["roundtrip"] = e.metadata["out_t"] - e.metadata["in_t"] + return e class FlinkRuntime(): """A Runtime that runs Dataflows on Flink.""" @@ -402,7 +427,7 @@ def init(self, kafka_broker="localhost:9092", bundle_time=1, bundle_size=5, para WatermarkStrategy.no_watermarks(), "Kafka Source" ) - .map(lambda x: pickle.loads(x)) + .map(lambda x: deserialize_and_timestamp(x)) .name("DESERIALIZE") # .filter(lambda e: isinstance(e, Event)) # Enforced by `send` type safety ) @@ -510,16 +535,21 @@ def run(self, run_async=False, output: Literal["collect", "kafka", "stdout"]="ka ds = full_stream_filtered.union(full_stream_unfiltered) # Output the stream + results = ( + ds + .filter(lambda e: isinstance(e, EventResult)) + .map(lambda e: timestamp_result(e)) + ) if output == "collect": - ds_external = ds.filter(lambda e: isinstance(e, EventResult)).execute_and_collect() + ds_external = results.execute_and_collect() elif output == "stdout": - ds_external = ds.filter(lambda e: isinstance(e, EventResult)).print() + ds_external = results.print() elif output == "kafka": - ds_external = ds.filter(lambda e: isinstance(e, EventResult)).sink_to(self.kafka_external_sink).name("EXTERNAL KAFKA SINK") + ds_external = results.sink_to(self.kafka_external_sink).name("EXTERNAL KAFKA SINK") else: raise ValueError(f"Invalid output: {output}") - ds_internal = ds.filter(lambda e: isinstance(e, Event)).sink_to(self.kafka_internal_sink).name("INTERNAL KAFKA SINK") + ds_internal = ds.filter(lambda e: isinstance(e, Event)).map(lambda e: timestamp_event(e)).sink_to(self.kafka_internal_sink).name("INTERNAL KAFKA SINK") if run_async: logger.debug("FlinkRuntime starting (async)") From 72b5b976ac2dc4fbe2db55a04b7fa29363e3e459 Mon Sep 17 00:00:00 2001 From: Lucas Van Mol Date: Thu, 13 Feb 2025 12:04:01 +0100 Subject: [PATCH 12/12] Add deathstar benchmarks --- .gitignore | 6 +- deathstar/entities/user.py | 103 - .../__init__.py | 0 .../demo.py | 748 +-- .../demo_python.py | 2 +- .../entities/__init__.py | 0 .../entities/flight.py | 64 +- .../entities/hotel.py | 162 +- .../entities/recommendation.py | 256 +- .../entities/search.py | 152 +- deathstar_hotel_reservation/entities/user.py | 105 + .../test_demo.py | 198 +- deathstar_movie_review/__init__.py | 0 deathstar_movie_review/demo.py | 224 + deathstar_movie_review/entities/__init__.py | 0 .../entities/compose_review.py | 59 + deathstar_movie_review/entities/frontend.py | 188 + deathstar_movie_review/entities/movie.py | 72 + deathstar_movie_review/entities/user.py | 36 + deathstar_movie_review/movie_data.py | 2571 +++++++++ .../test_movie_review_demo.py | 101 + deathstar_movie_review/workload_data.py | 1008 ++++ display_results.ipynb | 4659 +++++++++-------- login_10mps.png | Bin 61636 -> 0 bytes reserve_10mps.png | Bin 36356 -> 0 bytes reserve_10mps_parallel.png | Bin 34048 -> 0 bytes reserve_1mps.png | Bin 14193 -> 0 bytes reserve_1mps_parallel.png | Bin 28572 -> 0 bytes src/cascade/dataflow/dataflow.py | 89 +- .../dataflow/optimization/dead_node_elim.py | 23 +- src/cascade/runtime/flink_runtime.py | 163 +- src/cascade/runtime/python_runtime.py | 4 +- 32 files changed, 7707 insertions(+), 3286 deletions(-) delete mode 100644 deathstar/entities/user.py rename {deathstar => deathstar_hotel_reservation}/__init__.py (100%) rename {deathstar => deathstar_hotel_reservation}/demo.py (92%) rename {deathstar => deathstar_hotel_reservation}/demo_python.py (97%) rename {deathstar => deathstar_hotel_reservation}/entities/__init__.py (100%) rename {deathstar => deathstar_hotel_reservation}/entities/flight.py (95%) rename {deathstar => deathstar_hotel_reservation}/entities/hotel.py (95%) rename {deathstar => deathstar_hotel_reservation}/entities/recommendation.py (96%) rename {deathstar => deathstar_hotel_reservation}/entities/search.py (94%) create mode 100644 deathstar_hotel_reservation/entities/user.py rename {deathstar => deathstar_hotel_reservation}/test_demo.py (92%) create mode 100644 deathstar_movie_review/__init__.py create mode 100644 deathstar_movie_review/demo.py create mode 100644 deathstar_movie_review/entities/__init__.py create mode 100644 deathstar_movie_review/entities/compose_review.py create mode 100644 deathstar_movie_review/entities/frontend.py create mode 100644 deathstar_movie_review/entities/movie.py create mode 100644 deathstar_movie_review/entities/user.py create mode 100644 deathstar_movie_review/movie_data.py create mode 100644 deathstar_movie_review/test_movie_review_demo.py create mode 100644 deathstar_movie_review/workload_data.py delete mode 100644 login_10mps.png delete mode 100644 reserve_10mps.png delete mode 100644 reserve_10mps_parallel.png delete mode 100644 reserve_1mps.png delete mode 100644 reserve_1mps_parallel.png diff --git a/.gitignore b/.gitignore index 0652183..9a91afa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,8 @@ __pycache__ .DS_Store *.log *.egg-info -build \ No newline at end of file +build + +# Experiment artifacts +*.png +*.pkl \ No newline at end of file diff --git a/deathstar/entities/user.py b/deathstar/entities/user.py deleted file mode 100644 index fb620d7..0000000 --- a/deathstar/entities/user.py +++ /dev/null @@ -1,103 +0,0 @@ -from typing import Any -from cascade.dataflow.dataflow import CollectNode, CollectTarget, DataFlow, Edge, InvokeMethod, OpNode -from cascade.dataflow.operator import StatefulOperator -from deathstar.entities.flight import Flight, flight_op -from deathstar.entities.hotel import Hotel, hotel_op - - -class User(): - def __init__(self, user_id: str, password: str): - self.id = user_id - self.password = password - - def check(self, password): - return self.password == password - - def order(self, flight: Flight, hotel: Hotel): - if hotel.reserve() and flight.reserve(): - return True - else: - return False - -#### COMPILED FUNCTIONS (ORACLE) ##### - -def check_compiled(variable_map: dict[str, Any], state: User) -> Any: - return state.password == variable_map["password"] - -def order_compiled_entry_0(variable_map: dict[str, Any], state: User) -> Any: - pass - -def order_compiled_entry_1(variable_map: dict[str, Any], state: User) -> Any: - pass - -def order_compiled_if_cond(variable_map: dict[str, Any], state: User) -> Any: - return variable_map["hotel_reserve"] and variable_map["flight_reserve"] - -def order_compiled_if_cond_parallel(variable_map: dict[str, Any], state: User) -> Any: - return variable_map["reserves"][0] and variable_map["reserves"][1] - -def order_compiled_if_body(variable_map: dict[str, Any], state: User) -> Any: - return True - -def order_compiled_else_body(variable_map: dict[str, Any], state: User) -> Any: - return False - -user_op = StatefulOperator( - User, - { - "login": check_compiled, - "order_compiled_entry_0": order_compiled_entry_0, - "order_compiled_entry_1": order_compiled_entry_1, - # "order_compiled_if_cond": order_compiled_if_cond, - "order_compiled_if_cond": order_compiled_if_cond_parallel, - "order_compiled_if_body": order_compiled_if_body, - "order_compiled_else_body": order_compiled_else_body - }, - {} -) - -# For now, the dataflow will be serial instead of parallel. Future optimizations -# will try to automatically parallelize this. -# There is also no user entry (this could also be an optimization) -# df = DataFlow("user_order") -# n0 = OpNode(User, InvokeMethod("order_compiled_entry_0"), read_key_from="user_key") -# n1 = OpNode(Hotel, InvokeMethod("reserve"), assign_result_to="hotel_reserve", read_key_from="hotel_key") -# n2 = OpNode(User, InvokeMethod("order_compiled_entry_1"), read_key_from="user_key") -# n3 = OpNode(Flight, InvokeMethod("reserve"), assign_result_to="flight_reserve", read_key_from="flight_key") -# n4 = OpNode(User, InvokeMethod("order_compiled_if_cond"), is_conditional=True, read_key_from="user_key") -# n5 = OpNode(User, InvokeMethod("order_compiled_if_body"), read_key_from="user_key") -# n6 = OpNode(User, InvokeMethod("order_compiled_else_body"), read_key_from="user_key") - -# df.add_edge(Edge(n0, n1)) -# df.add_edge(Edge(n1, n2)) -# df.add_edge(Edge(n2, n3)) -# df.add_edge(Edge(n3, n4)) -# df.add_edge(Edge(n4, n5, if_conditional=True)) -# df.add_edge(Edge(n4, n6, if_conditional=False)) - -# df.entry = n0 - -# user_op.dataflows["order"] = df - - -# PARALEL DATAFLOW -df = DataFlow("user_order") -n0 = OpNode(User, InvokeMethod("order_compiled_entry_0"), read_key_from="user_key") -ct = CollectNode(assign_result_to="reserves", read_results_from="reserve") -n1 = OpNode(Hotel, InvokeMethod("reserve"), assign_result_to="reserve", read_key_from="hotel_key", collect_target=CollectTarget(ct, 2, 0)) -n3 = OpNode(Flight, InvokeMethod("reserve"), assign_result_to="reserve", read_key_from="flight_key", collect_target=CollectTarget(ct, 2, 1)) -n4 = OpNode(User, InvokeMethod("order_compiled_if_cond"), is_conditional=True, read_key_from="user_key") -n5 = OpNode(User, InvokeMethod("order_compiled_if_body"), read_key_from="user_key") -n6 = OpNode(User, InvokeMethod("order_compiled_else_body"), read_key_from="user_key") - -df.add_edge(Edge(n0, n1)) -df.add_edge(Edge(n0, n3)) -df.add_edge(Edge(n1, ct)) -df.add_edge(Edge(n3, ct)) -df.add_edge(Edge(ct, n4)) -df.add_edge(Edge(n4, n5, if_conditional=True)) -df.add_edge(Edge(n4, n6, if_conditional=False)) - -df.entry = n0 - -user_op.dataflows["order"] = df diff --git a/deathstar/__init__.py b/deathstar_hotel_reservation/__init__.py similarity index 100% rename from deathstar/__init__.py rename to deathstar_hotel_reservation/__init__.py diff --git a/deathstar/demo.py b/deathstar_hotel_reservation/demo.py similarity index 92% rename from deathstar/demo.py rename to deathstar_hotel_reservation/demo.py index 5c7a6a1..b54d643 100644 --- a/deathstar/demo.py +++ b/deathstar_hotel_reservation/demo.py @@ -1,375 +1,375 @@ -import random -import sys -import os -import time -import csv -from timeit import default_timer as timer -from multiprocessing import Pool - -# import cascade -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) - -from cascade.dataflow.dataflow import Event, EventResult, InitClass, InvokeMethod, OpNode -from cascade.runtime.flink_runtime import FlinkClientSync, FlinkOperator, FlinkRuntime, FlinkStatelessOperator -from deathstar.entities.flight import Flight, flight_op -from deathstar.entities.hotel import Geo, Hotel, Rate, hotel_op -from deathstar.entities.recommendation import Recommendation, recommend_op -from deathstar.entities.search import Search, search_op -from deathstar.entities.user import User, user_op -import pandas as pd - - -class DeathstarDemo(): - def __init__(self): - self.init_user = OpNode(User, InitClass(), read_key_from="user_id") - self.init_hotel = OpNode(Hotel, InitClass(), read_key_from="key") - self.init_flight = OpNode(Flight, InitClass(), read_key_from="id") - - def init_runtime(self, runtime, **kwargs): - self.runtime = runtime - self.runtime.init(**kwargs) - self.runtime.add_operator(hotel_op) - self.runtime.add_operator(flight_op) - self.runtime.add_operator(user_op) - self.runtime.add_stateless_operator(search_op) - self.runtime.add_stateless_operator(recommend_op) - - - def populate(self): - # Create locations & rates for hotels - geos = [] - geos.append(Geo(37.7867, 0)) - geos.append(Geo(37.7854, -122.4005)) - geos.append(Geo(37.7867, -122.4071)) - geos.append(Geo(37.7936, -122.3930)) - geos.append(Geo(37.7831, -122.4181)) - geos.append(Geo(37.7863, -122.4015)) - - for i in range(6, 100): - lat: float = 37.7835 + i / 500.0 * 3 - lon: float = -122.41 + i / 500.0 * 4 - geos.append(Geo(lat, lon)) - - rates = {} - rates[1] = Rate(1, "RACK", - "2015-04-09", - "2015-04-10", - { "BookableRate": 190.0, - "Code": "KNG", - "RoomDescription": "King sized bed", - "TotalRate": 109.0, - "TotalRateInclusive": 123.17}) - - rates[2] = Rate(2, "RACK", - "2015-04-09", - "2015-04-10", - { "BookableRate": 139.0, - "Code": "QN", - "RoomDescription": "Queen sized bed", - "TotalRate": 139.0, - "TotalRateInclusive": 153.09}) - - rates[3] = Rate(3, "RACK", - "2015-04-09", - "2015-04-10", - { "BookableRate": 109.0, - "Code": "KNG", - "RoomDescription": "King sized bed", - "TotalRate": 109.0, - "TotalRateInclusive": 123.17}) - - for i in range(4, 80): - if i % 3 == 0: - hotel_id = i - end_date = "2015-04-" - rate = 109.0 - rate_inc = 123.17 - if i % 2 == 0: - end_date += '17' - else: - end_date += '24' - if i % 5 == 1: - rate = 120.0 - rate_inc = 140.0 - elif i % 5 == 2: - rate = 124.0 - rate_inc = 144.0 - elif i % 5 == 3: - rate = 132.0 - rate_inc = 158.0 - elif i % 5 == 4: - rate = 232.0 - rate_inc = 258.0 - - rates[hotel_id] = Rate(i, "RACK", - "2015-04-09", - end_date, - { "BookableRate": rate, - "Code": "KNG", - "RoomDescription": "King sized bed", - "TotalRate": rate, - "TotalRateInclusive": rate_inc}) - - # we don't create recommendations, because it doesn't really - # correspond to an entity - prices = [] - - prices.append(150.00) - prices.append(120.00) - prices.append(190.00) - prices.append(160.00) - prices.append(140.00) - prices.append(200.00) - - for i in range(6, 100): - price = 179.00 - if i % 3 == 0: - if i % 5 == 0: - price = 123.17 - elif i % 5 == 1: - price = 140.00 - elif i % 5 == 2: - price = 144.00 - elif i % 5 == 3: - price = 158.00 - elif i % 5 == 4: - price = 258.00 - - prices.append(price) - - # populate users - self.users = [User(f"Cornell_{i}", str(i) * 10) for i in range(501)] - for user in self.users: - event = Event(self.init_user, {"user_id": user.id, "password": user.password}, None) - self.runtime.send(event) - - # populate hotels - self.hotels: list[Hotel] = [] - for i in range(100): - geo = geos[i] - rate = rates[i] if i in rates else [] - price = prices[i] - hotel = Hotel(str(i), 10, geo, rate, price) - self.hotels.append(hotel) - event = Event(self.init_hotel, - { - "key": hotel.key, - "cap": hotel.cap, - "geo": hotel.geo, - "rates": hotel.rates, - "price": hotel.price - }, None) - self.runtime.send(event) - - # populate flights - self.flights = [Flight(str(i), 10) for i in range(100)] - for flight in self.flights[:-1]: - event = Event(self.init_flight, { - "id": flight.id, - "cap": flight.cap - }, None) - self.runtime.send(event) - flight = self.flights[-1] - event = Event(self.init_flight, { - "id": flight.id, - "cap": flight.cap - }, None) - self.runtime.send(event, flush=True) - -def search_hotel(): - in_date = random.randint(9, 23) - out_date = random.randint(in_date + 1, 24) - - if in_date < 10: - in_date_str = f"2015-04-0{in_date}" - else: - in_date_str = f"2015-04-{in_date}" - if out_date < 10: - out_date_str = f"2015-04-0{out_date}" - else: - out_date_str = f"2015-04-{out_date}" - - lat = 38.0235 + (random.randint(0, 481) - 240.5) / 1000.0 - lon = -122.095 + (random.randint(0, 325) - 157.0) / 1000.0 - - # We don't really use the in_date, out_date information - return Event(search_op.dataflow.entry, {"lat": lat, "lon": lon}, search_op.dataflow) - -def recommend(req_param=None): - if req_param is None: - coin = random.random() - if coin < 0.5: - req_param = "distance" - else: - req_param = "price" - - lat = 38.0235 + (random.randint(0, 481) - 240.5) / 1000.0 - lon = -122.095 + (random.randint(0, 325) - 157.0) / 1000.0 - - - return Event(recommend_op.dataflow.entry, {"requirement": req_param, "lat": lat, "lon": lon}, recommend_op.dataflow) - -def user_login(succesfull=True): - user_id = random.randint(0, 500) - username = f"Cornell_{user_id}" - password = str(user_id) * 10 if succesfull else "" - return Event(OpNode(User, InvokeMethod("login"), read_key_from="user_key"), {"user_key": username, "password": password}, None) - - -def reserve(): - hotel_id = random.randint(0, 99) - flight_id = random.randint(0, 99) - - user_id = "Cornell_" + str(random.randint(0, 500)) - - return Event( - user_op.dataflows["order"].entry, - { - "user_key": user_id, - "flight_key": str(flight_id), - "hotel_key": str(hotel_id) - }, - user_op.dataflows["order"]) - -def deathstar_workload_generator(): - search_ratio = 0.6 - recommend_ratio = 0.39 - user_ratio = 0.005 - reserve_ratio = 0.005 - c = 0 - while True: - coin = random.random() - if coin < search_ratio: - yield search_hotel() - elif coin < search_ratio + recommend_ratio: - yield recommend() - elif coin < search_ratio + recommend_ratio + user_ratio: - yield user_login() - else: - yield reserve() - c += 1 - -def reserve_workload_generator(): - while True: - yield reserve() - -def user_login_workload_generator(): - while True: - yield user_login() - -threads = 1 -messages_per_burst = 1 -sleeps_per_burst = 1 -sleep_time = 0.0085 -seconds_per_burst = 1 -bursts = 100 - - -def benchmark_runner(proc_num) -> dict[int, dict]: - print(f'Generator: {proc_num} starting') - client = FlinkClientSync("deathstar", "ds-out", "localhost:9092", True) - deathstar_generator = reserve_workload_generator() - start = timer() - - for _ in range(bursts): - sec_start = timer() - - # send burst of messages - for i in range(messages_per_burst): - - # sleep sometimes between messages - if i % (messages_per_burst // sleeps_per_burst) == 0: - time.sleep(sleep_time) - event = next(deathstar_generator) - client.send(event) - - client.flush() - sec_end = timer() - - # wait out the second - lps = sec_end - sec_start - if lps < seconds_per_burst: - time.sleep(1 - lps) - sec_end2 = timer() - print(f'Latency per burst: {sec_end2 - sec_start} ({seconds_per_burst})') - - end = timer() - print(f'Average latency per burst: {(end - start) / bursts} ({seconds_per_burst})') - - done = False - while not done: - done = True - for event_id, fut in client._futures.items(): - result = fut["ret"] - if result is None: - done = False - time.sleep(0.5) - break - futures = client._futures - client.close() - return futures - - -def write_dict_to_pkl(futures_dict, filename): - """ - Writes a dictionary of event data to a pickle file. - - Args: - futures_dict (dict): A dictionary where each key is an event ID and the value is another dict. - filename (str): The name of the pickle file to write to. - """ - - # Prepare the data for the DataFrame - data = [] - for event_id, event_data in futures_dict.items(): - ret: EventResult = event_data.get("ret") - row = { - "event_id": event_id, - "sent": str(event_data.get("sent")), - "sent_t": event_data.get("sent_t"), - "ret": str(event_data.get("ret")), - "ret_t": event_data.get("ret_t"), - "roundtrip": ret.metadata["roundtrip"] if ret else None, - "flink_time": ret.metadata["flink_time"] if ret else None, - "deser_times": ret.metadata["deser_times"] if ret else None, - "loops": ret.metadata["loops"] if ret else None, - "latency": event_data["ret_t"][1] - event_data["sent_t"][1] if ret else None - } - data.append(row) - - # Create a DataFrame and save it as a pickle file - df = pd.DataFrame(data) - df.to_pickle(filename) - -def main(): - ds = DeathstarDemo() - ds.init_runtime(FlinkRuntime("deathstar", "ds-out"), bundle_time=5, bundle_size=10) - ds.runtime.run(run_async=True) - ds.populate() - - - time.sleep(1) - input() - - # with Pool(threads) as p: - # results = p.map(benchmark_runner, range(threads)) - - # results = {k: v for d in results for k, v in d.items()} - results = benchmark_runner(0) - - # pd.DataFrame({"request_id": list(results.keys()), - # "timestamp": [res["timestamp"] for res in results.values()], - # "op": [res["op"] for res in results.values()] - # }).sort_values("timestamp").to_csv(f'{SAVE_DIR}/client_requests.csv', index=False) - print(results) - t = len(results) - r = 0 - for result in results.values(): - if result["ret"] is not None: - print(result) - r += 1 - print(f"{r}/{t} results recieved.") - write_dict_to_pkl(results, "test2.pkl") - -if __name__ == "__main__": +import random +import sys +import os +import time +import csv +from timeit import default_timer as timer +from multiprocessing import Pool + +# import cascade +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from cascade.dataflow.dataflow import Event, EventResult, InitClass, InvokeMethod, OpNode +from cascade.runtime.flink_runtime import FlinkClientSync, FlinkOperator, FlinkRuntime, FlinkStatelessOperator +from deathstar_hotel_reservation.entities.flight import Flight, flight_op +from deathstar_hotel_reservation.entities.hotel import Geo, Hotel, Rate, hotel_op +from deathstar_hotel_reservation.entities.recommendation import Recommendation, recommend_op +from deathstar_hotel_reservation.entities.search import Search, search_op +from deathstar_hotel_reservation.entities.user import User, user_op +import pandas as pd + + +class DeathstarDemo(): + def __init__(self): + self.init_user = OpNode(User, InitClass(), read_key_from="user_id") + self.init_hotel = OpNode(Hotel, InitClass(), read_key_from="key") + self.init_flight = OpNode(Flight, InitClass(), read_key_from="id") + + def init_runtime(self, runtime, **kwargs): + self.runtime = runtime + self.runtime.init(**kwargs) + self.runtime.add_operator(hotel_op) + self.runtime.add_operator(flight_op) + self.runtime.add_operator(user_op) + # self.runtime.add_stateless_operator(search_op) + # self.runtime.add_stateless_operator(recommend_op) + + + def populate(self): + # Create locations & rates for hotels + geos = [] + geos.append(Geo(37.7867, 0)) + geos.append(Geo(37.7854, -122.4005)) + geos.append(Geo(37.7867, -122.4071)) + geos.append(Geo(37.7936, -122.3930)) + geos.append(Geo(37.7831, -122.4181)) + geos.append(Geo(37.7863, -122.4015)) + + for i in range(6, 100): + lat: float = 37.7835 + i / 500.0 * 3 + lon: float = -122.41 + i / 500.0 * 4 + geos.append(Geo(lat, lon)) + + rates = {} + rates[1] = Rate(1, "RACK", + "2015-04-09", + "2015-04-10", + { "BookableRate": 190.0, + "Code": "KNG", + "RoomDescription": "King sized bed", + "TotalRate": 109.0, + "TotalRateInclusive": 123.17}) + + rates[2] = Rate(2, "RACK", + "2015-04-09", + "2015-04-10", + { "BookableRate": 139.0, + "Code": "QN", + "RoomDescription": "Queen sized bed", + "TotalRate": 139.0, + "TotalRateInclusive": 153.09}) + + rates[3] = Rate(3, "RACK", + "2015-04-09", + "2015-04-10", + { "BookableRate": 109.0, + "Code": "KNG", + "RoomDescription": "King sized bed", + "TotalRate": 109.0, + "TotalRateInclusive": 123.17}) + + for i in range(4, 80): + if i % 3 == 0: + hotel_id = i + end_date = "2015-04-" + rate = 109.0 + rate_inc = 123.17 + if i % 2 == 0: + end_date += '17' + else: + end_date += '24' + if i % 5 == 1: + rate = 120.0 + rate_inc = 140.0 + elif i % 5 == 2: + rate = 124.0 + rate_inc = 144.0 + elif i % 5 == 3: + rate = 132.0 + rate_inc = 158.0 + elif i % 5 == 4: + rate = 232.0 + rate_inc = 258.0 + + rates[hotel_id] = Rate(i, "RACK", + "2015-04-09", + end_date, + { "BookableRate": rate, + "Code": "KNG", + "RoomDescription": "King sized bed", + "TotalRate": rate, + "TotalRateInclusive": rate_inc}) + + # we don't create recommendations, because it doesn't really + # correspond to an entity + prices = [] + + prices.append(150.00) + prices.append(120.00) + prices.append(190.00) + prices.append(160.00) + prices.append(140.00) + prices.append(200.00) + + for i in range(6, 100): + price = 179.00 + if i % 3 == 0: + if i % 5 == 0: + price = 123.17 + elif i % 5 == 1: + price = 140.00 + elif i % 5 == 2: + price = 144.00 + elif i % 5 == 3: + price = 158.00 + elif i % 5 == 4: + price = 258.00 + + prices.append(price) + + # populate users + self.users = [User(f"Cornell_{i}", str(i) * 10) for i in range(501)] + for user in self.users: + event = Event(self.init_user, {"user_id": user.id, "password": user.password}, None) + self.runtime.send(event) + + # populate hotels + self.hotels: list[Hotel] = [] + for i in range(100): + geo = geos[i] + rate = rates[i] if i in rates else [] + price = prices[i] + hotel = Hotel(str(i), 10, geo, rate, price) + self.hotels.append(hotel) + event = Event(self.init_hotel, + { + "key": hotel.key, + "cap": hotel.cap, + "geo": hotel.geo, + "rates": hotel.rates, + "price": hotel.price + }, None) + self.runtime.send(event) + + # populate flights + self.flights = [Flight(str(i), 10) for i in range(100)] + for flight in self.flights[:-1]: + event = Event(self.init_flight, { + "id": flight.id, + "cap": flight.cap + }, None) + self.runtime.send(event) + flight = self.flights[-1] + event = Event(self.init_flight, { + "id": flight.id, + "cap": flight.cap + }, None) + self.runtime.send(event, flush=True) + +def search_hotel(): + in_date = random.randint(9, 23) + out_date = random.randint(in_date + 1, 24) + + if in_date < 10: + in_date_str = f"2015-04-0{in_date}" + else: + in_date_str = f"2015-04-{in_date}" + if out_date < 10: + out_date_str = f"2015-04-0{out_date}" + else: + out_date_str = f"2015-04-{out_date}" + + lat = 38.0235 + (random.randint(0, 481) - 240.5) / 1000.0 + lon = -122.095 + (random.randint(0, 325) - 157.0) / 1000.0 + + # We don't really use the in_date, out_date information + return Event(search_op.dataflow.entry, {"lat": lat, "lon": lon}, search_op.dataflow) + +def recommend(req_param=None): + if req_param is None: + coin = random.random() + if coin < 0.5: + req_param = "distance" + else: + req_param = "price" + + lat = 38.0235 + (random.randint(0, 481) - 240.5) / 1000.0 + lon = -122.095 + (random.randint(0, 325) - 157.0) / 1000.0 + + + return Event(recommend_op.dataflow.entry, {"requirement": req_param, "lat": lat, "lon": lon}, recommend_op.dataflow) + +def user_login(succesfull=True): + user_id = random.randint(0, 500) + username = f"Cornell_{user_id}" + password = str(user_id) * 10 if succesfull else "" + return Event(OpNode(User, InvokeMethod("login"), read_key_from="user_key"), {"user_key": username, "password": password}, None) + + +def reserve(): + hotel_id = random.randint(0, 99) + flight_id = random.randint(0, 99) + + user_id = "Cornell_" + str(random.randint(0, 500)) + + return Event( + user_op.dataflows["order"].entry, + { + "user_key": user_id, + "flight_key": str(flight_id), + "hotel_key": str(hotel_id) + }, + user_op.dataflows["order"]) + +def deathstar_workload_generator(): + search_ratio = 0.6 + recommend_ratio = 0.39 + user_ratio = 0.005 + reserve_ratio = 0.005 + c = 0 + while True: + coin = random.random() + if coin < search_ratio: + yield search_hotel() + elif coin < search_ratio + recommend_ratio: + yield recommend() + elif coin < search_ratio + recommend_ratio + user_ratio: + yield user_login() + else: + yield reserve() + c += 1 + +def reserve_workload_generator(): + while True: + yield reserve() + +def user_login_workload_generator(): + while True: + yield user_login() + +threads = 1 +messages_per_burst = 10 +sleeps_per_burst = 1 +sleep_time = 0.0085 +seconds_per_burst = 1 +bursts = 50 + + +def benchmark_runner(proc_num) -> dict[int, dict]: + print(f'Generator: {proc_num} starting') + client = FlinkClientSync("deathstar", "ds-out", "localhost:9092", True) + deathstar_generator = user_login_workload_generator() + start = timer() + + for _ in range(bursts): + sec_start = timer() + + # send burst of messages + for i in range(messages_per_burst): + + # sleep sometimes between messages + if i % (messages_per_burst // sleeps_per_burst) == 0: + time.sleep(sleep_time) + event = next(deathstar_generator) + client.send(event) + + client.flush() + sec_end = timer() + + # wait out the second + lps = sec_end - sec_start + if lps < seconds_per_burst: + time.sleep(1 - lps) + sec_end2 = timer() + print(f'Latency per burst: {sec_end2 - sec_start} ({seconds_per_burst})') + + end = timer() + print(f'Average latency per burst: {(end - start) / bursts} ({seconds_per_burst})') + + done = False + while not done: + done = True + for event_id, fut in client._futures.items(): + result = fut["ret"] + if result is None: + done = False + time.sleep(0.5) + break + futures = client._futures + client.close() + return futures + + +def write_dict_to_pkl(futures_dict, filename): + """ + Writes a dictionary of event data to a pickle file. + + Args: + futures_dict (dict): A dictionary where each key is an event ID and the value is another dict. + filename (str): The name of the pickle file to write to. + """ + + # Prepare the data for the DataFrame + data = [] + for event_id, event_data in futures_dict.items(): + ret: EventResult = event_data.get("ret") + row = { + "event_id": event_id, + "sent": str(event_data.get("sent")), + "sent_t": event_data.get("sent_t"), + "ret": str(event_data.get("ret")), + "ret_t": event_data.get("ret_t"), + "roundtrip": ret.metadata["roundtrip"] if ret else None, + "flink_time": ret.metadata["flink_time"] if ret else None, + "deser_times": ret.metadata["deser_times"] if ret else None, + "loops": ret.metadata["loops"] if ret else None, + "latency": event_data["ret_t"][1] - event_data["sent_t"][1] if ret else None + } + data.append(row) + + # Create a DataFrame and save it as a pickle file + df = pd.DataFrame(data) + df.to_pickle(filename) + +def main(): + ds = DeathstarDemo() + ds.init_runtime(FlinkRuntime("deathstar", "ds-out", ui_port=8081), bundle_time=5, bundle_size=10) + ds.runtime.run(run_async=True) + ds.populate() + + + time.sleep(1) + input() + + # with Pool(threads) as p: + # results = p.map(benchmark_runner, range(threads)) + + # results = {k: v for d in results for k, v in d.items()} + results = benchmark_runner(0) + + # pd.DataFrame({"request_id": list(results.keys()), + # "timestamp": [res["timestamp"] for res in results.values()], + # "op": [res["op"] for res in results.values()] + # }).sort_values("timestamp").to_csv(f'{SAVE_DIR}/client_requests.csv', index=False) + print(results) + t = len(results) + r = 0 + for result in results.values(): + if result["ret"] is not None: + print(result) + r += 1 + print(f"{r}/{t} results recieved.") + write_dict_to_pkl(results, "test2.pkl") + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/deathstar/demo_python.py b/deathstar_hotel_reservation/demo_python.py similarity index 97% rename from deathstar/demo_python.py rename to deathstar_hotel_reservation/demo_python.py index 80e687c..1d44ac4 100644 --- a/deathstar/demo_python.py +++ b/deathstar_hotel_reservation/demo_python.py @@ -7,7 +7,7 @@ from cascade.runtime.python_runtime import PythonRuntime -from deathstar.demo import DeathstarDemo, deathstar_workload_generator +from deathstar_hotel_reservation.demo import DeathstarDemo, deathstar_workload_generator from timeit import default_timer as timer import csv diff --git a/deathstar/entities/__init__.py b/deathstar_hotel_reservation/entities/__init__.py similarity index 100% rename from deathstar/entities/__init__.py rename to deathstar_hotel_reservation/entities/__init__.py diff --git a/deathstar/entities/flight.py b/deathstar_hotel_reservation/entities/flight.py similarity index 95% rename from deathstar/entities/flight.py rename to deathstar_hotel_reservation/entities/flight.py index 445ff9e..9a70415 100644 --- a/deathstar/entities/flight.py +++ b/deathstar_hotel_reservation/entities/flight.py @@ -1,32 +1,32 @@ -from typing import Any -from cascade.dataflow.dataflow import Operator -from cascade.dataflow.operator import StatefulOperator - - -class Flight(): - def __init__(self, id: str, cap: int): - self.id = id - self.cap = cap - # self.customers = [] - - # In order to be deterministic, we don't actually change the capacity - def reserve(self) -> bool: - if self.cap <= 0: - return False - return True - - -#### COMPILED FUNCTIONS (ORACLE) ##### - -def reserve_compiled(variable_map: dict[str, Any], state: Flight) -> Any: - if state.cap <= 0: - return False - return True - -flight_op = StatefulOperator( - Flight, - { - "reserve": reserve_compiled - }, - {} # no dataflow? -) +from typing import Any +from cascade.dataflow.dataflow import Operator +from cascade.dataflow.operator import StatefulOperator + + +class Flight(): + def __init__(self, id: str, cap: int): + self.id = id + self.cap = cap + # self.customers = [] + + # In order to be deterministic, we don't actually change the capacity + def reserve(self) -> bool: + if self.cap <= 0: + return False + return True + + +#### COMPILED FUNCTIONS (ORACLE) ##### + +def reserve_compiled(variable_map: dict[str, Any], state: Flight) -> Any: + if state.cap <= 0: + return False + return True + +flight_op = StatefulOperator( + Flight, + { + "reserve": reserve_compiled + }, + {} # no dataflow? +) diff --git a/deathstar/entities/hotel.py b/deathstar_hotel_reservation/entities/hotel.py similarity index 95% rename from deathstar/entities/hotel.py rename to deathstar_hotel_reservation/entities/hotel.py index e57386d..923acb6 100644 --- a/deathstar/entities/hotel.py +++ b/deathstar_hotel_reservation/entities/hotel.py @@ -1,81 +1,81 @@ -from dataclasses import dataclass -from typing import Any -from cascade.dataflow.operator import StatefulOperator -from geopy.distance import distance - - -@dataclass -class Geo(): - lat: float - lon: float - - def distance_km(self, lat: float, lon: float): - return distance((lat, lon), (self.lat, self.lon)).km - -@dataclass -class Rate(): - key: int - code: str - in_date: str - out_date: str - room_type: dict - - def __key__(self): - return self.key - -# todo: add a linked entity -# e.g. reviews: list[Review] where Review is an entity -class Hotel(): - def __init__(self, - key: str, - cap: int, - geo: Geo, - rates: list[Rate], - price: float): - self.key = key - self.cap = cap - self.customers = [] - self.rates = rates - self.geo = geo - self.price = price - - # In order to be deterministic, we don't actually change the capacity - def reserve(self) -> bool: - if self.cap < 0: - return False - return True - - def get_geo(self) -> Geo: - return self.geo - - @staticmethod - def __all__() -> list['Hotel']: - pass - - def __key__(self) -> int: - return self.key - - - -#### COMPILED FUNCTIONS (ORACLE) ##### - -def reserve_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: - if state.cap <= 0: - return False - return True - -def get_geo_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: - return state.geo - -def get_price_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: - return state.price - -hotel_op = StatefulOperator( - Hotel, - { - "reserve": reserve_compiled, - "get_geo": get_geo_compiled, - "get_price": get_price_compiled - }, - {} # no dataflow? -) +from dataclasses import dataclass +from typing import Any +from cascade.dataflow.operator import StatefulOperator +from geopy.distance import distance + + +@dataclass +class Geo(): + lat: float + lon: float + + def distance_km(self, lat: float, lon: float): + return distance((lat, lon), (self.lat, self.lon)).km + +@dataclass +class Rate(): + key: int + code: str + in_date: str + out_date: str + room_type: dict + + def __key__(self): + return self.key + +# todo: add a linked entity +# e.g. reviews: list[Review] where Review is an entity +class Hotel(): + def __init__(self, + key: str, + cap: int, + geo: Geo, + rates: list[Rate], + price: float): + self.key = key + self.cap = cap + self.customers = [] + self.rates = rates + self.geo = geo + self.price = price + + # In order to be deterministic, we don't actually change the capacity + def reserve(self) -> bool: + if self.cap < 0: + return False + return True + + def get_geo(self) -> Geo: + return self.geo + + @staticmethod + def __all__() -> list['Hotel']: + pass + + def __key__(self) -> int: + return self.key + + + +#### COMPILED FUNCTIONS (ORACLE) ##### + +def reserve_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: + if state.cap <= 0: + return False + return True + +def get_geo_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: + return state.geo + +def get_price_compiled(variable_map: dict[str, Any], state: Hotel) -> Any: + return state.price + +hotel_op = StatefulOperator( + Hotel, + { + "reserve": reserve_compiled, + "get_geo": get_geo_compiled, + "get_price": get_price_compiled + }, + {} # no dataflow? +) diff --git a/deathstar/entities/recommendation.py b/deathstar_hotel_reservation/entities/recommendation.py similarity index 96% rename from deathstar/entities/recommendation.py rename to deathstar_hotel_reservation/entities/recommendation.py index 99883ea..576da60 100644 --- a/deathstar/entities/recommendation.py +++ b/deathstar_hotel_reservation/entities/recommendation.py @@ -1,129 +1,129 @@ -from typing import Any, Literal -from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode -from cascade.dataflow.operator import StatelessOperator -from deathstar.entities.hotel import Geo, Hotel - -# Stateless -class Recommendation(): - @staticmethod - def get_recommendations(requirement: Literal["distance", "price"], lat: float, lon: float) -> list[Hotel]: - if requirement == "distance": - distances = [(hotel.geo.distance_km(lat, lon), hotel) - for hotel in Hotel.__all__()] - min_dist = min(distances, key=lambda x: x[0]) - res = [hotel for dist, hotel in distances if dist == min_dist] - elif requirement == "price": - prices = [(hotel.price, hotel) - for hotel in Hotel.__all__()] - min_price = min(prices, key=lambda x: x[0]) - res = [hotel for rate, hotel in prices if rate == min_price] - - # todo: raise error on else ...? - return res - -#### COMPILED FUNCTIONS (ORACLE) #### - -def get_recs_if_cond(variable_map: dict[str, Any]): - return variable_map["requirement"] == "distance" - -# list comprehension entry -def get_recs_if_body_0(variable_map: dict[str, Any]): - pass - - -# list comprehension body -def get_recs_if_body_1(variable_map: dict[str, Any]): - hotel_geo: Geo = variable_map["hotel_geo"] - lat, lon = variable_map["lat"], variable_map["lon"] - dist = hotel_geo.distance_km(lat, lon) - return (dist, variable_map["hotel_key"]) - -# after list comprehension -def get_recs_if_body_2(variable_map: dict[str, Any]): - distances = variable_map["distances"] - min_dist = min(distances, key=lambda x: x[0])[0] - variable_map["res"] = [hotel for dist, hotel in distances if dist == min_dist] - - -def get_recs_elif_cond(variable_map: dict[str, Any]): - return variable_map["requirement"] == "price" - - -# list comprehension entry -def get_recs_elif_body_0(variable_map: dict[str, Any]): - pass - - -# list comprehension body -def get_recs_elif_body_1(variable_map: dict[str, Any]): - return (variable_map["hotel_price"], variable_map["hotel_key"]) - -# after list comprehension -def get_recs_elif_body_2(variable_map: dict[str, Any]): - prices = variable_map["prices"] - min_price = min(prices, key=lambda x: x[0])[0] - variable_map["res"] = [hotel for price, hotel in prices if price == min_price] - - - -# a future optimization might instead duplicate this piece of code over the two -# branches, in order to reduce the number of splits by one -def get_recs_final(variable_map: dict[str, Any]): - return variable_map["res"] - - -recommend_op = StatelessOperator({ - "get_recs_if_cond": get_recs_if_cond, - "get_recs_if_body_0": get_recs_if_body_0, - "get_recs_if_body_1": get_recs_if_body_1, - "get_recs_if_body_2": get_recs_if_body_2, - "get_recs_elif_cond": get_recs_elif_cond, - "get_recs_elif_body_0": get_recs_elif_body_0, - "get_recs_elif_body_1": get_recs_elif_body_1, - "get_recs_elif_body_2": get_recs_elif_body_2, - "get_recs_final": get_recs_final, -}, None) - -df = DataFlow("get_recommendations") -n1 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_cond"), is_conditional=True) -n2 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_0")) -n3 = OpNode(Hotel, InvokeMethod("get_geo"), assign_result_to="hotel_geo", read_key_from="hotel_key") -n4 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_1"), assign_result_to="distance") -n5 = CollectNode("distances", "distance") -n6 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_2")) -ns1 = SelectAllNode(Hotel, n5, assign_key_to="hotel_key") - -n7 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_cond"), is_conditional=True) -n8 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_0")) -n9 = OpNode(Hotel, InvokeMethod("get_price"), assign_result_to="hotel_price", read_key_from="hotel_key") -n10 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_1"), assign_result_to="price") -n11 = CollectNode("prices", "price") -n12 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_2")) -ns2 = SelectAllNode(Hotel, n11, assign_key_to="hotel_key") - - -n13 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_final")) - -df.add_edge(Edge(n1, ns1, if_conditional=True)) -df.add_edge(Edge(n1, n7, if_conditional=False)) -df.add_edge(Edge(n7, ns2, if_conditional=True)) -df.add_edge(Edge(n7, n13, if_conditional=False)) - -# if branch -df.add_edge(Edge(ns1, n2)) -df.add_edge(Edge(n2, n3)) -df.add_edge(Edge(n3, n4)) -df.add_edge(Edge(n4, n5)) -df.add_edge(Edge(n5, n6)) -df.add_edge(Edge(n6, n13)) - -# elif branch -df.add_edge(Edge(ns2, n8)) -df.add_edge(Edge(n8, n9)) -df.add_edge(Edge(n9, n10)) -df.add_edge(Edge(n10, n11)) -df.add_edge(Edge(n11, n12)) -df.add_edge(Edge(n12, n13)) - -df.entry = n1 +from typing import Any, Literal +from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode +from cascade.dataflow.operator import StatelessOperator +from deathstar_hotel_reservation.entities.hotel import Geo, Hotel + +# Stateless +class Recommendation(): + @staticmethod + def get_recommendations(requirement: Literal["distance", "price"], lat: float, lon: float) -> list[Hotel]: + if requirement == "distance": + distances = [(hotel.geo.distance_km(lat, lon), hotel) + for hotel in Hotel.__all__()] + min_dist = min(distances, key=lambda x: x[0]) + res = [hotel for dist, hotel in distances if dist == min_dist] + elif requirement == "price": + prices = [(hotel.price, hotel) + for hotel in Hotel.__all__()] + min_price = min(prices, key=lambda x: x[0]) + res = [hotel for rate, hotel in prices if rate == min_price] + + # todo: raise error on else ...? + return res + +#### COMPILED FUNCTIONS (ORACLE) #### + +def get_recs_if_cond(variable_map: dict[str, Any]): + return variable_map["requirement"] == "distance" + +# list comprehension entry +def get_recs_if_body_0(variable_map: dict[str, Any]): + pass + + +# list comprehension body +def get_recs_if_body_1(variable_map: dict[str, Any]): + hotel_geo: Geo = variable_map["hotel_geo"] + lat, lon = variable_map["lat"], variable_map["lon"] + dist = hotel_geo.distance_km(lat, lon) + return (dist, variable_map["hotel_key"]) + +# after list comprehension +def get_recs_if_body_2(variable_map: dict[str, Any]): + distances = variable_map["distances"] + min_dist = min(distances, key=lambda x: x[0])[0] + variable_map["res"] = [hotel for dist, hotel in distances if dist == min_dist] + + +def get_recs_elif_cond(variable_map: dict[str, Any]): + return variable_map["requirement"] == "price" + + +# list comprehension entry +def get_recs_elif_body_0(variable_map: dict[str, Any]): + pass + + +# list comprehension body +def get_recs_elif_body_1(variable_map: dict[str, Any]): + return (variable_map["hotel_price"], variable_map["hotel_key"]) + +# after list comprehension +def get_recs_elif_body_2(variable_map: dict[str, Any]): + prices = variable_map["prices"] + min_price = min(prices, key=lambda x: x[0])[0] + variable_map["res"] = [hotel for price, hotel in prices if price == min_price] + + + +# a future optimization might instead duplicate this piece of code over the two +# branches, in order to reduce the number of splits by one +def get_recs_final(variable_map: dict[str, Any]): + return variable_map["res"] + + +recommend_op = StatelessOperator({ + "get_recs_if_cond": get_recs_if_cond, + "get_recs_if_body_0": get_recs_if_body_0, + "get_recs_if_body_1": get_recs_if_body_1, + "get_recs_if_body_2": get_recs_if_body_2, + "get_recs_elif_cond": get_recs_elif_cond, + "get_recs_elif_body_0": get_recs_elif_body_0, + "get_recs_elif_body_1": get_recs_elif_body_1, + "get_recs_elif_body_2": get_recs_elif_body_2, + "get_recs_final": get_recs_final, +}, None) + +df = DataFlow("get_recommendations") +n1 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_cond"), is_conditional=True) +n2 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_0")) +n3 = OpNode(Hotel, InvokeMethod("get_geo"), assign_result_to="hotel_geo", read_key_from="hotel_key") +n4 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_1"), assign_result_to="distance") +n5 = CollectNode("distances", "distance") +n6 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_if_body_2")) +ns1 = SelectAllNode(Hotel, n5, assign_key_to="hotel_key") + +n7 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_cond"), is_conditional=True) +n8 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_0")) +n9 = OpNode(Hotel, InvokeMethod("get_price"), assign_result_to="hotel_price", read_key_from="hotel_key") +n10 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_1"), assign_result_to="price") +n11 = CollectNode("prices", "price") +n12 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_elif_body_2")) +ns2 = SelectAllNode(Hotel, n11, assign_key_to="hotel_key") + + +n13 = StatelessOpNode(recommend_op, InvokeMethod("get_recs_final")) + +df.add_edge(Edge(n1, ns1, if_conditional=True)) +df.add_edge(Edge(n1, n7, if_conditional=False)) +df.add_edge(Edge(n7, ns2, if_conditional=True)) +df.add_edge(Edge(n7, n13, if_conditional=False)) + +# if branch +df.add_edge(Edge(ns1, n2)) +df.add_edge(Edge(n2, n3)) +df.add_edge(Edge(n3, n4)) +df.add_edge(Edge(n4, n5)) +df.add_edge(Edge(n5, n6)) +df.add_edge(Edge(n6, n13)) + +# elif branch +df.add_edge(Edge(ns2, n8)) +df.add_edge(Edge(n8, n9)) +df.add_edge(Edge(n9, n10)) +df.add_edge(Edge(n10, n11)) +df.add_edge(Edge(n11, n12)) +df.add_edge(Edge(n12, n13)) + +df.entry = n1 recommend_op.dataflow = df \ No newline at end of file diff --git a/deathstar/entities/search.py b/deathstar_hotel_reservation/entities/search.py similarity index 94% rename from deathstar/entities/search.py rename to deathstar_hotel_reservation/entities/search.py index 0b508d3..abc5895 100644 --- a/deathstar/entities/search.py +++ b/deathstar_hotel_reservation/entities/search.py @@ -1,77 +1,77 @@ -from typing import Any -from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode -from cascade.dataflow.operator import StatelessOperator -from deathstar.entities.hotel import Geo, Hotel, hotel_op - -# Stateless -class Search(): - # Get the 5 nearest hotels - @staticmethod - def nearby(lat: float, lon: float, in_date: int, out_date: int): - distances = [ - (dist, hotel) - for hotel in Hotel.__all__() - if (dist := hotel.geo.distance_km(lat, lon)) < 10] - hotels = [hotel for dist, hotel in sorted(distances)[:5]] - return hotels - - -#### COMPILED FUNCTIONS (ORACLE) ##### - - - -# predicate 1 -def search_nearby_compiled_0(variable_map: dict[str, Any]): - pass - -# predicate 2 -def search_nearby_compiled_1(variable_map: dict[str, Any]): - hotel_geo: Geo = variable_map["hotel_geo"] - lat, lon = variable_map["lat"], variable_map["lon"] - dist = hotel_geo.distance_km(lat, lon) - variable_map["dist"] = dist - return dist < 10 - - -# body -def search_nearby_compiled_2(variable_map: dict[str, Any]): - return (variable_map["dist"], variable_map["hotel_key"]) - -# next line -def search_nearby_compiled_3(variable_map: dict[str, Any]): - distances = variable_map["distances"] - hotels = [hotel for dist, hotel in sorted(distances)[:5]] - return hotels - - -search_op = StatelessOperator({ - "search_nearby_compiled_0": search_nearby_compiled_0, - "search_nearby_compiled_1": search_nearby_compiled_1, - "search_nearby_compiled_2": search_nearby_compiled_2, - "search_nearby_compiled_3": search_nearby_compiled_3, -}, None) - -df = DataFlow("search_nearby") -n1 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_0")) -n2 = OpNode(Hotel, InvokeMethod("get_geo"), assign_result_to="hotel_geo", read_key_from="hotel_key") -n3 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_1"), is_conditional=True) -n4 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_2"), assign_result_to="search_body") -n5 = CollectNode("distances", "search_body") -n0 = SelectAllNode(Hotel, n5, assign_key_to="hotel_key") - -n6 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_3")) - -df.add_edge(Edge(n0, n1)) -df.add_edge(Edge(n1, n2)) -df.add_edge(Edge(n2, n3)) - -# if true make the body -df.add_edge(Edge(n3, n4, if_conditional=True)) -df.add_edge(Edge(n4, n5)) -# if false skip past -df.add_edge(Edge(n3, n5, if_conditional=False)) - -df.add_edge(Edge(n5, n6)) - -df.entry = n0 +from typing import Any +from cascade.dataflow.dataflow import CollectNode, DataFlow, Edge, InvokeMethod, OpNode, SelectAllNode, StatelessOpNode +from cascade.dataflow.operator import StatelessOperator +from deathstar_hotel_reservation.entities.hotel import Geo, Hotel, hotel_op + +# Stateless +class Search(): + # Get the 5 nearest hotels + @staticmethod + def nearby(lat: float, lon: float, in_date: int, out_date: int): + distances = [ + (dist, hotel) + for hotel in Hotel.__all__() + if (dist := hotel.geo.distance_km(lat, lon)) < 10] + hotels = [hotel for dist, hotel in sorted(distances)[:5]] + return hotels + + +#### COMPILED FUNCTIONS (ORACLE) ##### + + + +# predicate 1 +def search_nearby_compiled_0(variable_map: dict[str, Any]): + pass + +# predicate 2 +def search_nearby_compiled_1(variable_map: dict[str, Any]): + hotel_geo: Geo = variable_map["hotel_geo"] + lat, lon = variable_map["lat"], variable_map["lon"] + dist = hotel_geo.distance_km(lat, lon) + variable_map["dist"] = dist + return dist < 10 + + +# body +def search_nearby_compiled_2(variable_map: dict[str, Any]): + return (variable_map["dist"], variable_map["hotel_key"]) + +# next line +def search_nearby_compiled_3(variable_map: dict[str, Any]): + distances = variable_map["distances"] + hotels = [hotel for dist, hotel in sorted(distances)[:5]] + return hotels + + +search_op = StatelessOperator({ + "search_nearby_compiled_0": search_nearby_compiled_0, + "search_nearby_compiled_1": search_nearby_compiled_1, + "search_nearby_compiled_2": search_nearby_compiled_2, + "search_nearby_compiled_3": search_nearby_compiled_3, +}, None) + +df = DataFlow("search_nearby") +n1 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_0")) +n2 = OpNode(Hotel, InvokeMethod("get_geo"), assign_result_to="hotel_geo", read_key_from="hotel_key") +n3 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_1"), is_conditional=True) +n4 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_2"), assign_result_to="search_body") +n5 = CollectNode("distances", "search_body") +n0 = SelectAllNode(Hotel, n5, assign_key_to="hotel_key") + +n6 = StatelessOpNode(search_op, InvokeMethod("search_nearby_compiled_3")) + +df.add_edge(Edge(n0, n1)) +df.add_edge(Edge(n1, n2)) +df.add_edge(Edge(n2, n3)) + +# if true make the body +df.add_edge(Edge(n3, n4, if_conditional=True)) +df.add_edge(Edge(n4, n5)) +# if false skip past +df.add_edge(Edge(n3, n5, if_conditional=False)) + +df.add_edge(Edge(n5, n6)) + +df.entry = n0 search_op.dataflow = df \ No newline at end of file diff --git a/deathstar_hotel_reservation/entities/user.py b/deathstar_hotel_reservation/entities/user.py new file mode 100644 index 0000000..dba7567 --- /dev/null +++ b/deathstar_hotel_reservation/entities/user.py @@ -0,0 +1,105 @@ +from typing import Any +from cascade.dataflow.dataflow import CollectNode, CollectTarget, DataFlow, Edge, InvokeMethod, OpNode +from cascade.dataflow.operator import StatefulOperator +from deathstar_hotel_reservation.entities.flight import Flight, flight_op +from deathstar_hotel_reservation.entities.hotel import Hotel, hotel_op + + +class User(): + def __init__(self, user_id: str, password: str): + self.id = user_id + self.password = password + + def check(self, password): + return self.password == password + + def order(self, flight: Flight, hotel: Hotel): + if hotel.reserve() and flight.reserve(): + return True + else: + return False + +#### COMPILED FUNCTIONS (ORACLE) ##### + +def check_compiled(variable_map: dict[str, Any], state: User) -> Any: + return state.password == variable_map["password"] + +def order_compiled_entry_0(variable_map: dict[str, Any], state: User) -> Any: + pass + +def order_compiled_entry_1(variable_map: dict[str, Any], state: User) -> Any: + pass + +def order_compiled_if_cond(variable_map: dict[str, Any], state: User) -> Any: + return variable_map["hotel_reserve"] and variable_map["flight_reserve"] + +def order_compiled_if_cond_parallel(variable_map: dict[str, Any], state: User) -> Any: + return variable_map["reserves"][0] and variable_map["reserves"][1] + +def order_compiled_if_body(variable_map: dict[str, Any], state: User) -> Any: + return True + +def order_compiled_else_body(variable_map: dict[str, Any], state: User) -> Any: + return False + +user_op = StatefulOperator( + User, + { + "login": check_compiled, + "order_compiled_entry_0": order_compiled_entry_0, + "order_compiled_entry_1": order_compiled_entry_1, + # "order_compiled_if_cond": order_compiled_if_cond, + "order_compiled_if_cond": order_compiled_if_cond_parallel, + "order_compiled_if_body": order_compiled_if_body, + "order_compiled_else_body": order_compiled_else_body + }, + {} +) + +# For now, the dataflow will be serial instead of parallel. Future optimizations +# will try to automatically parallelize this. +# There is also no user entry (this could also be an optimization) +def df_serial(): + df = DataFlow("user_order") + n0 = OpNode(User, InvokeMethod("order_compiled_entry_0"), read_key_from="user_key") + n1 = OpNode(Hotel, InvokeMethod("reserve"), assign_result_to="hotel_reserve", read_key_from="hotel_key") + n2 = OpNode(User, InvokeMethod("order_compiled_entry_1"), read_key_from="user_key") + n3 = OpNode(Flight, InvokeMethod("reserve"), assign_result_to="flight_reserve", read_key_from="flight_key") + n4 = OpNode(User, InvokeMethod("order_compiled_if_cond"), is_conditional=True, read_key_from="user_key") + n5 = OpNode(User, InvokeMethod("order_compiled_if_body"), read_key_from="user_key") + n6 = OpNode(User, InvokeMethod("order_compiled_else_body"), read_key_from="user_key") + + df.add_edge(Edge(n0, n1)) + df.add_edge(Edge(n1, n2)) + df.add_edge(Edge(n2, n3)) + df.add_edge(Edge(n3, n4)) + df.add_edge(Edge(n4, n5, if_conditional=True)) + df.add_edge(Edge(n4, n6, if_conditional=False)) + + df.entry = n0 + return df + + +# PARALLEL DATAFLOW +def df_parallel(): + df = DataFlow("user_order") + n0 = OpNode(User, InvokeMethod("order_compiled_entry_0"), read_key_from="user_key") + ct = CollectNode(assign_result_to="reserves", read_results_from="reserve") + n1 = OpNode(Hotel, InvokeMethod("reserve"), assign_result_to="reserve", read_key_from="hotel_key", collect_target=CollectTarget(ct, 2, 0)) + n3 = OpNode(Flight, InvokeMethod("reserve"), assign_result_to="reserve", read_key_from="flight_key", collect_target=CollectTarget(ct, 2, 1)) + n4 = OpNode(User, InvokeMethod("order_compiled_if_cond"), is_conditional=True, read_key_from="user_key") + n5 = OpNode(User, InvokeMethod("order_compiled_if_body"), read_key_from="user_key") + n6 = OpNode(User, InvokeMethod("order_compiled_else_body"), read_key_from="user_key") + + df.add_edge(Edge(n0, n1)) + df.add_edge(Edge(n0, n3)) + df.add_edge(Edge(n1, ct)) + df.add_edge(Edge(n3, ct)) + df.add_edge(Edge(ct, n4)) + df.add_edge(Edge(n4, n5, if_conditional=True)) + df.add_edge(Edge(n4, n6, if_conditional=False)) + + df.entry = n0 + return df + +user_op.dataflows["order"] = df_serial() diff --git a/deathstar/test_demo.py b/deathstar_hotel_reservation/test_demo.py similarity index 92% rename from deathstar/test_demo.py rename to deathstar_hotel_reservation/test_demo.py index 751c9ed..dea227f 100644 --- a/deathstar/test_demo.py +++ b/deathstar_hotel_reservation/test_demo.py @@ -1,100 +1,100 @@ - -import os -import sys - -# import cascade -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) - -from cascade.runtime.python_runtime import PythonClientSync, PythonRuntime -from cascade.runtime.flink_runtime import FlinkClientSync, FlinkRuntime -from deathstar.demo import DeathstarDemo, recommend, reserve, search_hotel, user_login -import time -import pytest - -@pytest.mark.integration -def test_deathstar_demo(): - ds = DeathstarDemo() - ds.init_runtime(FlinkRuntime("deathstardemo-test", "dsd-out")) - ds.runtime.run(run_async=True) - print("Populating, press enter to go to the next step when done") - ds.populate() - - client = FlinkClientSync("deathstardemo-test", "dsd-out") - input() - print("testing user login") - event = user_login() - client.send(event) - - input() - print("testing reserve") - event = reserve() - client.send(event) - - input() - print("testing search") - event = search_hotel() - client.send(event) - - input() - print("testing recommend (distance)") - time.sleep(0.5) - event = recommend(req_param="distance") - client.send(event) - - input() - print("testing recommend (price)") - time.sleep(0.5) - event = recommend(req_param="price") - client.send(event) - - print(client._futures) - input() - print("done!") - print(client._futures) - -def test_deathstar_demo_python(): - ds = DeathstarDemo() - ds.init_runtime(PythonRuntime()) - ds.runtime.run() - print("Populating, press enter to go to the next step when done") - ds.populate() - - time.sleep(0.1) - - client = PythonClientSync(ds.runtime) - print("testing user login") - event = user_login() - result = client.send(event) - assert result == True - event = user_login(succesfull=False) - result = client.send(event) - assert result == False - - print("testing reserve") - event = reserve() - result = client.send(event) - assert result == True - - return - print("testing search") - event = search_hotel() - result = client.send(event) - print(result) - - print("testing recommend (distance)") - time.sleep(0.5) - event = recommend(req_param="distance") - result = client.send(event) - print(result) - - print("testing recommend (price)") - time.sleep(0.5) - event = recommend(req_param="price") - result = client.send(event) - print(result) - - print("done!") - - -if __name__ == "__main__": + +import os +import sys + +# import cascade +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from cascade.runtime.python_runtime import PythonClientSync, PythonRuntime +from cascade.runtime.flink_runtime import FlinkClientSync, FlinkRuntime +from deathstar_hotel_reservation.demo import DeathstarDemo, recommend, reserve, search_hotel, user_login +import time +import pytest + +@pytest.mark.integration +def test_deathstar_demo(): + ds = DeathstarDemo() + ds.init_runtime(FlinkRuntime("deathstardemo-test", "dsd-out")) + ds.runtime.run(run_async=True) + print("Populating, press enter to go to the next step when done") + ds.populate() + + client = FlinkClientSync("deathstardemo-test", "dsd-out") + input() + print("testing user login") + event = user_login() + client.send(event) + + input() + print("testing reserve") + event = reserve() + client.send(event) + + input() + print("testing search") + event = search_hotel() + client.send(event) + + input() + print("testing recommend (distance)") + time.sleep(0.5) + event = recommend(req_param="distance") + client.send(event) + + input() + print("testing recommend (price)") + time.sleep(0.5) + event = recommend(req_param="price") + client.send(event) + + print(client._futures) + input() + print("done!") + print(client._futures) + +def test_deathstar_demo_python(): + ds = DeathstarDemo() + ds.init_runtime(PythonRuntime()) + ds.runtime.run() + print("Populating, press enter to go to the next step when done") + ds.populate() + + time.sleep(0.1) + + client = PythonClientSync(ds.runtime) + print("testing user login") + event = user_login() + result = client.send(event) + assert result == True + event = user_login(succesfull=False) + result = client.send(event) + assert result == False + + print("testing reserve") + event = reserve() + result = client.send(event) + assert result == True + + return + print("testing search") + event = search_hotel() + result = client.send(event) + print(result) + + print("testing recommend (distance)") + time.sleep(0.5) + event = recommend(req_param="distance") + result = client.send(event) + print(result) + + print("testing recommend (price)") + time.sleep(0.5) + event = recommend(req_param="price") + result = client.send(event) + print(result) + + print("done!") + + +if __name__ == "__main__": test_deathstar_demo() \ No newline at end of file diff --git a/deathstar_movie_review/__init__.py b/deathstar_movie_review/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deathstar_movie_review/demo.py b/deathstar_movie_review/demo.py new file mode 100644 index 0000000..6e546e7 --- /dev/null +++ b/deathstar_movie_review/demo.py @@ -0,0 +1,224 @@ +import hashlib +import uuid + +from .movie_data import movie_data +from .workload_data import movie_titles, charset +import random +from timeit import default_timer as timer +import sys +import os + +# import cascade +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from cascade.dataflow.dataflow import Event, EventResult, InitClass, OpNode +from cascade.runtime.flink_runtime import FlinkClientSync, FlinkRuntime +from cascade.dataflow.optimization.dead_node_elim import dead_node_elimination + +from .entities.user import user_op, User +from .entities.compose_review import compose_review_op +from .entities.frontend import frontend_op, text_op, unique_id_op +from .entities.movie import MovieInfo, movie_id_op, movie_info_op, plot_op, Plot, MovieId + +import time +import pandas as pd + +def populate_user(client: FlinkRuntime): + init_user = OpNode(User, InitClass(), read_key_from="username") + for i in range(1000): + user_id = f'user{i}' + username = f'username_{i}' + password = f'password_{i}' + hasher = hashlib.new('sha512') + salt = uuid.uuid1().bytes + hasher.update(password.encode()) + hasher.update(salt) + + password_hash = hasher.hexdigest() + + user_data = { + "userId": user_id, + "FirstName": "firstname", + "LastName": "lastname", + "Username": username, + "Password": password_hash, + "Salt": salt + } + event = Event(init_user, {"username": username, "user_data": user_data}, None) + client.send(event) + + +def populate_movie(client: FlinkRuntime): + init_movie_info = OpNode(MovieInfo, InitClass(), read_key_from="movie_id") + init_plot = OpNode(Plot, InitClass(), read_key_from="movie_id") + init_movie_id = OpNode(MovieId, InitClass(), read_key_from="title") + + for movie in movie_data: + movie_id = movie["MovieId"] + + # movie info -> write `movie` + event = Event(init_movie_info, {"movie_id": movie_id, "info": movie}, None) + client.send(event) + + # plot -> write "plot" + event = Event(init_plot, {"movie_id": movie_id, "plot": "plot"}, None) + client.send(event) + + # movie_id_op -> register movie id + event = Event(init_movie_id, {"title": movie["Title"], "movie_id": movie_id}, None) + client.send(event) + + +def compose_review(req_id): + user_index = random.randint(0, 999) + username = f"username_{user_index}" + password = f"password_{user_index}" + title = random.choice(movie_titles) + rating = random.randint(0, 10) + text = ''.join(random.choice(charset) for _ in range(256)) + + return frontend_op.dataflow.generate_event({ + "review": req_id, + "user": username, + "title": title, + "rating": rating, + "text": text + }) + +def deathstar_workload_generator(): + c = 1 + while True: + yield compose_review(c) + c += 1 + +threads = 1 +messages_per_burst = 10 +sleeps_per_burst = 10 +sleep_time = 0.08 #0.0085 +seconds_per_burst = 1 +bursts = 100 + + +def benchmark_runner(proc_num) -> dict[int, dict]: + print(f'Generator: {proc_num} starting') + client = FlinkClientSync("ds-movie-in", "ds-movie-out") + deathstar_generator = deathstar_workload_generator() + start = timer() + + for _ in range(bursts): + sec_start = timer() + + # send burst of messages + for i in range(messages_per_burst): + + # sleep sometimes between messages + if i % (messages_per_burst // sleeps_per_burst) == 0: + time.sleep(sleep_time) + event = next(deathstar_generator) + client.send(event) + + client.flush() + sec_end = timer() + + # wait out the second + lps = sec_end - sec_start + if lps < seconds_per_burst: + time.sleep(1 - lps) + sec_end2 = timer() + print(f'Latency per burst: {sec_end2 - sec_start} ({seconds_per_burst})') + + end = timer() + print(f'Average latency per burst: {(end - start) / bursts} ({seconds_per_burst})') + + done = False + while not done: + done = True + for event_id, fut in client._futures.items(): + result = fut["ret"] + if result is None: + done = False + time.sleep(0.5) + break + futures = client._futures + client.close() + return futures + + +def write_dict_to_pkl(futures_dict, filename): + """ + Writes a dictionary of event data to a pickle file. + + Args: + futures_dict (dict): A dictionary where each key is an event ID and the value is another dict. + filename (str): The name of the pickle file to write to. + """ + + # Prepare the data for the DataFrame + data = [] + for event_id, event_data in futures_dict.items(): + ret: EventResult = event_data.get("ret") + row = { + "event_id": event_id, + "sent": str(event_data.get("sent")), + "sent_t": event_data.get("sent_t"), + "ret": str(event_data.get("ret")), + "ret_t": event_data.get("ret_t"), + "roundtrip": ret.metadata["roundtrip"] if ret else None, + "flink_time": ret.metadata["flink_time"] if ret else None, + "deser_times": ret.metadata["deser_times"] if ret else None, + "loops": ret.metadata["loops"] if ret else None, + "latency": event_data["ret_t"][1] - event_data["sent_t"][1] if ret else None + } + data.append(row) + + # Create a DataFrame and save it as a pickle file + df = pd.DataFrame(data) + df.to_pickle(filename) + +def main(): + runtime = FlinkRuntime("ds-movie-in", "ds-movie-out", 8081) + runtime.init(bundle_time=5, bundle_size=10) + + print(frontend_op.dataflow.to_dot()) + # dead_node_elimination([], [frontend_op]) + print(frontend_op.dataflow.to_dot()) + input() + + + runtime.add_operator(compose_review_op) + runtime.add_operator(user_op) + runtime.add_operator(movie_info_op) + runtime.add_operator(movie_id_op) + runtime.add_operator(plot_op) + runtime.add_stateless_operator(frontend_op) + runtime.add_stateless_operator(unique_id_op) + runtime.add_stateless_operator(text_op) + + runtime.run(run_async=True) + populate_user(runtime) + populate_movie(runtime) + runtime.producer.flush() + time.sleep(1) + + input() + + # with Pool(threads) as p: + # results = p.map(benchmark_runner, range(threads)) + + # results = {k: v for d in results for k, v in d.items()} + results = benchmark_runner(0) + + print("last result:") + print(list(results.values())[-1]) + t = len(results) + r = 0 + for result in results.values(): + if result["ret"] is not None: + print(result) + r += 1 + print(f"{r}/{t} results recieved.") + write_dict_to_pkl(results, "test2.pkl") + +if __name__ == "__main__": + main() + diff --git a/deathstar_movie_review/entities/__init__.py b/deathstar_movie_review/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deathstar_movie_review/entities/compose_review.py b/deathstar_movie_review/entities/compose_review.py new file mode 100644 index 0000000..7423c4b --- /dev/null +++ b/deathstar_movie_review/entities/compose_review.py @@ -0,0 +1,59 @@ +from typing import Any + +from src.cascade.dataflow.operator import StatefulOperator + + +class ComposeReview: + def __init__(self, req_id: str, *args): # *args is a temporary hack to allow for creation of composereview on the fly + self.req_id = req_id + self.review_data = {} + + def upload_unique_id(self, review_id: int): + self.review_data["review_id"] = review_id + + # could use the User class instead? + def upload_user_id(self, user_id: str): + self.review_data["userId"] = user_id + + def upload_movie_id(self, movie_id: str): + self.review_data["movieId"] = movie_id + + def upload_rating(self, rating: int): + self.review_data["rating"] = rating + + def upload_text(self, text: str): + self.review_data["text"] = text + + def get_data(self): + return self.review_data + +def upload_unique_id_compiled(variable_map: dict[str, Any], state: ComposeReview) -> Any: + state.review_data["review_id"] = variable_map["review_id"] + +def upload_user_id_compiled(variable_map: dict[str, Any], state: ComposeReview) -> Any: + state.review_data["userId"] = variable_map["user_id"] + +def upload_movie_id_compiled(variable_map: dict[str, Any], state: ComposeReview) -> Any: + state.review_data["movieId"] = variable_map["movie_id"] + +def upload_rating_compiled(variable_map: dict[str, Any], state: ComposeReview) -> Any: + state.review_data["rating"] = variable_map["rating"] + +def upload_text_compiled(variable_map: dict[str, Any], state: ComposeReview) -> Any: + state.review_data["text"] = variable_map["text"] + +def get_data_compiled(variable_map: dict[str, Any], state: ComposeReview) -> Any: + return state.review_data + +compose_review_op = StatefulOperator( + ComposeReview, + { + "upload_unique_id": upload_unique_id_compiled, + "upload_user_id": upload_user_id_compiled, + "upload_movie_id": upload_movie_id_compiled, + "upload_rating": upload_rating_compiled, + "upload_text": upload_text_compiled, + "get_data": get_data_compiled, + }, + {} +) \ No newline at end of file diff --git a/deathstar_movie_review/entities/frontend.py b/deathstar_movie_review/entities/frontend.py new file mode 100644 index 0000000..db75bc2 --- /dev/null +++ b/deathstar_movie_review/entities/frontend.py @@ -0,0 +1,188 @@ +from typing import Any +import uuid + +from cascade.dataflow.dataflow import CollectNode, CollectTarget, DataFlow, Edge, InvokeMethod, OpNode, StatelessOpNode +from cascade.dataflow.operator import StatelessOperator +from deathstar_movie_review.entities.compose_review import ComposeReview +from deathstar_movie_review.entities.movie import MovieId +from deathstar_movie_review.entities.user import User + + +# unique_id is stateless +class UniqueId(): + @staticmethod + def upload_unique_id_2(review: ComposeReview): + review_id = uuid.uuid1().int >> 64 + review.upload_unique_id(review_id) + +# text is stateless +class Text(): + @staticmethod + def upload_text_2(review: ComposeReview, text: str): + review.upload_text(text) + +CHAR_LIMIT = 50 + +# frontend is made stateless +class Frontend(): + @staticmethod + def compose(review: ComposeReview, user: User, title: MovieId, rating: int, text: str): + + # dead node elimination will remove "returning back" to the original function + # + # cascade could theoritically allow for more advanced analysis, + # that would enable all these to run in parallel. However, this is only + # possible because + # 1. the individual functions don't depend on each other + # 2. the ordering of side-effects does not matter + UniqueId.upload_unique_id_2(review) + user.upload_user(review) + title.upload_movie(review, rating) + + text = text[:CHAR_LIMIT] # an operation like this could be reorderd for better efficiency! + Text.upload_text_2(review, text) + +###### COMPILED FUNCTIONS ###### + +### UPLOAD UNIQUE ### + +def upload_unique_compiled_0(variable_map: dict[str, Any]): + variable_map["review_id"] = uuid.uuid1().int >> 64 + +unique_id_op = StatelessOperator( + { + "upload_unique": upload_unique_compiled_0, + }, + None +) + +df = DataFlow("upload_unique_id") +n0 = StatelessOpNode(unique_id_op, InvokeMethod("upload_unique")) +n1 = OpNode(ComposeReview, InvokeMethod("upload_unique_id"), read_key_from="review") +df.entry = n0 +unique_id_op.dataflow = df + +### TEXT ### + +text_op = StatelessOperator( + {}, + None +) + +df = DataFlow("upload_text") +n0 = OpNode(ComposeReview, InvokeMethod("upload_text"), read_key_from="review") +df.entry = n0 +text_op.dataflow = df + +### FRONTEND ### + +def compose_compiled_0(variable_map: dict[str, Any]): + pass + + +frontend_op = StatelessOperator( + { + "empty": compose_compiled_0, + }, + None +) + +def frontend_df_serial(): + # This dataflow calls many other dataflows. + # It could be more useful to have a "Dataflow" node + df = DataFlow("compose") + n0 = StatelessOpNode(frontend_op, InvokeMethod("empty")) + + # Upload Unique DF + n1_a = StatelessOpNode(unique_id_op, InvokeMethod("upload_unique")) + n1_b = OpNode(ComposeReview, InvokeMethod("upload_unique_id"), read_key_from="review") + + n2 = StatelessOpNode(frontend_op, InvokeMethod("empty")) + + # Upload User DF + n3_a = OpNode(User, InvokeMethod("upload_user_compiled_0"), read_key_from="user") + n3_b = OpNode(ComposeReview, InvokeMethod("upload_user_id"), read_key_from="review") + + n4 = StatelessOpNode(frontend_op, InvokeMethod("empty")) + + # Upload Movie DF + n5_a = OpNode(MovieId, InvokeMethod("upload_movie_cond"), read_key_from="title", is_conditional=True) + n5_b = OpNode(ComposeReview, InvokeMethod("upload_movie_id"), read_key_from="review") + n5_c = OpNode(ComposeReview, InvokeMethod("upload_rating"), read_key_from="review") + + n6 = StatelessOpNode(frontend_op, InvokeMethod("empty")) + + # Upload Text DF + n7 = OpNode(ComposeReview, InvokeMethod("upload_text"), read_key_from="review") + + n8 = StatelessOpNode(frontend_op, InvokeMethod("empty")) + + df.add_edge(Edge(n0, n1_a)) + df.add_edge(Edge(n1_a, n1_b)) + df.add_edge(Edge(n1_b, n2)) + + df.add_edge(Edge(n2, n3_a)) + df.add_edge(Edge(n3_a, n3_b)) + df.add_edge(Edge(n3_b, n4)) + + df.add_edge(Edge(n4, n5_a)) + df.add_edge(Edge(n5_a, n5_b, if_conditional=True)) + df.add_edge(Edge(n5_a, n5_c, if_conditional=False)) + df.add_edge(Edge(n5_b, n6)) + df.add_edge(Edge(n5_c, n6)) + + df.add_edge(Edge(n6, n7)) + df.add_edge(Edge(n7, n8)) + + df.entry = n0 + return df + +def frontend_df_parallel(): + # This dataflow calls many other dataflows. + # It could be more useful to have a "Dataflow" node + df = DataFlow("compose") + # n0 = StatelessOpNode(frontend_op, InvokeMethod("empty")) + ct = CollectNode(assign_result_to="results", read_results_from="dummy") + + # Upload Unique DF + n1_a = StatelessOpNode(unique_id_op, InvokeMethod("upload_unique")) + n1_b = OpNode(ComposeReview, InvokeMethod("upload_unique_id"), read_key_from="review", collect_target=CollectTarget(ct, 4, 0)) + + + # Upload User DF + n3_a = OpNode(User, InvokeMethod("upload_user_compiled_0"), read_key_from="user") + n3_b = OpNode(ComposeReview, InvokeMethod("upload_user_id"), read_key_from="review", collect_target=CollectTarget(ct, 4, 1)) + + + # Upload Movie DF + n5_a = OpNode(MovieId, InvokeMethod("upload_movie_cond"), read_key_from="title", is_conditional=True) + n5_b = OpNode(ComposeReview, InvokeMethod("upload_movie_id"), read_key_from="review", collect_target=CollectTarget(ct, 4, 2)) + n5_c = OpNode(ComposeReview, InvokeMethod("upload_rating"), read_key_from="review", collect_target=CollectTarget(ct, 4, 2)) + + + # Upload Text DF + n7 = OpNode(ComposeReview, InvokeMethod("upload_text"), read_key_from="review",collect_target=CollectTarget(ct, 4, 3)) + + + # df.add_edge(Edge(n0, n1_a)) + df.add_edge(Edge(n1_a, n1_b)) + df.add_edge(Edge(n1_b, ct)) + + # df.add_edge(Edge(n0, n3_a)) + df.add_edge(Edge(n3_a, n3_b)) + df.add_edge(Edge(n3_b, ct)) + + # df.add_edge(Edge(n0, n5_a)) + df.add_edge(Edge(n5_a, n5_b, if_conditional=True)) + df.add_edge(Edge(n5_a, n5_c, if_conditional=False)) + df.add_edge(Edge(n5_b, ct)) + df.add_edge(Edge(n5_c, ct)) + + # df.add_edge(Edge(n0, n7)) + df.add_edge(Edge(n7, ct)) + + df.entry = [n1_a, n3_a, n5_a, n7] + return df + +frontend_op.dataflow = frontend_df_parallel() + diff --git a/deathstar_movie_review/entities/movie.py b/deathstar_movie_review/entities/movie.py new file mode 100644 index 0000000..ade4d19 --- /dev/null +++ b/deathstar_movie_review/entities/movie.py @@ -0,0 +1,72 @@ +from typing import Any +from cascade.dataflow.dataflow import DataFlow, Edge, InvokeMethod, OpNode +from cascade.dataflow.operator import StatefulOperator +from deathstar_movie_review.entities.compose_review import ComposeReview +from deathstar_movie_review.entities.user import User + + +class MovieId: + # key: 'title' + def __init__(self, title: str, movie_id: str): + self.title = title + self.movie_id = movie_id + + def upload_movie(self, review: ComposeReview, rating: int): + if self.movie_id is not None: + review.upload_movie_id(self.movie_id) + else: + review.upload_rating(rating) + + +def upload_movie_compiled_cond_0(variable_map: dict[str, Any], state: MovieId) -> Any: + variable_map["movie_id"] = state.movie_id # SSA + return variable_map["movie_id"] is not None + +movie_id_op = StatefulOperator( + MovieId, + { + "upload_movie_cond": upload_movie_compiled_cond_0 + }, + {} +) + +def upload_movie_df(): + df = DataFlow("movieId_upload_movie") + n0 = OpNode(MovieId, InvokeMethod("upload_movie_cond"), read_key_from="title", is_conditional=True) + n1 = OpNode(ComposeReview, InvokeMethod("upload_movie_id"), read_key_from="review") + n2 = OpNode(ComposeReview, InvokeMethod("upload_rating"), read_key_from="review") + + df.add_edge(Edge(n0, n1, if_conditional=True)) + df.add_edge(Edge(n0, n2, if_conditional=False)) + df.entry = n0 + return df + +movie_id_op.dataflows["upload_movie"] = upload_movie_df() + + + +### Other movie-related operators + +# key: movie_id + +class Plot: + def __init__(self, movie_id: str, plot: str): + self.movie_id = movie_id + self.plot = plot + +class MovieInfo: + def __init__(self, movie_id: str, info: dict): + self.movie_id = movie_id + self.info = info + +movie_info_op = StatefulOperator( + MovieInfo, + {}, + {} +) + +plot_op = StatefulOperator( + Plot, + {}, + {} +) \ No newline at end of file diff --git a/deathstar_movie_review/entities/user.py b/deathstar_movie_review/entities/user.py new file mode 100644 index 0000000..2b244a9 --- /dev/null +++ b/deathstar_movie_review/entities/user.py @@ -0,0 +1,36 @@ +from typing import Any +from deathstar_movie_review.entities.compose_review import ComposeReview +from src.cascade.dataflow.dataflow import DataFlow, Edge, InvokeMethod, OpNode +from src.cascade.dataflow.operator import StatefulOperator + + +class User: + def __init__(self, username: str, user_data: dict): + self.username = username + self.user_data = user_data + + def upload_user(self, review: ComposeReview): + review.upload_user_id(self.user_data["userId"]) + + +def upload_user_compiled_0(variable_map: dict[str, Any], state: User) -> Any: + variable_map["user_id"] = state.user_data["userId"] + +user_op = StatefulOperator( + User, + { + "upload_user_compiled_0": upload_user_compiled_0, + }, + {} +) + +def upload_df(): + df = DataFlow("user_upload_user") + n0 = OpNode(User, InvokeMethod("upload_user_compiled_0"), read_key_from="username") + n1 = OpNode(ComposeReview, InvokeMethod("upload_user_id"), read_key_from="review") + + df.add_edge(Edge(n0, n1)) + df.entry = n0 + return df + +user_op.dataflows["upload_user"] = upload_df() \ No newline at end of file diff --git a/deathstar_movie_review/movie_data.py b/deathstar_movie_review/movie_data.py new file mode 100644 index 0000000..a25f23f --- /dev/null +++ b/deathstar_movie_review/movie_data.py @@ -0,0 +1,2571 @@ +movie_data = [{"MovieId": "299534", "Title": "Avengers: Endgame", "Casts": [], "PlotId": "299534", + "ThumbnailIds": ["/or06FN3Dka5tukK1e9sl16pB3iy.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.6, + "NumRating": 4789}, + {"MovieId": "543103", "Title": "Kamen Rider Heisei Generations FOREVER", "Casts": [], "PlotId": "543103", + "ThumbnailIds": ["/6sOFQDlkY6El1B2P5gklzJfVdsT.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.8, + "NumRating": 12}, {"MovieId": "299537", "Title": "Captain Marvel", "Casts": [], "PlotId": "299537", + "ThumbnailIds": ["/AtsgWhDnHTq68L0lLsUrCnM7TjG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 4726}, + {"MovieId": "447404", "Title": "Pok\u00e9mon Detective Pikachu", "Casts": [], "PlotId": "447404", + "ThumbnailIds": ["/wgQ7APnFpf1TuviKHXeEe3KnsTV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 120}, {"MovieId": "456740", "Title": "Hellboy", "Casts": [], "PlotId": "456740", + "ThumbnailIds": ["/bk8LyaMqUtaQ9hUShuvFznQYQKR.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.2, "NumRating": 320}, + {"MovieId": "537915", "Title": "After", "Casts": [], "PlotId": "537915", + "ThumbnailIds": ["/u3B2YKUjWABcxXZ6Nm9h10hLUbh.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 819}, + {"MovieId": "299536", "Title": "Avengers: Infinity War", "Casts": [], "PlotId": "299536", + "ThumbnailIds": ["/7WsyChQLEftFiDOVTGkv3hFpyyt.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.3, + "NumRating": 13379}, + {"MovieId": "495925", "Title": "Doraemon the Movie: Nobita's Treasure Island", "Casts": [], + "PlotId": "495925", "ThumbnailIds": ["/cmJ71gdZxCqkMUvGwWgSg3MK7pC.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 16}, + {"MovieId": "438650", "Title": "Cold Pursuit", "Casts": [], "PlotId": "438650", + "ThumbnailIds": ["/otK0H9H1w3JVGJjad5Kzx3Z9kt2.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.2, + "NumRating": 445}, {"MovieId": "287947", "Title": "Shazam!", "Casts": [], "PlotId": "287947", + "ThumbnailIds": ["/xnopI5Xtky18MPhK40cZAGAOVeV.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 1442}, + {"MovieId": "487297", "Title": "What Men Want", "Casts": [], "PlotId": "487297", + "ThumbnailIds": ["/30IiwvIRqPGjUV0bxJkZfnSiCL.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 222}, {"MovieId": "450465", "Title": "Glass", "Casts": [], "PlotId": "450465", + "ThumbnailIds": ["/svIDTNUoajS8dLEo7EosxvyAsgJ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 2695}, + {"MovieId": "24428", "Title": "The Avengers", "Casts": [], "PlotId": "24428", + "ThumbnailIds": ["/cezWGskPY5x7GaglTTRN4Fugfb8.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 19269}, {"MovieId": "284054", "Title": "Black Panther", "Casts": [], "PlotId": "284054", + "ThumbnailIds": ["/uxzzxijgPIY7slzFvMotPv8wjKA.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.4, "NumRating": 11967}, + {"MovieId": "166428", "Title": "How to Train Your Dragon: The Hidden World", "Casts": [], + "PlotId": "166428", "ThumbnailIds": ["/xvx4Yhf0DVH8G4LzNISpMfFBDy2.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.6, "NumRating": 1718}, + {"MovieId": "468224", "Title": "Tolkien", "Casts": [], "PlotId": "468224", + "ThumbnailIds": ["/5ElXRKi773koW0mAnRjpNTpgZXZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.9, + "NumRating": 8}, + {"MovieId": "445629", "Title": "Fighting with My Family", "Casts": [], "PlotId": "445629", + "ThumbnailIds": ["/3TZCBAdKQiz0cGKGEjZiyZUA01O.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 174}, + {"MovieId": "283995", "Title": "Guardians of the Galaxy Vol. 2", "Casts": [], "PlotId": "283995", + "ThumbnailIds": ["/y4MBh0EjBlMuOzv9axM4qJlmhzz.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 12212}, + {"MovieId": "99861", "Title": "Avengers: Age of Ultron", "Casts": [], "PlotId": "99861", + "ThumbnailIds": ["/t90Y3G8UGQp0f0DrP60wRu9gfrH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 13126}, {"MovieId": "532671", "Title": "The Prodigy", "Casts": [], "PlotId": "532671", + "ThumbnailIds": ["/yyejodyk3lWncVjVhhrEkPctY9o.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.9, "NumRating": 161}, + {"MovieId": "920", "Title": "Cars", "Casts": [], "PlotId": "920", + "ThumbnailIds": ["/5damnMcRFKSjhCirgX3CMa88MBj.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 7366}, {"MovieId": "284053", "Title": "Thor: Ragnarok", "Casts": [], "PlotId": "284053", + "ThumbnailIds": ["/rzRwTcFvttcN1ZpX2xv4j3tSdJu.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.5, "NumRating": 11089}, + {"MovieId": "535167", "Title": "The Wandering Earth", "Casts": [], "PlotId": "535167", + "ThumbnailIds": ["/yzqpJcJT79CLTNdZVU7HHee6L3a.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 101}, + {"MovieId": "457799", "Title": "Extremely Wicked, Shockingly Evil and Vile", "Casts": [], + "PlotId": "457799", "ThumbnailIds": ["/zSuJ3r5zr5T26tTxyygHhgkUAIM.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 272}, + {"MovieId": "157433", "Title": "Pet Sematary", "Casts": [], "PlotId": "157433", + "ThumbnailIds": ["/7SPhr7Qj39vbnfF9O2qHRYaKHAL.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 383}, {"MovieId": "263115", "Title": "Logan", "Casts": [], "PlotId": "263115", + "ThumbnailIds": ["/gGBu0hKw9BGddG8RkRAMX7B6NDB.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.8, "NumRating": 12092}, + {"MovieId": "118340", "Title": "Guardians of the Galaxy", "Casts": [], "PlotId": "118340", + "ThumbnailIds": ["/y31QB9kn3XSudA15tV7UWQ9XLuW.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 17578}, + {"MovieId": "576393", "Title": "Fall in Love at First Kiss", "Casts": [], "PlotId": "576393", + "ThumbnailIds": ["/wtaSH8MfJSCEIrrEX9SQuHdU5sl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 20}, + {"MovieId": "324857", "Title": "Spider-Man: Into the Spider-Verse", "Casts": [], "PlotId": "324857", + "ThumbnailIds": ["/iiZZdoQBEYBv6id8su7ImL0oCbD.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.4, + "NumRating": 3649}, {"MovieId": "424783", "Title": "Bumblebee", "Casts": [], "PlotId": "424783", + "ThumbnailIds": ["/fw02ONlDhrYjTSZV8XO6hhU3ds3.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.4, "NumRating": 2221}, + {"MovieId": "10195", "Title": "Thor", "Casts": [], "PlotId": "10195", + "ThumbnailIds": ["/9zDwvsISU8bR15R2yN3kh1lfqve.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 12476}, {"MovieId": "297802", "Title": "Aquaman", "Casts": [], "PlotId": "297802", + "ThumbnailIds": ["/5Kg76ldv7VxeX9YlcQXiowHgdX6.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.8, "NumRating": 5783}, + {"MovieId": "363088", "Title": "Ant-Man and the Wasp", "Casts": [], "PlotId": "363088", + "ThumbnailIds": ["/eivQmS3wqzqnQWILHLc4FsEfcXP.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 5977}, + {"MovieId": "280217", "Title": "The Lego Movie 2: The Second Part", "Casts": [], "PlotId": "280217", + "ThumbnailIds": ["/QTESAsBVZwjtGJNDP7utiGV37z.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 459}, {"MovieId": "137113", "Title": "Edge of Tomorrow", "Casts": [], "PlotId": "137113", + "ThumbnailIds": ["/tpoVEYvm6qcXueZrQYJNRLXL88s.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.6, "NumRating": 7924}, + {"MovieId": "449562", "Title": "The Hustle", "Casts": [], "PlotId": "449562", + "ThumbnailIds": ["/qibqW5Dnvqp4hcEnoTARbQgxwJy.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 11}, {"MovieId": "329996", "Title": "Dumbo", "Casts": [], "PlotId": "329996", + "ThumbnailIds": ["/279PwJAcelI4VuBtdzrZASqDPQr.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 1102}, + {"MovieId": "485811", "Title": "Redcon-1", "Casts": [], "PlotId": "485811", + "ThumbnailIds": ["/jOYUbe61DQiY628inVkR1KERS30.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.2, + "NumRating": 31}, {"MovieId": "491418", "Title": "Instant Family", "Casts": [], "PlotId": "491418", + "ThumbnailIds": ["/dic3GdmMpxxfkCQfvZnasb5ZkSG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 690}, + {"MovieId": "315635", "Title": "Spider-Man: Homecoming", "Casts": [], "PlotId": "315635", + "ThumbnailIds": ["/kY2c7wKgOfQjvbqe7yVzLTYkxJO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 11238}, + {"MovieId": "122917", "Title": "The Hobbit: The Battle of the Five Armies", "Casts": [], + "PlotId": "122917", "ThumbnailIds": ["/9zRzFJuaj0CHIOhAkcCcFTvyu2X.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 8241}, + {"MovieId": "526050", "Title": "Little", "Casts": [], "PlotId": "526050", + "ThumbnailIds": ["/4MDB6jJl3U7xK1Gw64zIqt9pQA4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 38}, + {"MovieId": "480414", "Title": "The Curse of La Llorona", "Casts": [], "PlotId": "480414", + "ThumbnailIds": ["/jhZlXSnFUpNiLAek9EkPrtLEWQI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 243}, {"MovieId": "522681", "Title": "Escape Room", "Casts": [], "PlotId": "522681", + "ThumbnailIds": ["/8Ls1tZ6qjGzfGHjBB7ihOnf7f0b.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 1069}, + {"MovieId": "500852", "Title": "Miss Bala", "Casts": [], "PlotId": "500852", + "ThumbnailIds": ["/ae9yrSAS7nLZPbbkOm61pSuIqeo.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 52}, {"MovieId": "376865", "Title": "High Life", "Casts": [], "PlotId": "376865", + "ThumbnailIds": ["/wElOvH7H6sLElsTOLu1MY6oWRUx.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 105}, + {"MovieId": "500682", "Title": "The Highwaymen", "Casts": [], "PlotId": "500682", + "ThumbnailIds": ["/4bRYg4l12yDuJvAfqvUOPnBrxno.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 514}, + {"MovieId": "337339", "Title": "The Fate of the Furious", "Casts": [], "PlotId": "337339", + "ThumbnailIds": ["/dImWM7GJqryWJO9LHa3XQ8DD5NH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 6236}, {"MovieId": "1726", "Title": "Iron Man", "Casts": [], "PlotId": "1726", + "ThumbnailIds": ["/848chlIWVT41VtAAgyh9bWymAYb.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.5, "NumRating": 15386}, + {"MovieId": "1771", "Title": "Captain America: The First Avenger", "Casts": [], "PlotId": "1771", + "ThumbnailIds": ["/vSNxAJTlD0r02V9sPYpOjqDZXUK.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 12515}, + {"MovieId": "271110", "Title": "Captain America: Civil War", "Casts": [], "PlotId": "271110", + "ThumbnailIds": ["/kSBXou5Ac7vEqKd97wotJumyJvU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 13696}, {"MovieId": "458723", "Title": "Us", "Casts": [], "PlotId": "458723", + "ThumbnailIds": ["/ux2dU1jQ2ACIMShzB3yP93Udpzc.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.2, "NumRating": 1268}, + {"MovieId": "566555", "Title": "Detective Conan: The Fist of Blue Sapphire", "Casts": [], + "PlotId": "566555", "ThumbnailIds": ["/1GyvpwvgswOrHvxjnw2FBLNkTyo.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.0, "NumRating": 2}, + {"MovieId": "390634", "Title": "Fate/stay night: Heaven\u2019s Feel II. lost butterfly", "Casts": [], + "PlotId": "390634", "ThumbnailIds": ["/4tS0iyKQBDFqVpVcH21MSJwXZdq.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.7, "NumRating": 46}, + {"MovieId": "375588", "Title": "Robin Hood", "Casts": [], "PlotId": "375588", + "ThumbnailIds": ["/AiRfixFcfTkNbn2A73qVJPlpkUo.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 1095}, + {"MovieId": "338952", "Title": "Fantastic Beasts: The Crimes of Grindelwald", "Casts": [], + "PlotId": "338952", "ThumbnailIds": ["/fMMrl8fD9gRCFJvsx0SuFwkEOop.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 4636}, + {"MovieId": "76338", "Title": "Thor: The Dark World", "Casts": [], "PlotId": "76338", + "ThumbnailIds": ["/bnX5PqAdQZRXSw3aX3DutDcdso5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 9932}, + {"MovieId": "399579", "Title": "Alita: Battle Angel", "Casts": [], "PlotId": "399579", + "ThumbnailIds": ["/xRWht48C2V8XNfzvPehyClOvDni.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 1951}, {"MovieId": "512196", "Title": "Happy Death Day 2U", "Casts": [], "PlotId": "512196", + "ThumbnailIds": ["/4tdnePOkOOzwuGPEOAHp8UA4vqx.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.1, "NumRating": 714}, + {"MovieId": "245891", "Title": "John Wick", "Casts": [], "PlotId": "245891", + "ThumbnailIds": ["/b9uYMMbm87IBFOq59pppvkkkgNg.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 9589}, {"MovieId": "347375", "Title": "Mile 22", "Casts": [], "PlotId": "347375", + "ThumbnailIds": ["/2L8ehd95eSW9x7KINYtZmRkAlrZ.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.0, "NumRating": 882}, + {"MovieId": "10138", "Title": "Iron Man 2", "Casts": [], "PlotId": "10138", + "ThumbnailIds": ["/ArqpkNYGfcTIA6umWt6xihfIZZv.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 12070}, {"MovieId": "287424", "Title": "Maggie", "Casts": [], "PlotId": "287424", + "ThumbnailIds": ["/mFt7Oo3pf8f1BZAdWLyUpYM63aT.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.3, "NumRating": 947}, + {"MovieId": "335983", "Title": "Venom", "Casts": [], "PlotId": "335983", + "ThumbnailIds": ["/2uNW4WbgBXL25BAbXGLnLqX71Sw.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 5909}, + {"MovieId": "671", "Title": "Harry Potter and the Philosopher's Stone", "Casts": [], "PlotId": "671", + "ThumbnailIds": ["/dCtFvscYcXQKTNvyyaQr2g2UacJ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 13675}, + {"MovieId": "454294", "Title": "The Kid Who Would Be King", "Casts": [], "PlotId": "454294", + "ThumbnailIds": ["/kBuvLX6zynQP0sjyqbXV4jNaZ4E.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 160}, + {"MovieId": "22", "Title": "Pirates of the Caribbean: The Curse of the Black Pearl", "Casts": [], + "PlotId": "22", "ThumbnailIds": ["/tkt9xR1kNX5R9rCebASKck44si2.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.7, "NumRating": 12157}, + {"MovieId": "424694", "Title": "Bohemian Rhapsody", "Casts": [], "PlotId": "424694", + "ThumbnailIds": ["/lHu1wtNaczFPGFDTrjCSzeLPTKN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 7236}, {"MovieId": "504172", "Title": "The Mule", "Casts": [], "PlotId": "504172", + "ThumbnailIds": ["/oeZh7yEz3PMnZLgBPhrafFHRbVz.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.5, "NumRating": 1390}, + {"MovieId": "404368", "Title": "Ralph Breaks the Internet", "Casts": [], "PlotId": "404368", + "ThumbnailIds": ["/lvfIaThG5HA8THf76nghKinjjji.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 2406}, + {"MovieId": "120", "Title": "The Lord of the Rings: The Fellowship of the Ring", "Casts": [], + "PlotId": "120", "ThumbnailIds": ["/56zTpe2xvaA4alU51sRWPoKPYZy.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.3, "NumRating": 14253}, + {"MovieId": "395990", "Title": "Death Wish", "Casts": [], "PlotId": "395990", + "ThumbnailIds": ["/7FG13lLQcV9DC2Bhn0hjxc6AFXV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 1158}, + {"MovieId": "458156", "Title": "John Wick: Chapter 3 \u2013 Parabellum", "Casts": [], "PlotId": "458156", + "ThumbnailIds": ["/ziEuG1essDuWuC5lpWUaw1uXY2O.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "440161", "Title": "The Sisters Brothers", "Casts": [], "PlotId": "440161", + "ThumbnailIds": ["/7Tl2nZ6uvmxwK14Skbf9VFHEHpX.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 517}, + {"MovieId": "301351", "Title": "We Are Your Friends", "Casts": [], "PlotId": "301351", + "ThumbnailIds": ["/accc6f6h3Xi8kURvYpPoATOsm2Z.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 1218}, + {"MovieId": "579598", "Title": "Vaarikkuzhiyile Kolapathakam", "Casts": [], "PlotId": "579598", + "ThumbnailIds": ["/qwDA7qSSQLwQ7JgDmHrflHFyQZf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "442249", "Title": "The First Purge", "Casts": [], "PlotId": "442249", + "ThumbnailIds": ["/litjsBoiydO6JlO70uOX4N3WnNL.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 1778}, + {"MovieId": "218043", "Title": "Left Behind", "Casts": [], "PlotId": "218043", + "ThumbnailIds": ["/lWf8po2lGleVuzRB4lpHavVT1Lv.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 3.9, + "NumRating": 694}, {"MovieId": "383498", "Title": "Deadpool 2", "Casts": [], "PlotId": "383498", + "ThumbnailIds": ["/to0spRl1CMDvyUbOnbb4fTk3VAd.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 8451}, + {"MovieId": "487558", "Title": "BlacKkKlansman", "Casts": [], "PlotId": "487558", + "ThumbnailIds": ["/3ntR66u2SHZ2UA3r3DjF2Dl6Kwx.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 2892}, {"MovieId": "298250", "Title": "Jigsaw", "Casts": [], "PlotId": "298250", + "ThumbnailIds": ["/2mUqHJG7ZiGwZYIylczFCsRPbXM.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.1, "NumRating": 1837}, + {"MovieId": "454983", "Title": "The Kissing Booth", "Casts": [], "PlotId": "454983", + "ThumbnailIds": ["/7Dktk2ST6aL8h9Oe5rpk903VLhx.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 3269}, + {"MovieId": "263109", "Title": "Shaun the Sheep Movie", "Casts": [], "PlotId": "263109", + "ThumbnailIds": ["/aOHsNN1p2nuiF9WaMaCNXy0T80J.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 697}, + {"MovieId": "3512", "Title": "Under Siege 2: Dark Territory", "Casts": [], "PlotId": "3512", + "ThumbnailIds": ["/6Z1p71nkm45cYuIZWOx5JSCYc0o.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 358}, {"MovieId": "460019", "Title": "Truth or Dare", "Casts": [], "PlotId": "460019", + "ThumbnailIds": ["/kdkNaQYZ7dhM80LsnAGKpH8ca2g.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 1908}, + {"MovieId": "284052", "Title": "Doctor Strange", "Casts": [], "PlotId": "284052", + "ThumbnailIds": ["/4PiiNGXj1KENTmCBHeN6Mskj2Fq.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 12292}, + {"MovieId": "122", "Title": "The Lord of the Rings: The Return of the King", "Casts": [], "PlotId": "122", + "ThumbnailIds": ["/rCzpDGLbOoPwLjy3OAm5NUPOTrC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.4, + "NumRating": 12967}, {"MovieId": "1585", "Title": "It's a Wonderful Life", "Casts": [], "PlotId": "1585", + "ThumbnailIds": ["/rgj6QjdyCeDrO9KGt1kusGyhvb2.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 8.2, "NumRating": 1986}, + {"MovieId": "157336", "Title": "Interstellar", "Casts": [], "PlotId": "157336", + "ThumbnailIds": ["/nBNZadXqJSdt05SHLqgT0HuC5Gm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.2, + "NumRating": 18294}, + {"MovieId": "446021", "Title": "Bad Times at the El Royale", "Casts": [], "PlotId": "446021", + "ThumbnailIds": ["/iNtFgXqXPRMkm1QO8CHn5sHfUgE.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 1261}, + {"MovieId": "464504", "Title": "A Madea Family Funeral", "Casts": [], "PlotId": "464504", + "ThumbnailIds": ["/sFvOTUlZrIxCLdmz1fC16wK0lme.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 305}, {"MovieId": "459992", "Title": "Long Shot", "Casts": [], "PlotId": "459992", + "ThumbnailIds": ["/m2ttWZ8rMRwIMT7zA48Jo6mTkDS.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 35}, + {"MovieId": "361292", "Title": "Suspiria", "Casts": [], "PlotId": "361292", + "ThumbnailIds": ["/dzWTnkert9EoiPWldWJ15dnfAFl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 668}, {"MovieId": "155", "Title": "The Dark Knight", "Casts": [], "PlotId": "155", + "ThumbnailIds": ["/1hRoyzDtpgMU7Dz4JF22RANzQO7.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.4, "NumRating": 18634}, + {"MovieId": "627", "Title": "Trainspotting", "Casts": [], "PlotId": "627", + "ThumbnailIds": ["/p1O3eFsdb0GEIYu87xlwV7P4jM1.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 5228}, {"MovieId": "11", "Title": "Star Wars", "Casts": [], "PlotId": "11", + "ThumbnailIds": ["/btTdmkgIvOi0FFip1sPuZI2oQG6.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 8.2, "NumRating": 11282}, + {"MovieId": "428078", "Title": "Mortal Engines", "Casts": [], "PlotId": "428078", + "ThumbnailIds": ["/iteUvQKCW0EqNQrIVzZGJntYq9s.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 1597}, {"MovieId": "339877", "Title": "Loving Vincent", "Casts": [], "PlotId": "339877", + "ThumbnailIds": ["/56sq57kDm7XgyXBYrgJLumo0Jac.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 8.2, "NumRating": 1178}, + {"MovieId": "351286", "Title": "Jurassic World: Fallen Kingdom", "Casts": [], "PlotId": "351286", + "ThumbnailIds": ["/c9XxwwhPHdaImA2f1WEfEsbhaFB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 5800}, {"MovieId": "463684", "Title": "Velvet Buzzsaw", "Casts": [], "PlotId": "463684", + "ThumbnailIds": ["/3rViQPcrWthMNecp5XnkKev6BzW.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.3, "NumRating": 806}, + {"MovieId": "407451", "Title": "A Wrinkle in Time", "Casts": [], "PlotId": "407451", + "ThumbnailIds": ["/yAcb58vipewa1BfNit2RjE6boXA.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.0, + "NumRating": 1046}, + {"MovieId": "11430", "Title": "The Lion King 1\u00bd", "Casts": [], "PlotId": "11430", + "ThumbnailIds": ["/vDfaIoXTaQLUD5HVAv2hLIFKAcq.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 1547}, + {"MovieId": "429617", "Title": "Spider-Man: Far from Home", "Casts": [], "PlotId": "429617", + "ThumbnailIds": ["/q1ZcgXatgXo58tUO3vEsrJhYSbu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, + {"MovieId": "672", "Title": "Harry Potter and the Chamber of Secrets", "Casts": [], "PlotId": "672", + "ThumbnailIds": ["/sdEOH0992YZ0QSxgXNIGLq1ToUi.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 11563}, + {"MovieId": "348350", "Title": "Solo: A Star Wars Story", "Casts": [], "PlotId": "348350", + "ThumbnailIds": ["/3IGbjc5ZC5yxim5W0sFING2kdcz.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 3689}, {"MovieId": "502292", "Title": "The Last Summer", "Casts": [], "PlotId": "502292", + "ThumbnailIds": ["/roxH78GESToUjfd6Tc973jV0Wu7.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.1, "NumRating": 259}, + {"MovieId": "68721", "Title": "Iron Man 3", "Casts": [], "PlotId": "68721", + "ThumbnailIds": ["/7XiGqZE8meUv7L4720L0tIDd7gO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 13891}, {"MovieId": "489925", "Title": "Eighth Grade", "Casts": [], "PlotId": "489925", + "ThumbnailIds": ["/xTa9cLhGHfQ7084UvoPQ2bBXKqd.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.4, "NumRating": 480}, + {"MovieId": "460885", "Title": "Mandy", "Casts": [], "PlotId": "460885", + "ThumbnailIds": ["/m0yf7J7HsKeK6E81SMRcX8vx6mH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 591}, {"MovieId": "562", "Title": "Die Hard", "Casts": [], "PlotId": "562", + "ThumbnailIds": ["/mc7MubOLcIw3MDvnuQFrO9psfCa.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.7, "NumRating": 6137}, + {"MovieId": "12536", "Title": "Home Alone 4", "Casts": [], "PlotId": "12536", + "ThumbnailIds": ["/359bHILkiD6ZVCq6WoHSD0UuJQV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.2, + "NumRating": 513}, {"MovieId": "429300", "Title": "Adrift", "Casts": [], "PlotId": "429300", + "ThumbnailIds": ["/5gLDeADaETvwQlQow5szlyuhLbj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 1207}, + {"MovieId": "346910", "Title": "The Predator", "Casts": [], "PlotId": "346910", + "ThumbnailIds": ["/wMq9kQXTeQCHUZOG4fAe5cAxyUA.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.2, + "NumRating": 1934}, {"MovieId": "375262", "Title": "The Favourite", "Casts": [], "PlotId": "375262", + "ThumbnailIds": ["/cwBq0onfmeilU5xgqNNjJAMPfpw.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.7, "NumRating": 1925}, + {"MovieId": "438799", "Title": "Overlord", "Casts": [], "PlotId": "438799", + "ThumbnailIds": ["/l76Rgp32z2UxjULApxGXAPpYdAP.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 965}, {"MovieId": "244506", "Title": "Camp X-Ray", "Casts": [], "PlotId": "244506", + "ThumbnailIds": ["/oGcmIqOAbV8npgY57u7tqzwPgc.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 608}, + {"MovieId": "400650", "Title": "Mary Poppins Returns", "Casts": [], "PlotId": "400650", + "ThumbnailIds": ["/uTVGku4LibMGyKgQvjBtv3OYfAX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 1341}, + {"MovieId": "450001", "Title": "Master Z: Ip Man Legacy", "Casts": [], "PlotId": "450001", + "ThumbnailIds": ["/6VxEvOF7QDovsG6ro9OVyjH07LF.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 133}, + {"MovieId": "532321", "Title": "Re: Zero kara Hajimeru Isekai Seikatsu - Memory Snow", "Casts": [], + "PlotId": "532321", "ThumbnailIds": ["/xqR4ABkFTFYe8NDJi3knwWX7zfn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.9, "NumRating": 7}, + {"MovieId": "11024", "Title": "Scooby-Doo 2: Monsters Unleashed", "Casts": [], "PlotId": "11024", + "ThumbnailIds": ["/vEp6qw25qY2n03O9EaeiWNv89vb.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 1223}, + {"MovieId": "287948", "Title": "The Transporter Refueled", "Casts": [], "PlotId": "287948", + "ThumbnailIds": ["/zW7oC3tucYLzu77xNbPbYjXUN4o.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 824}, {"MovieId": "340676", "Title": "Personal Shopper", "Casts": [], "PlotId": "340676", + "ThumbnailIds": ["/jDOWYDmJvGkVkVHf7Ru66gy6ry8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 584}, + {"MovieId": "226857", "Title": "Endless Love", "Casts": [], "PlotId": "226857", + "ThumbnailIds": ["/wWkTnQosziIXEePOyunr53el8fe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 977}, {"MovieId": "102899", "Title": "Ant-Man", "Casts": [], "PlotId": "102899", + "ThumbnailIds": ["/D6e8RJf2qUstnfkTslTXNTUAlT.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 11671}, + {"MovieId": "483906", "Title": "Polar", "Casts": [], "PlotId": "483906", + "ThumbnailIds": ["/qOBEpKVLl8Q9CZScbOcRRVISezV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 781}, {"MovieId": "527261", "Title": "The Silence", "Casts": [], "PlotId": "527261", + "ThumbnailIds": ["/lTVOquzxw2DPF3MKuYd1ynz9F6H.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 526}, + {"MovieId": "411728", "Title": "The Professor and the Madman", "Casts": [], "PlotId": "411728", + "ThumbnailIds": ["/gtGCDLhfjW96qVarwctnuTpGOtD.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 88}, {"MovieId": "429351", "Title": "12 Strong", "Casts": [], "PlotId": "429351", + "ThumbnailIds": ["/j18021qCeRi3yUBtqd2UFj1c0RQ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.6, "NumRating": 1278}, + {"MovieId": "396806", "Title": "Anon", "Casts": [], "PlotId": "396806", + "ThumbnailIds": ["/xhBTO9n3fxy3HJt7WlR9h9vvVmk.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 659}, {"MovieId": "531309", "Title": "Brightburn", "Casts": [], "PlotId": "531309", + "ThumbnailIds": ["/roslEbKdY0WSgYaB5KXvPKY0bXS.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 1}, + {"MovieId": "454227", "Title": "Outlaw King", "Casts": [], "PlotId": "454227", + "ThumbnailIds": ["/rT49ousKUWN3dV7UlhaC9onTNdl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 592}, {"MovieId": "76617", "Title": "Texas Chainsaw 3D", "Casts": [], "PlotId": "76617", + "ThumbnailIds": ["/p0kJOoNvwnWgmvKvL4GqlV3OPUV.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 765}, + {"MovieId": "440472", "Title": "The Upside", "Casts": [], "PlotId": "440472", + "ThumbnailIds": ["/hPZ2caow1PCND6qnerfgn6RTXdm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 200}, {"MovieId": "514439", "Title": "Breakthrough", "Casts": [], "PlotId": "514439", + "ThumbnailIds": ["/t58dx7JIgchr9If5uxn3NmHaHoS.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 34}, + {"MovieId": "378236", "Title": "The Emoji Movie", "Casts": [], "PlotId": "378236", + "ThumbnailIds": ["/f5pF4OYzh4wb1dYL2ARQNdqUsEZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 1411}, {"MovieId": "490132", "Title": "Green Book", "Casts": [], "PlotId": "490132", + "ThumbnailIds": ["/7BsvSuDQuoqhWmU2fL7W2GOcZHU.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 8.3, "NumRating": 3120}, + {"MovieId": "431530", "Title": "A Bad Moms Christmas", "Casts": [], "PlotId": "431530", + "ThumbnailIds": ["/gPNHolu7AGnrB7r5kvJRRTfwMFR.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 1013}, {"MovieId": "293660", "Title": "Deadpool", "Casts": [], "PlotId": "293660", + "ThumbnailIds": ["/inVq3FRqcYIRl2la8iZikYYxFNR.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.6, "NumRating": 19875}, + {"MovieId": "11451", "Title": "Herbie Fully Loaded", "Casts": [], "PlotId": "11451", + "ThumbnailIds": ["/7lTfTZ8CDfXw09eAv3OOvsbCVgs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.3, + "NumRating": 1168}, + {"MovieId": "209112", "Title": "Batman v Superman: Dawn of Justice", "Casts": [], "PlotId": "209112", + "ThumbnailIds": ["/cGOPbv9wA5gEejkUN892JrveARt.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 11827}, {"MovieId": "86467", "Title": "Bel Ami", "Casts": [], "PlotId": "86467", + "ThumbnailIds": ["/59bgVtpX6MPy2d5zberJiRNkDDx.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.2, "NumRating": 308}, + {"MovieId": "270010", "Title": "A Hologram for the King", "Casts": [], "PlotId": "270010", + "ThumbnailIds": ["/dDHJBd2iv7KTDzI7ybNLs31vvkM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 574}, + {"MovieId": "131631", "Title": "The Hunger Games: Mockingjay - Part 1", "Casts": [], "PlotId": "131631", + "ThumbnailIds": ["/gj282Pniaa78ZJfbaixyLXnXEDI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 9938}, {"MovieId": "151960", "Title": "Planes", "Casts": [], "PlotId": "151960", + "ThumbnailIds": ["/c8ALtmmmtofVYOxSJG0S5ZcP2pF.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.8, "NumRating": 860}, + {"MovieId": "13186", "Title": "Wrong Turn 2: Dead End", "Casts": [], "PlotId": "13186", + "ThumbnailIds": ["/ftmiyfrTrqTi5Qod62nGRP9BGPn.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 528}, {"MovieId": "1949", "Title": "Zodiac", "Casts": [], "PlotId": "1949", + "ThumbnailIds": ["/bgLyOROfFQI3FqYL7jQbiaV8lkN.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 4148}, + {"MovieId": "4922", "Title": "The Curious Case of Benjamin Button", "Casts": [], "PlotId": "4922", + "ThumbnailIds": ["/gjMR102u5hPdIAWX7O2rim8ZWgA.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 6474}, {"MovieId": "297762", "Title": "Wonder Woman", "Casts": [], "PlotId": "297762", + "ThumbnailIds": ["/imekS7f1OuHyUP2LAiTEM0zBzUz.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.3, "NumRating": 12322}, + {"MovieId": "15789", "Title": "A Goofy Movie", "Casts": [], "PlotId": "15789", + "ThumbnailIds": ["/bycmMhO3iIoEDzP768sUjq2RV4T.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 796}, {"MovieId": "1724", "Title": "The Incredible Hulk", "Casts": [], "PlotId": "1724", + "ThumbnailIds": ["/gCQ4e8klADtzoa6bL7XLPnjiUIg.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 6127}, + {"MovieId": "254128", "Title": "San Andreas", "Casts": [], "PlotId": "254128", + "ThumbnailIds": ["/qey0tdcOp9kCDdEZuJ87yE3crSe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 5035}, {"MovieId": "480530", "Title": "Creed II", "Casts": [], "PlotId": "480530", + "ThumbnailIds": ["/v3QyboWRoA4O9RbcsqH8tJMe8EB.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.7, "NumRating": 2107}, + {"MovieId": "127585", "Title": "X-Men: Days of Future Past", "Casts": [], "PlotId": "127585", + "ThumbnailIds": ["/pb1IURTkK5rImP9ZV83lxJO2us7.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 9884}, {"MovieId": "2320", "Title": "Executive Decision", "Casts": [], "PlotId": "2320", + "ThumbnailIds": ["/wmgkFuEr8bHRAgs1HS5U46Rzo0I.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.1, "NumRating": 387}, + {"MovieId": "574", "Title": "The Man Who Knew Too Much", "Casts": [], "PlotId": "574", + "ThumbnailIds": ["/vhUOukoJTWPfVpZOiKwrjdEV4OX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 541}, {"MovieId": "68817", "Title": "Footloose", "Casts": [], "PlotId": "68817", + "ThumbnailIds": ["/kDpo6G7rYRHQ1bFhyLiJEW9ESPO.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 910}, + {"MovieId": "141052", "Title": "Justice League", "Casts": [], "PlotId": "141052", + "ThumbnailIds": ["/eifGNCSDuxJeS1loAXil5bIGgvC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 7362}, {"MovieId": "403119", "Title": "47 Meters Down", "Casts": [], "PlotId": "403119", + "ThumbnailIds": ["/2IgdRUTdHyoI3nFORcnnYEKOGIH.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.6, "NumRating": 1484}, + {"MovieId": "332562", "Title": "A Star Is Born", "Casts": [], "PlotId": "332562", + "ThumbnailIds": ["/wrFpXMNBRj2PBiN4Z5kix51XaIZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 5321}, {"MovieId": "260513", "Title": "Incredibles 2", "Casts": [], "PlotId": "260513", + "ThumbnailIds": ["/9lFKBtaVIhP7E2Pk0IY1CwTKTMZ.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.6, "NumRating": 6089}, + {"MovieId": "401246", "Title": "The Square", "Casts": [], "PlotId": "401246", + "ThumbnailIds": ["/qYb8hzGGX7uBAVMYW1YFtcFeZhp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 594}, {"MovieId": "393519", "Title": "Raw", "Casts": [], "PlotId": "393519", + "ThumbnailIds": ["/6kXW9b1FZXvB3l0mLMDbKwGgL3P.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 1421}, + {"MovieId": "241257", "Title": "Regression", "Casts": [], "PlotId": "241257", + "ThumbnailIds": ["/d1dtQQfJJWU31cGSFbfhyeLtsTm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 1047}, {"MovieId": "12289", "Title": "Red Cliff", "Casts": [], "PlotId": "12289", + "ThumbnailIds": ["/uMiA2c1wrySRTI3f2ij5i2aCCya.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.2, "NumRating": 354}, + {"MovieId": "181808", "Title": "Star Wars: The Last Jedi", "Casts": [], "PlotId": "181808", + "ThumbnailIds": ["/kOVEVeg59E0wsnXmF9nrh6OmWII.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 8361}, {"MovieId": "399402", "Title": "Hunter Killer", "Casts": [], "PlotId": "399402", + "ThumbnailIds": ["/a0j18XNVhP4RcW3wXwsqT0kVoQm.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.2, "NumRating": 623}, + {"MovieId": "12123", "Title": "Chain Reaction", "Casts": [], "PlotId": "12123", + "ThumbnailIds": ["/62jE7wHYXLMBdYrdpTHpHsAu7NL.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 444}, + {"MovieId": "1116", "Title": "The Wind That Shakes the Barley", "Casts": [], "PlotId": "1116", + "ThumbnailIds": ["/uLt7SpgPjfwkWqJpUk08xONISFZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 304}, {"MovieId": "308504", "Title": "Last Knights", "Casts": [], "PlotId": "308504", + "ThumbnailIds": ["/jKcbKy4C9bbwcBWGkMQR70vBNXJ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 523}, + {"MovieId": "19995", "Title": "Avatar", "Casts": [], "PlotId": "19995", + "ThumbnailIds": ["/kmcqlZGaSh20zpTbuoF0Cdn07dT.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 18408}, {"MovieId": "603", "Title": "The Matrix", "Casts": [], "PlotId": "603", + "ThumbnailIds": ["/hEpWvX6Bp79eLxY1kX5ZZJcme5U.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 8.1, "NumRating": 14155}, + {"MovieId": "207768", "Title": "I Spit on Your Grave 2", "Casts": [], "PlotId": "207768", + "ThumbnailIds": ["/pHratkL4ETydC0Kcwgc3tOyvLK0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 487}, + {"MovieId": "145247", "Title": "The 100 Year-Old Man Who Climbed Out the Window and Disappeared", + "Casts": [], "PlotId": "145247", "ThumbnailIds": ["/cE51BdbaxpOWnfSKlSPNy5rzzFV.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.8, "NumRating": 491}, + {"MovieId": "844", "Title": "2046", "Casts": [], "PlotId": "844", + "ThumbnailIds": ["/orslhtqVBbI1n7RECP7Fkhuodph.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 339}, {"MovieId": "77931", "Title": "The Smurfs 2", "Casts": [], "PlotId": "77931", + "ThumbnailIds": ["/mF2FXzZ3EktWoagU4fzoabNsK6J.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 1186}, + {"MovieId": "320007", "Title": "Victoria", "Casts": [], "PlotId": "320007", + "ThumbnailIds": ["/t66eAg7xBLGoSSoA8JtJtX3NKs1.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 609}, {"MovieId": "266396", "Title": "The Gunman", "Casts": [], "PlotId": "266396", + "ThumbnailIds": ["/lnUozDnDANTsDYEdsNsHC6b8IiS.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 575}, + {"MovieId": "272", "Title": "Batman Begins", "Casts": [], "PlotId": "272", + "ThumbnailIds": ["/dr6x4GyyegBWtinPBzipY02J2lV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 11969}, {"MovieId": "374473", "Title": "I, Daniel Blake", "Casts": [], "PlotId": "374473", + "ThumbnailIds": ["/jJhqXTsAXCS46NhkYXBM7HDs8z8.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.7, "NumRating": 598}, + {"MovieId": "290751", "Title": "Secret in Their Eyes", "Casts": [], "PlotId": "290751", + "ThumbnailIds": ["/jHaJkzzmxjwXnvNHBFqXd8l4UE4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 682}, + {"MovieId": "353326", "Title": "The Man Who Knew Infinity", "Casts": [], "PlotId": "353326", + "ThumbnailIds": ["/stXjVMZlY8khhjiJYrq4DdQ5uyV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 685}, {"MovieId": "152780", "Title": "The Past", "Casts": [], "PlotId": "152780", + "ThumbnailIds": ["/kjerwTkZJev6Ve5DS0hRbaOuVJb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 247}, + {"MovieId": "341006", "Title": "The Belko Experiment", "Casts": [], "PlotId": "341006", + "ThumbnailIds": ["/faJK0dP3S92kQoKtO4LZMjy41kf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 743}, {"MovieId": "9703", "Title": "The Last Legion", "Casts": [], "PlotId": "9703", + "ThumbnailIds": ["/8K4WWwFew1CzCGVkgmKdamCA6kz.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 393}, + {"MovieId": "316152", "Title": "Free State of Jones", "Casts": [], "PlotId": "316152", + "ThumbnailIds": ["/tQcwB5nXpN4vH5ewP79tyXJcA1I.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 773}, {"MovieId": "97614", "Title": "Deadfall", "Casts": [], "PlotId": "97614", + "ThumbnailIds": ["/kt3bqW8pgbIxJY7aOqcQfUpB8dA.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 352}, + {"MovieId": "278", "Title": "The Shawshank Redemption", "Casts": [], "PlotId": "278", + "ThumbnailIds": ["/9O7gLzmreU0nGkIB6K3BsJbzvNv.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.7, + "NumRating": 12976}, {"MovieId": "351339", "Title": "Anthropoid", "Casts": [], "PlotId": "351339", + "ThumbnailIds": ["/5hUghRVVkBYufftfNQkGevY5AmE.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.0, "NumRating": 504}, + {"MovieId": "353616", "Title": "Pitch Perfect 3", "Casts": [], "PlotId": "353616", + "ThumbnailIds": ["/fchHLsLjFvzAFSQykiMwdF1051.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 1834}, + {"MovieId": "77948", "Title": "The Cold Light of Day", "Casts": [], "PlotId": "77948", + "ThumbnailIds": ["/zXhphNKS56VQbVJXqk3OMrjtNNc.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 454}, {"MovieId": "1667", "Title": "March of the Penguins", "Casts": [], "PlotId": "1667", + "ThumbnailIds": ["/yjAayG6nY9VO8IJoTLrSLUyu5kD.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 593}, + {"MovieId": "341012", "Title": "Popstar: Never Stop Never Stopping", "Casts": [], "PlotId": "341012", + "ThumbnailIds": ["/m9k2MCvZJWkV5DfJY9fueeeqNxb.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 662}, {"MovieId": "15370", "Title": "The Cat Returns", "Casts": [], "PlotId": "15370", + "ThumbnailIds": ["/57rS5DXN8YfRHgh609RgCKjKbel.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 698}, + {"MovieId": "289222", "Title": "The Zookeeper's Wife", "Casts": [], "PlotId": "289222", + "ThumbnailIds": ["/50KGpMiIvSkF4WHOgp0gM6r6sMU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 705}, {"MovieId": "420817", "Title": "Aladdin", "Casts": [], "PlotId": "420817", + "ThumbnailIds": ["/3iYQTLGoy7QnjcUYRJy4YrAgGvp.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "395458", "Title": "Suburbicon", "Casts": [], "PlotId": "395458", + "ThumbnailIds": ["/a3IHgSwO5jWPLcGjKqbQ7pxVGkq.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 859}, {"MovieId": "54318", "Title": "The Water Horse", "Casts": [], "PlotId": "54318", + "ThumbnailIds": ["/i1DU0Nux6CozY1uEjyz5uCWBxhc.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 518}, + {"MovieId": "27205", "Title": "Inception", "Casts": [], "PlotId": "27205", + "ThumbnailIds": ["/qmDpIHrmpJINaRKAfWQfftjCdyi.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.3, + "NumRating": 21844}, + {"MovieId": "10925", "Title": "The Return of the Living Dead", "Casts": [], "PlotId": "10925", + "ThumbnailIds": ["/1vydUitzC8W8W5oNevEArCltydJ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 465}, {"MovieId": "179826", "Title": "Odd Thomas", "Casts": [], "PlotId": "179826", + "ThumbnailIds": ["/b8FUEQnuig1BpBiWVfybOk3VozN.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 539}, + {"MovieId": "390062", "Title": "Jungle", "Casts": [], "PlotId": "390062", + "ThumbnailIds": ["/tDgxknTVwrScxpCYyGUjXSn5NRk.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 800}, {"MovieId": "680", "Title": "Pulp Fiction", "Casts": [], "PlotId": "680", + "ThumbnailIds": ["/dM2w364MScsjFf8pfMbaWUcWrR.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.4, "NumRating": 14963}, + {"MovieId": "2019", "Title": "Hard Target", "Casts": [], "PlotId": "2019", + "ThumbnailIds": ["/6WEu60V7EzncuFJSVmGJzhFvs4I.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 407}, + {"MovieId": "302349", "Title": "Iron Sky: The Coming Race", "Casts": [], "PlotId": "302349", + "ThumbnailIds": ["/l5t2Nf1F7iQUKTrODg93xmQzZLj.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.8, + "NumRating": 16}, {"MovieId": "280", "Title": "Terminator 2: Judgment Day", "Casts": [], "PlotId": "280", + "ThumbnailIds": ["/2y4dmgWYRMYXdD1UyJVcn2HSd1D.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.9, "NumRating": 6589}, + {"MovieId": "11976", "Title": "Legend", "Casts": [], "PlotId": "11976", + "ThumbnailIds": ["/xNWs9LM46vJWwf1q0VGPOccTKg4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 496}, {"MovieId": "8966", "Title": "Twilight", "Casts": [], "PlotId": "8966", + "ThumbnailIds": ["/lcMp3AONdNhjYE9MmTtMMTOiRDP.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 7194}, + {"MovieId": "10048", "Title": "Stealth", "Casts": [], "PlotId": "10048", + "ThumbnailIds": ["/tiezxIq6TJO4B09c13Z8a675dDy.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 492}, {"MovieId": "369192", "Title": "Battle of the Sexes", "Casts": [], "PlotId": "369192", + "ThumbnailIds": ["/fWy0A3VojTCb0S2MKtEJjpquubF.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 1022}, + {"MovieId": "381288", "Title": "Split", "Casts": [], "PlotId": "381288", + "ThumbnailIds": ["/rXMWOZiCt6eMX22jWuTOSdQ98bY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 10125}, {"MovieId": "111190", "Title": "Adore", "Casts": [], "PlotId": "111190", + "ThumbnailIds": ["/l2OTtTGfSfBRKArvzn14sk3xiNL.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.1, "NumRating": 547}, + {"MovieId": "12103", "Title": "Don't Say a Word", "Casts": [], "PlotId": "12103", + "ThumbnailIds": ["/qx3hgW9MqxsEEFjx4eSbpp1Fe2l.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 402}, {"MovieId": "88273", "Title": "A Royal Affair", "Casts": [], "PlotId": "88273", + "ThumbnailIds": ["/f7bfKSEasjNLJ7I8rS5YJmzSDGr.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 442}, + {"MovieId": "5336", "Title": "Sal\u00f2, or the 120 Days of Sodom", "Casts": [], "PlotId": "5336", + "ThumbnailIds": ["/xnaDdiRfZlJaTf6JRc4in40eaeI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 658}, {"MovieId": "399361", "Title": "Triple Frontier", "Casts": [], "PlotId": "399361", + "ThumbnailIds": ["/aBw8zYuAljVM1FeK5bZKITPH8ZD.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 992}, + {"MovieId": "11319", "Title": "The Rescuers", "Casts": [], "PlotId": "11319", + "ThumbnailIds": ["/49rGpB2x6AFB83SC4IBl9foRIGp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 1264}, {"MovieId": "335984", "Title": "Blade Runner 2049", "Casts": [], "PlotId": "335984", + "ThumbnailIds": ["/gajva2L0rPYkEWjzgFlBXCAVBE5.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.4, "NumRating": 6549}, + {"MovieId": "314405", "Title": "Tale of Tales", "Casts": [], "PlotId": "314405", + "ThumbnailIds": ["/9HKUonyvlNvW74rxX9XwbiWCjQV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 827}, {"MovieId": "260346", "Title": "Taken 3", "Casts": [], "PlotId": "260346", + "ThumbnailIds": ["/ikDwR3i2bczqnRf1urJTy77YTFf.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 3360}, + {"MovieId": "10199", "Title": "The Tale of Despereaux", "Casts": [], "PlotId": "10199", + "ThumbnailIds": ["/8Nge4rXAQzU5w9U8OvnXuuJltL9.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 454}, + {"MovieId": "338189", "Title": "It's Only the End of the World", "Casts": [], "PlotId": "338189", + "ThumbnailIds": ["/xTqy97V5EYtnMWWo2wVpScanpDS.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 845}, {"MovieId": "182560", "Title": "Dark Places", "Casts": [], "PlotId": "182560", + "ThumbnailIds": ["/1z7Bxnxi1lgO0ksc6peI4UssEPf.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 747}, + {"MovieId": "574241", "Title": "Poms", "Casts": [], "PlotId": "574241", + "ThumbnailIds": ["/m2ksKdmWIoUh3GJfu9oEBLorjAJ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.5, + "NumRating": 1}, + {"MovieId": "438674", "Title": "Dragged Across Concrete", "Casts": [], "PlotId": "438674", + "ThumbnailIds": ["/fVG4a27ImyPS4vvNMjCtan3QhDl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 95}, {"MovieId": "467660", "Title": "Unsane", "Casts": [], "PlotId": "467660", + "ThumbnailIds": ["/jvDBfavZASdKsJunu9VCAtXjLS2.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 632}, + {"MovieId": "2749", "Title": "15 Minutes", "Casts": [], "PlotId": "2749", + "ThumbnailIds": ["/qqqleHV8y7NLKt7isSZAvOwVPH6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 321}, + {"MovieId": "123025", "Title": "Batman: The Dark Knight Returns, Part 1", "Casts": [], "PlotId": "123025", + "ThumbnailIds": ["/mFPD2YsdaWAzjuxF7ItGmsEFpdY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 691}, {"MovieId": "38543", "Title": "Ironclad", "Casts": [], "PlotId": "38543", + "ThumbnailIds": ["/kxAkA3cL0pSiGlW1JeiGHJr5Jv2.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 366}, + {"MovieId": "11045", "Title": "Taxi", "Casts": [], "PlotId": "11045", + "ThumbnailIds": ["/7Ly55SeRBozH3ZBeSOodcgKGYlF.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.9, + "NumRating": 434}, {"MovieId": "3093", "Title": "Basic Instinct 2", "Casts": [], "PlotId": "3093", + "ThumbnailIds": ["/gXzrKzHnMmfWCF5PMYQfNOzCxYl.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.7, "NumRating": 319}, + {"MovieId": "315664", "Title": "Florence Foster Jenkins", "Casts": [], "PlotId": "315664", + "ThumbnailIds": ["/aBjQeNw7JBpXWGFaPYcGNlecRyr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 837}, {"MovieId": "210577", "Title": "Gone Girl", "Casts": [], "PlotId": "210577", + "ThumbnailIds": ["/gdiLTof3rbPDAmPaCf4g6op46bj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.9, "NumRating": 10253}, + {"MovieId": "397422", "Title": "Rough Night", "Casts": [], "PlotId": "397422", + "ThumbnailIds": ["/i66xbL1C6FEWDm2KoX11DHmP4Rz.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 1053}, {"MovieId": "424", "Title": "Schindler's List", "Casts": [], "PlotId": "424", + "ThumbnailIds": ["/yPisjyLweCl1tbgwgtzBCNCBle.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.5, "NumRating": 7885}, + {"MovieId": "218778", "Title": "Alexander and the Terrible, Horrible, No Good, Very Bad Day", "Casts": [], + "PlotId": "218778", "ThumbnailIds": ["/f29NQJaWyzXldOGT7F5CDKbwAH9.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 801}, + {"MovieId": "9994", "Title": "The Great Mouse Detective", "Casts": [], "PlotId": "9994", + "ThumbnailIds": ["/9uDr7vfjCFr39KGCcqrk44Cg7fQ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 746}, {"MovieId": "10320", "Title": "The Ring Two", "Casts": [], "PlotId": "10320", + "ThumbnailIds": ["/mYZdcd5XeZkye72oQejkLuAGI7q.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.6, "NumRating": 1219}, + {"MovieId": "374475", "Title": "Toni Erdmann", "Casts": [], "PlotId": "374475", + "ThumbnailIds": ["/gJNjVE8WGUjiSKUtMDEvNzxR5zq.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 479}, {"MovieId": "830", "Title": "Forbidden Planet", "Casts": [], "PlotId": "830", + "ThumbnailIds": ["/qdH1Fm5vl1DAjXNHPmRZ4yEiUgM.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 376}, + {"MovieId": "243940", "Title": "The Lazarus Effect", "Casts": [], "PlotId": "243940", + "ThumbnailIds": ["/3oqjCN38pb7MI5i8lUlDQd4IAeA.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 809}, + {"MovieId": "9662", "Title": "The Triplets of Belleville", "Casts": [], "PlotId": "9662", + "ThumbnailIds": ["/A8aSp63Xi4cnip3wAhk7Jml1uWG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 437}, {"MovieId": "41211", "Title": "Heartbreaker", "Casts": [], "PlotId": "41211", + "ThumbnailIds": ["/fECaEIjjvozG1dGoi6cgTevIae5.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 743}, + {"MovieId": "515248", "Title": "Someone Great", "Casts": [], "PlotId": "515248", + "ThumbnailIds": ["/h0nz5lIBXeUZChBNfwL08bLWQaU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 328}, {"MovieId": "177494", "Title": "Veronica Mars", "Casts": [], "PlotId": "177494", + "ThumbnailIds": ["/nS3L07mQfcNJcisLEKgi8fWoBS1.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 695}, + {"MovieId": "102780", "Title": "Byzantium", "Casts": [], "PlotId": "102780", + "ThumbnailIds": ["/pSnlpdh27luQ6GcchSkPomZ5fyr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 465}, {"MovieId": "155084", "Title": "13 Sins", "Casts": [], "PlotId": "155084", + "ThumbnailIds": ["/uYPy89cVgvE1ic0bN39YJrumxwo.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 561}, + {"MovieId": "353081", "Title": "Mission: Impossible - Fallout", "Casts": [], "PlotId": "353081", + "ThumbnailIds": ["/AkJQpZp9WoNdj7pLYSj1L0RcMMN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 3664}, + {"MovieId": "250734", "Title": "Far from the Madding Crowd", "Casts": [], "PlotId": "250734", + "ThumbnailIds": ["/6v1DEaO2TZzrqcaGohUf1K7BXkU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 578}, {"MovieId": "471859", "Title": "Charlie Says", "Casts": [], "PlotId": "471859", + "ThumbnailIds": ["/nwYDXLQpPULdUbwkTc4gWpMJufn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.0, "NumRating": 5}, + {"MovieId": "252178", "Title": "'71", "Casts": [], "PlotId": "252178", + "ThumbnailIds": ["/b8dmfG84peFdouN2N8wOsiI9WHt.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 617}, {"MovieId": "16608", "Title": "The Proposition", "Casts": [], "PlotId": "16608", + "ThumbnailIds": ["/e4j7s9SzaJTfjkorMX1iY38IzZi.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 327}, + {"MovieId": "127560", "Title": "The Railway Man", "Casts": [], "PlotId": "127560", + "ThumbnailIds": ["/3iU9KLIhvc3pdl1SkeD0gv4BO0f.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 400}, {"MovieId": "428449", "Title": "A Ghost Story", "Casts": [], "PlotId": "428449", + "ThumbnailIds": ["/rp5JPIyZi9sMob15l46zNQLe5cO.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 879}, + {"MovieId": "330483", "Title": "The Choice", "Casts": [], "PlotId": "330483", + "ThumbnailIds": ["/7EenqQdtZjOeBAAlxNtnG1eLHnf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 1022}, + {"MovieId": "10131", "Title": "A Nightmare on Elm Street 4: The Dream Master", "Casts": [], + "PlotId": "10131", "ThumbnailIds": ["/gERdob60xVv0tJ1Kr3XEqqj0KFl.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 556}, + {"MovieId": "10497", "Title": "Bitter Moon", "Casts": [], "PlotId": "10497", + "ThumbnailIds": ["/sUiqWj0k5oPcThd1WaXvt9Vymzr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 231}, {"MovieId": "329865", "Title": "Arrival", "Casts": [], "PlotId": "329865", + "ThumbnailIds": ["/hLudzvGfpi6JlwUnsNhXwKKg4j.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 10225}, + {"MovieId": "55301", "Title": "Alvin and the Chipmunks: Chipwrecked", "Casts": [], "PlotId": "55301", + "ThumbnailIds": ["/s1nHXSJTq45RUHn7RmW6fO7kHu9.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 1028}, {"MovieId": "375315", "Title": "The Salesman", "Casts": [], "PlotId": "375315", + "ThumbnailIds": ["/sLqw0NXQJY8S8Na94rlefavBnRX.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.3, "NumRating": 380}, + {"MovieId": "9425", "Title": "Soldier", "Casts": [], "PlotId": "9425", + "ThumbnailIds": ["/lpByPXb2xJvsmeEBxO9YPADLLdi.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 326}, + {"MovieId": "121", "Title": "The Lord of the Rings: The Two Towers", "Casts": [], "PlotId": "121", + "ThumbnailIds": ["/5VTN0pR8gcqV3EPUHHfMGnJYN9L.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.3, + "NumRating": 12288}, + {"MovieId": "400535", "Title": "Sicario: Day of the Soldado", "Casts": [], "PlotId": "400535", + "ThumbnailIds": ["/msqWSQkU403cQKjQHnWLnugv7EY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 1261}, + {"MovieId": "498248", "Title": "Mia and the White Lion", "Casts": [], "PlotId": "498248", + "ThumbnailIds": ["/zXfwBeYe0RnqsJ0dnBoFTB4SSrG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 134}, {"MovieId": "429197", "Title": "Vice", "Casts": [], "PlotId": "429197", + "ThumbnailIds": ["/1gCab6rNv1r6V64cwsU4oEr649Y.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 1095}, + {"MovieId": "12444", "Title": "Harry Potter and the Deathly Hallows: Part 1", "Casts": [], + "PlotId": "12444", "ThumbnailIds": ["/maP4MTfPCeVD2FZbKTLUgriOW4R.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.7, "NumRating": 10591}, + {"MovieId": "269149", "Title": "Zootopia", "Casts": [], "PlotId": "269149", + "ThumbnailIds": ["/sM33SANp9z6rXW8Itn7NnG1GOEs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 9815}, {"MovieId": "22787", "Title": "Whiteout", "Casts": [], "PlotId": "22787", + "ThumbnailIds": ["/oC28DSfylqtkf8AWkr0cVj7TcjS.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.4, "NumRating": 348}, + {"MovieId": "1930", "Title": "The Amazing Spider-Man", "Casts": [], "PlotId": "1930", + "ThumbnailIds": ["/eA2D86Y6VPWuUzZyatiLBwpTilQ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 10242}, {"MovieId": "9021", "Title": "The Santa Clause 2", "Casts": [], "PlotId": "9021", + "ThumbnailIds": ["/i7tbiDPIaa4VsQh1wWmbkY4zTRX.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.6, "NumRating": 609}, + {"MovieId": "527641", "Title": "Five Feet Apart", "Casts": [], "PlotId": "527641", + "ThumbnailIds": ["/kreTuJBkUjVWePRfhHZuYfhNE1T.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.2, + "NumRating": 470}, + {"MovieId": "140607", "Title": "Star Wars: The Force Awakens", "Casts": [], "PlotId": "140607", + "ThumbnailIds": ["/weUSwMdQIa3NaXVzwUoIIcAi85d.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 12473}, {"MovieId": "8342", "Title": "No Man's Land", "Casts": [], "PlotId": "8342", + "ThumbnailIds": ["/8DvLzDG9OeDzKdXZjRkCoWfXUcc.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.7, "NumRating": 184}, + {"MovieId": "244786", "Title": "Whiplash", "Casts": [], "PlotId": "244786", + "ThumbnailIds": ["/lIv1QinFqz4dlp5U4lQ6HaiskOZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.4, + "NumRating": 7750}, + {"MovieId": "243938", "Title": "Hot Tub Time Machine 2", "Casts": [], "PlotId": "243938", + "ThumbnailIds": ["/tQtWuwvMf0hCc2QR2tkolwl7c3c.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 498}, {"MovieId": "419479", "Title": "The Babysitter", "Casts": [], "PlotId": "419479", + "ThumbnailIds": ["/86a7GRVRCwfl7wdI4QadyvKa6Zu.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 1519}, + {"MovieId": "82390", "Title": "The Paperboy", "Casts": [], "PlotId": "82390", + "ThumbnailIds": ["/foYlHgTIT8PFOeHOFxBICDZmYrm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 398}, {"MovieId": "198663", "Title": "The Maze Runner", "Casts": [], "PlotId": "198663", + "ThumbnailIds": ["/coss7RgL0NH6g4fC2s5atvf3dFO.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 10483}, + {"MovieId": "11470", "Title": "Jason X", "Casts": [], "PlotId": "11470", + "ThumbnailIds": ["/zdfRa0FLVxfRwWhwitIZhjEIepI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.5, + "NumRating": 525}, + {"MovieId": "675", "Title": "Harry Potter and the Order of the Phoenix", "Casts": [], "PlotId": "675", + "ThumbnailIds": ["/4YnLxYLHhT4UQ8i9jxAXWy46Xuw.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 10605}, {"MovieId": "11050", "Title": "Terms of Endearment", "Casts": [], "PlotId": "11050", + "ThumbnailIds": ["/iZiYzBb6nyv2anbpVTV5ba6GSwq.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.1, "NumRating": 320}, + {"MovieId": "109445", "Title": "Frozen", "Casts": [], "PlotId": "109445", + "ThumbnailIds": ["/eFnGmj63QPUpK7QUWSOUhypIQOT.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 9577}, + {"MovieId": "11549", "Title": "Invasion of the Body Snatchers", "Casts": [], "PlotId": "11549", + "ThumbnailIds": ["/4oj0G2Zr6tQKBDOzKaLuefdeBsG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 406}, {"MovieId": "38541", "Title": "The Divide", "Casts": [], "PlotId": "38541", + "ThumbnailIds": ["/3AdJMWYMjZBED2DLfBiOq7Rfw03.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 300}, + {"MovieId": "398175", "Title": "Brawl in Cell Block 99", "Casts": [], "PlotId": "398175", + "ThumbnailIds": ["/bfB1J6jsjdGWKjXxKQ5hNd1OyAs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 489}, {"MovieId": "77663", "Title": "Killing Season", "Casts": [], "PlotId": "77663", + "ThumbnailIds": ["/ftZVLKyr3RDGtVelU06GAbtOFQa.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 571}, + {"MovieId": "673", "Title": "Harry Potter and the Prisoner of Azkaban", "Casts": [], "PlotId": "673", + "ThumbnailIds": ["/jUFjMoLh8T2CWzHUSjKCojI5SHu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 11378}, {"MovieId": "332340", "Title": "Man Up", "Casts": [], "PlotId": "332340", + "ThumbnailIds": ["/y7C1EQ9zxJ3mlaQeRztw3NVw41P.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.6, "NumRating": 534}, + {"MovieId": "458253", "Title": "Missing Link", "Casts": [], "PlotId": "458253", + "ThumbnailIds": ["/gEkKHiiQRVUSX15Iwo8VFydXrtu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 39}, {"MovieId": "9725", "Title": "Friday the 13th Part 2", "Casts": [], "PlotId": "9725", + "ThumbnailIds": ["/92rGctBMTv4uaSlIBVnhz01kRWL.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 618}, + {"MovieId": "265189", "Title": "Force Majeure", "Casts": [], "PlotId": "265189", + "ThumbnailIds": ["/rGMtc9AtZsnWSSL5VnLaGvx1PI6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 409}, + {"MovieId": "101", "Title": "L\u00e9on: The Professional", "Casts": [], "PlotId": "101", + "ThumbnailIds": ["/gE8S02QUOhVnAmYu4tcrBlMTujz.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.3, + "NumRating": 7509}, {"MovieId": "233063", "Title": "Suck Me Shakespeer", "Casts": [], "PlotId": "233063", + "ThumbnailIds": ["/9GSGg4McYID6Ld0OMwimLr1Zx4J.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.1, "NumRating": 1051}, + {"MovieId": "1497", "Title": "Teenage Mutant Ninja Turtles II: The Secret of the Ooze", "Casts": [], + "PlotId": "1497", "ThumbnailIds": ["/HD4LtUw4Ono9tzgwWOhHpHcjkj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 460}, + {"MovieId": "309886", "Title": "Blood Father", "Casts": [], "PlotId": "309886", + "ThumbnailIds": ["/rQ3CKv33u2Z4keC2AYZqi3RGIdX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 820}, {"MovieId": "31442", "Title": "Ivan's Childhood", "Casts": [], "PlotId": "31442", + "ThumbnailIds": ["/vmRWSLP1DE9WTta0hfzIafJ0dID.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.0, "NumRating": 221}, + {"MovieId": "198375", "Title": "The Garden of Words", "Casts": [], "PlotId": "198375", + "ThumbnailIds": ["/f1Mhgu0sxvXEcUYDH4yVWdNh10j.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 830}, {"MovieId": "11497", "Title": "All Dogs Go to Heaven", "Casts": [], "PlotId": "11497", + "ThumbnailIds": ["/nmWh1NglDinfkHD9zCNqGWyhl7Q.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 468}, + {"MovieId": "49051", "Title": "The Hobbit: An Unexpected Journey", "Casts": [], "PlotId": "49051", + "ThumbnailIds": ["/ysX7vDmSh5O19vFjAi56WL7l4nk.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 11926}, {"MovieId": "243683", "Title": "Step Up All In", "Casts": [], "PlotId": "243683", + "ThumbnailIds": ["/jFkvtmhE4NqENI4e0sZxmI8vrSS.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.8, "NumRating": 1187}, + {"MovieId": "11596", "Title": "New Nightmare", "Casts": [], "PlotId": "11596", + "ThumbnailIds": ["/oLEZibnraixTE68rTaYv4FEmvYd.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 554}, {"MovieId": "223485", "Title": "Slow West", "Casts": [], "PlotId": "223485", + "ThumbnailIds": ["/uZ9dBBn0GaEw0YIBvQd01Io3pho.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 529}, + {"MovieId": "524247", "Title": "The Intruder", "Casts": [], "PlotId": "524247", + "ThumbnailIds": ["/p9xKyetr0ihJ2K6HJMeXzc4IwEv.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 7}, {"MovieId": "453755", "Title": "Arctic", "Casts": [], "PlotId": "453755", + "ThumbnailIds": ["/dxRc0Y4IjSRLC2fYaguXQtJCI4e.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 159}, + {"MovieId": "6687", "Title": "Transsiberian", "Casts": [], "PlotId": "6687", + "ThumbnailIds": ["/izYDkKxj9sI8KNKK0PR54dpuiFu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 320}, {"MovieId": "10523", "Title": "W.", "Casts": [], "PlotId": "10523", + "ThumbnailIds": ["/upFQuRzN23kLvzO1brSFClLqZvV.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 238}, + {"MovieId": "12246", "Title": "Mongol: The Rise of Genghis Khan", "Casts": [], "PlotId": "12246", + "ThumbnailIds": ["/8dlyyuwSJtS0VOLM9xi7SA7xbfU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 271}, {"MovieId": "76544", "Title": "Man of Tai Chi", "Casts": [], "PlotId": "76544", + "ThumbnailIds": ["/42MutxsgeJS5pYLNWUGv8d6B9wa.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 422}, + {"MovieId": "93828", "Title": "Welcome to the Punch", "Casts": [], "PlotId": "93828", + "ThumbnailIds": ["/eJwSM618opISwPkzia9fNeFvW4Z.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 362}, + {"MovieId": "227348", "Title": "Paranormal Activity: The Marked Ones", "Casts": [], "PlotId": "227348", + "ThumbnailIds": ["/fMC6CqCRDzQDxAibydeZF9XBAJ4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.2, + "NumRating": 737}, {"MovieId": "205596", "Title": "The Imitation Game", "Casts": [], "PlotId": "205596", + "ThumbnailIds": ["/noUp0XOqIcmgefRnRZa1nhtRvWO.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.1, "NumRating": 10218}, + {"MovieId": "10587", "Title": "Police Academy 4: Citizens on Patrol", "Casts": [], "PlotId": "10587", + "ThumbnailIds": ["/AcvfIPLXGxSucYGSmfkRU2SqLi4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.3, + "NumRating": 486}, {"MovieId": "228150", "Title": "Fury", "Casts": [], "PlotId": "228150", + "ThumbnailIds": ["/pfte7wdMobMF4CVHuOxyu6oqeeA.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 6541}, + {"MovieId": "5902", "Title": "A Bridge Too Far", "Casts": [], "PlotId": "5902", + "ThumbnailIds": ["/ormdFwSHGVgsWdrzP5pRPaA6nme.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 329}, {"MovieId": "293310", "Title": "Citizenfour", "Casts": [], "PlotId": "293310", + "ThumbnailIds": ["/yNuLhb2y6I5cO7BfiJ7bdfllnIG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.9, "NumRating": 700}, + {"MovieId": "110420", "Title": "Wolf Children", "Casts": [], "PlotId": "110420", + "ThumbnailIds": ["/rDMxjCYEVnvLC4nsBpB6wjL0LDy.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.2, + "NumRating": 1024}, {"MovieId": "9962", "Title": "The Good Girl", "Casts": [], "PlotId": "9962", + "ThumbnailIds": ["/21AacCArwM5hQ8fYPRA0WKZviiN.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.0, "NumRating": 285}, + {"MovieId": "466282", "Title": "To All the Boys I've Loved Before", "Casts": [], "PlotId": "466282", + "ThumbnailIds": ["/hKHZhUbIyUAjcSrqJThFGYIR6kI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 4317}, {"MovieId": "439079", "Title": "The Nun", "Casts": [], "PlotId": "439079", + "ThumbnailIds": ["/sFC1ElvoKGdHJIWRpNB3xWJ9lJA.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.6, "NumRating": 2695}, + {"MovieId": "246080", "Title": "Black Sea", "Casts": [], "PlotId": "246080", + "ThumbnailIds": ["/ghac5aKzS0tsmBhXXAWURcEEVk0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 615}, {"MovieId": "535", "Title": "Flashdance", "Casts": [], "PlotId": "535", + "ThumbnailIds": ["/jwsnPftPybX2pTDbvWbyqviKO7S.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 626}, + {"MovieId": "11046", "Title": "Where Eagles Dare", "Casts": [], "PlotId": "11046", + "ThumbnailIds": ["/991u0tSWGTVXCYkgC6ftaGE4bkQ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 372}, {"MovieId": "10479", "Title": "Rules of Engagement", "Casts": [], "PlotId": "10479", + "ThumbnailIds": ["/1UlrMyCza2xINltCyYgmAwQAx07.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 276}, + {"MovieId": "526", "Title": "Ladyhawke", "Casts": [], "PlotId": "526", + "ThumbnailIds": ["/51RFCKLFuEbvLQsFzXcupQnkoRD.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 535}, {"MovieId": "345940", "Title": "The Meg", "Casts": [], "PlotId": "345940", + "ThumbnailIds": ["/eyWICPcxOuTcDDDbTMOZawoOn8d.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 2764}, + {"MovieId": "82702", "Title": "How to Train Your Dragon 2", "Casts": [], "PlotId": "82702", + "ThumbnailIds": ["/lRjOR4uclMQijUav4OjeZprlehu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 5471}, + {"MovieId": "767", "Title": "Harry Potter and the Half-Blood Prince", "Casts": [], "PlotId": "767", + "ThumbnailIds": ["/bFXys2nhALwDvpkF3dP3Vvdfn8b.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 10346}, {"MovieId": "8881", "Title": "Che: Part One", "Casts": [], "PlotId": "8881", + "ThumbnailIds": ["/eayI8CIR8miw3rDVLEwoqTL0TPt.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.8, "NumRating": 356}, + {"MovieId": "157845", "Title": "The Rover", "Casts": [], "PlotId": "157845", + "ThumbnailIds": ["/ugVbrbicJHfOw8e24RUYISKqgvf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 427}, + {"MovieId": "503129", "Title": "Student of the Year 2", "Casts": [], "PlotId": "503129", + "ThumbnailIds": ["/ij8au3Sw1iw6AhroZ0AYYsCra51.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 3.0, + "NumRating": 3}, {"MovieId": "471507", "Title": "Destroyer", "Casts": [], "PlotId": "471507", + "ThumbnailIds": ["/sHw9gTdo43nJL82py0oaROkXXNr.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 131}, + {"MovieId": "150117", "Title": "I Give It a Year", "Casts": [], "PlotId": "150117", + "ThumbnailIds": ["/d05UThmFSAUD6FTHNjYxhrmdTec.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 483}, {"MovieId": "221902", "Title": "Two Days, One Night", "Casts": [], "PlotId": "221902", + "ThumbnailIds": ["/1mYAejpMskvskGr0J0SaBvdjmrH.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 506}, + {"MovieId": "433808", "Title": "The Ritual", "Casts": [], "PlotId": "433808", + "ThumbnailIds": ["/hHuJqzby593lmYmw1SzT0XYy99t.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 1054}, {"MovieId": "153158", "Title": "Underdogs", "Casts": [], "PlotId": "153158", + "ThumbnailIds": ["/rKnfNwbRGd6p7SidQYlGemCE8wb.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.1, "NumRating": 190}, + {"MovieId": "209274", "Title": "Ida", "Casts": [], "PlotId": "209274", + "ThumbnailIds": ["/aO8dgaxaY1tcAdtFkLuz1WSxgdd.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 448}, + {"MovieId": "58", "Title": "Pirates of the Caribbean: Dead Man's Chest", "Casts": [], "PlotId": "58", + "ThumbnailIds": ["/waFr5RVKaQ9dzOt3nQuIVB1FiPu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 9232}, {"MovieId": "10972", "Title": "Session 9", "Casts": [], "PlotId": "10972", + "ThumbnailIds": ["/mWwrWhPHiHSoCWUkYcq4GplRF6V.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.2, "NumRating": 412}, + {"MovieId": "168705", "Title": "BloodRayne", "Casts": [], "PlotId": "168705", + "ThumbnailIds": ["/qCrvpGxWjOh7tVLxyqWLR65ZuWX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 3.8, + "NumRating": 207}, + {"MovieId": "382322", "Title": "Batman: The Killing Joke", "Casts": [], "PlotId": "382322", + "ThumbnailIds": ["/zm0ODjtfJfJW0W269LqsQl5OhJ8.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 860}, {"MovieId": "3035", "Title": "Frankenstein", "Casts": [], "PlotId": "3035", + "ThumbnailIds": ["/wB58wlBAr6484Wm6VyFDqSD8VOJ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 591}, + {"MovieId": "245916", "Title": "Kill the Messenger", "Casts": [], "PlotId": "245916", + "ThumbnailIds": ["/8gaNZiKZHvKCqMDByY00dUIV0YC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 502}, {"MovieId": "84287", "Title": "The Imposter", "Casts": [], "PlotId": "84287", + "ThumbnailIds": ["/aj6kq3QskRkC1qJyau8ukB478Hw.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 394}, + {"MovieId": "300681", "Title": "Replicas", "Casts": [], "PlotId": "300681", + "ThumbnailIds": ["/hhPBTAn9b4TYOxc1JYNsX4BFAlW.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 239}, {"MovieId": "157851", "Title": "Maps to the Stars", "Casts": [], "PlotId": "157851", + "ThumbnailIds": ["/ciIFIMR78V9BMfLEylJGSwnlwm.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 599}, + {"MovieId": "2614", "Title": "Innerspace", "Casts": [], "PlotId": "2614", + "ThumbnailIds": ["/krRl7QKdIYsVUPYmOwElA1sqQE7.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 557}, {"MovieId": "12233", "Title": "Oliver & Company", "Casts": [], "PlotId": "12233", + "ThumbnailIds": ["/m54pXsIUy3IJoaGhk3RwKsZQPG8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 759}, + {"MovieId": "11128", "Title": "Ladder 49", "Casts": [], "PlotId": "11128", + "ThumbnailIds": ["/opgPMSdkXsTOB5W169VlivKyaZQ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 348}, + {"MovieId": "408", "Title": "Snow White and the Seven Dwarfs", "Casts": [], "PlotId": "408", + "ThumbnailIds": ["/wbVGRBYPRRahIZNGXY9TfHDUSc2.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 4036}, + {"MovieId": "2312", "Title": "In the Name of the King: A Dungeon Siege Tale", "Casts": [], + "PlotId": "2312", "ThumbnailIds": ["/bbN1lmDk1PT0GsTFCy179sk5nIF.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.4, "NumRating": 369}, + {"MovieId": "74", "Title": "War of the Worlds", "Casts": [], "PlotId": "74", + "ThumbnailIds": ["/xXMM9KY2eq1SDOQif9zO91YOBA8.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 4330}, {"MovieId": "6522", "Title": "Life", "Casts": [], "PlotId": "6522", + "ThumbnailIds": ["/c9phL8FXuL7i15CrjQH5PfM60NB.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.5, "NumRating": 313}, + {"MovieId": "11186", "Title": "Child's Play 2", "Casts": [], "PlotId": "11186", + "ThumbnailIds": ["/xy0qUntbDOVgiTDvSIFlSM3MMzP.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 663}, {"MovieId": "2322", "Title": "Sneakers", "Casts": [], "PlotId": "2322", + "ThumbnailIds": ["/pAGKtPF8nMiJq4lsINOYJWvMS2D.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 440}, + {"MovieId": "550", "Title": "Fight Club", "Casts": [], "PlotId": "550", + "ThumbnailIds": ["/adw6Lq9FiC9zjYEpOqfq03ituwp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.4, + "NumRating": 16048}, + {"MovieId": "10191", "Title": "How to Train Your Dragon", "Casts": [], "PlotId": "10191", + "ThumbnailIds": ["/ygGmAO60t8GyqUo9xYeYxSZAR3b.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 7446}, {"MovieId": "1534", "Title": "Pathfinder", "Casts": [], "PlotId": "1534", + "ThumbnailIds": ["/y3OvDjTiq7LdM6nDlZQbuEAFKou.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.4, "NumRating": 294}, + {"MovieId": "454652", "Title": "Colette", "Casts": [], "PlotId": "454652", + "ThumbnailIds": ["/pGiUIkcTOEn2CwE5CUBFxWkcyxO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 293}, {"MovieId": "157847", "Title": "Joe", "Casts": [], "PlotId": "157847", + "ThumbnailIds": ["/bvcLnoffCm67n7pZPVdJ6pluJsi.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 504}, + {"MovieId": "13851", "Title": "Batman: Gotham Knight", "Casts": [], "PlotId": "13851", + "ThumbnailIds": ["/eIAhXROHG8t3QQ7qU0HfZgL5XFf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 270}, {"MovieId": "5550", "Title": "RoboCop 3", "Casts": [], "PlotId": "5550", + "ThumbnailIds": ["/1YSXqKHjMAutWpIat9AOTIgozDb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.5, "NumRating": 508}, + {"MovieId": "11230", "Title": "Drunken Master", "Casts": [], "PlotId": "11230", + "ThumbnailIds": ["/fWrUTM8jeiU6G6rA4NKwkeigxGa.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 319}, + {"MovieId": "9513", "Title": "Garfield: A Tail of Two Kitties", "Casts": [], "PlotId": "9513", + "ThumbnailIds": ["/aagx3t2Xv7R26hcqzrayTT28Yww.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.3, + "NumRating": 988}, {"MovieId": "571786", "Title": "Miss & Mrs. Cops", "Casts": [], "PlotId": "571786", + "ThumbnailIds": ["/v1sfULur2KiJIPGiXjGTDWNWxG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "790", "Title": "The Fog", "Casts": [], "PlotId": "790", + "ThumbnailIds": ["/u9wAIHya0BHJvGJh3mBPr010U7C.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 572}, {"MovieId": "245698", "Title": "Pawn Sacrifice", "Casts": [], "PlotId": "245698", + "ThumbnailIds": ["/mxdpBuSMqql2Uvv27NIV1pahcsW.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 438}, + {"MovieId": "345887", "Title": "The Equalizer 2", "Casts": [], "PlotId": "345887", + "ThumbnailIds": ["/cQvc9N6JiMVKqol3wcYrGshsIdZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 1987}, + {"MovieId": "12207", "Title": "The Legend of Drunken Master", "Casts": [], "PlotId": "12207", + "ThumbnailIds": ["/zZODgJHFtzs6wwa4cp6Nezb5AGe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 373}, + {"MovieId": "26123", "Title": "American Pie Presents: The Book of Love", "Casts": [], "PlotId": "26123", + "ThumbnailIds": ["/hwP0GEP0zy8ar965Xaht19SmMd3.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 900}, + {"MovieId": "172", "Title": "Star Trek V: The Final Frontier", "Casts": [], "PlotId": "172", + "ThumbnailIds": ["/kugwPq2E5IkzrgoxRycnoqqUS9H.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 572}, {"MovieId": "36647", "Title": "Blade", "Casts": [], "PlotId": "36647", + "ThumbnailIds": ["/r0RQ9ZOEZglLOeYDNJTehVTRoR6.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 3083}, + {"MovieId": "226486", "Title": "Tammy", "Casts": [], "PlotId": "226486", + "ThumbnailIds": ["/cq6wvDqETqJXgpQplkL0FBw2leM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.2, + "NumRating": 719}, {"MovieId": "11967", "Title": "Young Guns", "Casts": [], "PlotId": "11967", + "ThumbnailIds": ["/wkGbwWHIM23BkmMBm7GQHaCZan8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 405}, + {"MovieId": "130150", "Title": "Labor Day", "Casts": [], "PlotId": "130150", + "ThumbnailIds": ["/4TKMiTqZnG6KJWjmxy2ZCDuG3M0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 531}, + {"MovieId": "12118", "Title": "Police Academy 3: Back in Training", "Casts": [], "PlotId": "12118", + "ThumbnailIds": ["/r4j14ZjmWfcQbboG76gxloqa3N3.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 492}, + {"MovieId": "9064", "Title": "Hellbound: Hellraiser II", "Casts": [], "PlotId": "9064", + "ThumbnailIds": ["/ryuo4unQkzcEKhfAfzhbQo8oxVm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 398}, {"MovieId": "1683", "Title": "The Reaping", "Casts": [], "PlotId": "1683", + "ThumbnailIds": ["/wwpI649fFYWaOhit5O1WLsFuHOI.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 375}, + {"MovieId": "517166", "Title": "The Axiom", "Casts": [], "PlotId": "517166", + "ThumbnailIds": ["/6lxM6WEXxJUFOsnHbWPIYnwiT0c.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 4}, {"MovieId": "11826", "Title": "Sexy Beast", "Casts": [], "PlotId": "11826", + "ThumbnailIds": ["/rpo9njGpJVLPqRrdM6R7wIqiQ7K.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 354}, + {"MovieId": "318846", "Title": "The Big Short", "Casts": [], "PlotId": "318846", + "ThumbnailIds": ["/p11Ftd4VposrAzthkhF53ifYZRl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 4562}, + {"MovieId": "74997", "Title": "The Human Centipede 2 (Full Sequence)", "Casts": [], "PlotId": "74997", + "ThumbnailIds": ["/kFtAdkCO0vXN2RWu2oMcR9GZ9Hi.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.6, + "NumRating": 580}, {"MovieId": "339380", "Title": "On the Basis of Sex", "Casts": [], "PlotId": "339380", + "ThumbnailIds": ["/izY9Le3QWtu7xkHq7bjJnuE5yGI.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 187}, + {"MovieId": "10756", "Title": "The Haunted Mansion", "Casts": [], "PlotId": "10756", + "ThumbnailIds": ["/lGi5yio4pdDz5PkSeZCbnMQz5vK.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 998}, {"MovieId": "24122", "Title": "The Rebound", "Casts": [], "PlotId": "24122", + "ThumbnailIds": ["/nkkFebe8ZgE843rnTrlaRWJy0g9.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 393}, + {"MovieId": "10016", "Title": "Ghosts of Mars", "Casts": [], "PlotId": "10016", + "ThumbnailIds": ["/rBmkaKxRg55zBZr11EGbedFJM0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.0, + "NumRating": 483}, {"MovieId": "242582", "Title": "Nightcrawler", "Casts": [], "PlotId": "242582", + "ThumbnailIds": ["/8oPY6ULFOTbAEskySNhgsUIN4fW.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.7, "NumRating": 5760}, + {"MovieId": "66", "Title": "Absolute Power", "Casts": [], "PlotId": "66", + "ThumbnailIds": ["/oJQdp09Oc51DkArsMDvgDLdWiDu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 384}, {"MovieId": "304357", "Title": "Woman in Gold", "Casts": [], "PlotId": "304357", + "ThumbnailIds": ["/mNg0HRIhJyPgKo3NoxD3rfw061O.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 780}, + {"MovieId": "333339", "Title": "Ready Player One", "Casts": [], "PlotId": "333339", + "ThumbnailIds": ["/pU1ULUq8D3iRxl1fdX2lZIzdHuI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 6795}, {"MovieId": "8984", "Title": "Disclosure", "Casts": [], "PlotId": "8984", + "ThumbnailIds": ["/qbwcjmHnXkeE8l7wNBsdnlrp3J4.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.0, "NumRating": 392}, + {"MovieId": "10466", "Title": "The Money Pit", "Casts": [], "PlotId": "10466", + "ThumbnailIds": ["/vzUfCSAXDIQemW6Kdk2RU2JtfiV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 468}, {"MovieId": "5511", "Title": "Le Samoura\u00ef", "Casts": [], "PlotId": "5511", + "ThumbnailIds": ["/axuBeLVBeXfVZPGg6ph2taWRDFq.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.0, "NumRating": 351}, + {"MovieId": "4176", "Title": "Murder on the Orient Express", "Casts": [], "PlotId": "4176", + "ThumbnailIds": ["/66B9pHWl2KU7CwB3xcBNYvEo2CH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 560}, {"MovieId": "286217", "Title": "The Martian", "Casts": [], "PlotId": "286217", + "ThumbnailIds": ["/5aGhaIHYuQbqlHWvWYqMCnj40y2.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.7, "NumRating": 12004}, + {"MovieId": "331962", "Title": "Exposed", "Casts": [], "PlotId": "331962", + "ThumbnailIds": ["/nM26QosEfgjEegONCiNNrYMBTxD.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.6, + "NumRating": 243}, {"MovieId": "11866", "Title": "Flight of the Phoenix", "Casts": [], "PlotId": "11866", + "ThumbnailIds": ["/g1wvC9RyapnPQBgKcFJ3FChPEeC.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 427}, + {"MovieId": "606", "Title": "Out of Africa", "Casts": [], "PlotId": "606", + "ThumbnailIds": ["/gYNfg38sM4aSpxfC8gPkwg5UZHN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 573}, {"MovieId": "191714", "Title": "The Lunchbox", "Casts": [], "PlotId": "191714", + "ThumbnailIds": ["/xFJqU1W5WlJiKr4Witnb7h9HNHn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 347}, + {"MovieId": "87101", "Title": "Terminator Genisys", "Casts": [], "PlotId": "87101", + "ThumbnailIds": ["/5JU9ytZJyR3zmClGmVm9q4Geqbd.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 5248}, {"MovieId": "238636", "Title": "The Purge: Anarchy", "Casts": [], "PlotId": "238636", + "ThumbnailIds": ["/l1DRl40x2OWUoPP42v8fjKdS1Z3.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.6, "NumRating": 3694}, + {"MovieId": "121875", "Title": "In the House", "Casts": [], "PlotId": "121875", + "ThumbnailIds": ["/4qAnWMNKUadx10jlCrkRAAuT4bN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 416}, {"MovieId": "2275", "Title": "The General's Daughter", "Casts": [], "PlotId": "2275", + "ThumbnailIds": ["/4fc4mB9tGgYV4mdJrWPLMfcRvnC.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 367}, + {"MovieId": "396461", "Title": "Under the Silver Lake", "Casts": [], "PlotId": "396461", + "ThumbnailIds": ["/771Ey73LqsE9ORJhQCI25rgMXS2.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 305}, + {"MovieId": "11418", "Title": "National Lampoon's European Vacation", "Casts": [], "PlotId": "11418", + "ThumbnailIds": ["/6AhVFxvDs2WZgfpC0bm2n2mshaa.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 365}, + {"MovieId": "10437", "Title": "The Muppet Christmas Carol", "Casts": [], "PlotId": "10437", + "ThumbnailIds": ["/fe4nLJkDNXhyuMnrZ60MfoSpPes.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 415}, {"MovieId": "75802", "Title": "Hysteria", "Casts": [], "PlotId": "75802", + "ThumbnailIds": ["/1vtHZYTBS5iRA5ADibY6cwU7Ffm.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 448}, + {"MovieId": "246655", "Title": "X-Men: Apocalypse", "Casts": [], "PlotId": "246655", + "ThumbnailIds": ["/zSouWWrySXshPCT4t3UKCQGayyo.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 8149}, {"MovieId": "10975", "Title": "Operation Condor", "Casts": [], "PlotId": "10975", + "ThumbnailIds": ["/loOps7gaDy1h6FTxQQdmh1L9TXn.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.1, "NumRating": 189}, + {"MovieId": "338970", "Title": "Tomb Raider", "Casts": [], "PlotId": "338970", + "ThumbnailIds": ["/3zrC5tUiR35rTz9stuIxnU1nUS5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 4176}, {"MovieId": "597", "Title": "Titanic", "Casts": [], "PlotId": "597", + "ThumbnailIds": ["/kHXEpyfl6zqn8a6YuozZUujufXf.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.8, "NumRating": 13986}, + {"MovieId": "22586", "Title": "The Swan Princess", "Casts": [], "PlotId": "22586", + "ThumbnailIds": ["/oYp2bckWNBKOIbznjAfypVoMOFl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 496}, {"MovieId": "10029", "Title": "Very Bad Things", "Casts": [], "PlotId": "10029", + "ThumbnailIds": ["/rGZ4bR7dNcAWntnVY5R8kge2IVq.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 303}, + {"MovieId": "245168", "Title": "Suffragette", "Casts": [], "PlotId": "245168", + "ThumbnailIds": ["/gG2Y8GNXzMrZCx65a8EhdNq2iuu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 961}, {"MovieId": "1975", "Title": "The Grudge 2", "Casts": [], "PlotId": "1975", + "ThumbnailIds": ["/lPN7FyCuPDHwoupcuPT72cRE8lY.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 486}, + {"MovieId": "273477", "Title": "Scouts Guide to the Zombie Apocalypse", "Casts": [], "PlotId": "273477", + "ThumbnailIds": ["/3wHxtemithpFQiB7ffa50ZKI6bz.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 900}, {"MovieId": "8392", "Title": "My Neighbor Totoro", "Casts": [], "PlotId": "8392", + "ThumbnailIds": ["/2i0OOjvi7CqNQA6ZtYJtL65P9oZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.1, "NumRating": 3193}, + {"MovieId": "86597", "Title": "The Tall Man", "Casts": [], "PlotId": "86597", + "ThumbnailIds": ["/1fc87E3ppCq6xBnZJ63Sa9FKRUd.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 558}, {"MovieId": "10648", "Title": "Magnum Force", "Casts": [], "PlotId": "10648", + "ThumbnailIds": ["/3gqV4jpKNFqxUWug3BRD6yUzSL1.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 411}, + {"MovieId": "559", "Title": "Spider-Man 3", "Casts": [], "PlotId": "559", + "ThumbnailIds": ["/2N9lhZg6VtVJoGCZDjXVC3a81Ea.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 6969}, + {"MovieId": "527729", "Title": "Asterix: The Secret of the Magic Potion", "Casts": [], "PlotId": "527729", + "ThumbnailIds": ["/wmMq5ypRNJbWpdhC9aPjpdx1MMp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 341}, {"MovieId": "10052", "Title": "Dragonfly", "Casts": [], "PlotId": "10052", + "ThumbnailIds": ["/gs3fFBoiEaExy5x3m5yAQpmVhXs.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 370}, + {"MovieId": "397", "Title": "French Kiss", "Casts": [], "PlotId": "397", + "ThumbnailIds": ["/bZqvonwVru1XtnniQZgZRa3Znyk.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 331}, {"MovieId": "4477", "Title": "The Devil's Own", "Casts": [], "PlotId": "4477", + "ThumbnailIds": ["/87ObtgniFhhPgESlIl85MfawduY.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 502}, + {"MovieId": "150689", "Title": "Cinderella", "Casts": [], "PlotId": "150689", + "ThumbnailIds": ["/2i0JH5WqYFqki7WDhUW56Sg0obh.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 4437}, {"MovieId": "44114", "Title": "3some", "Casts": [], "PlotId": "44114", + "ThumbnailIds": ["/bUaAybF88Gm9eKrcjB0lewx55Y3.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.0, "NumRating": 14}, + {"MovieId": "4978", "Title": "An American Tail", "Casts": [], "PlotId": "4978", + "ThumbnailIds": ["/glZNfxN4cef0pJeD08xru7ZVWlI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 649}, {"MovieId": "11880", "Title": "Dog Soldiers", "Casts": [], "PlotId": "11880", + "ThumbnailIds": ["/kJLGyrjnphk6DnaJhYVFFoWJcCy.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 346}, + {"MovieId": "9754", "Title": "Firewall", "Casts": [], "PlotId": "9754", + "ThumbnailIds": ["/bp0BTphk7bgBof20XAfKkujRxwI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 440}, {"MovieId": "320288", "Title": "Dark Phoenix", "Casts": [], "PlotId": "320288", + "ThumbnailIds": ["/kZv92eTc0Gg3mKxqjjDAM73z9cy.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "9538", "Title": "Scanners", "Casts": [], "PlotId": "9538", + "ThumbnailIds": ["/gl7uT1nm7kpi4Fv0YMgv2C1dGj3.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 441}, {"MovieId": "9530", "Title": "RV", "Casts": [], "PlotId": "9530", + "ThumbnailIds": ["/eqV0JjfwcEJuK3JPZ2rsNvS1p30.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 888}, + {"MovieId": "84165", "Title": "2 Days in New York", "Casts": [], "PlotId": "84165", + "ThumbnailIds": ["/7Zo1oQ7ssDfT3OKUAWqu9TD4J0H.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 148}, {"MovieId": "80038", "Title": "Friends with Kids", "Casts": [], "PlotId": "80038", + "ThumbnailIds": ["/ojCTu5O2scmRjlX26eS3EZbhjPb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 257}, + {"MovieId": "2623", "Title": "An Officer and a Gentleman", "Casts": [], "PlotId": "2623", + "ThumbnailIds": ["/1fveHeEaOpGW91ZM4ViT0Jviuak.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 441}, {"MovieId": "10481", "Title": "102 Dalmatians", "Casts": [], "PlotId": "10481", + "ThumbnailIds": ["/dSxnIika9yWwTvEbpsmoGdeh65E.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 678}, + {"MovieId": "136", "Title": "Freaks", "Casts": [], "PlotId": "136", + "ThumbnailIds": ["/9hmmE4K6tOEXB1KeTajIC0pNta6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 426}, {"MovieId": "135397", "Title": "Jurassic World", "Casts": [], "PlotId": "135397", + "ThumbnailIds": ["/jjBgi2r5cRt36xF6iNUEhzscEcb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 13870}, + {"MovieId": "10934", "Title": "Under the Tuscan Sun", "Casts": [], "PlotId": "10934", + "ThumbnailIds": ["/z5mqOKxS8R9mnzyRulIbMX0U6kG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 329}, {"MovieId": "178809", "Title": "The Colony", "Casts": [], "PlotId": "178809", + "ThumbnailIds": ["/oYeCfvBSdIfxGSlSXMwtAUzKiJC.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.2, "NumRating": 624}, + {"MovieId": "198185", "Title": "Million Dollar Arm", "Casts": [], "PlotId": "198185", + "ThumbnailIds": ["/rYOQL42cDdsbhfgaxeEW4SlszUp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 348}, {"MovieId": "11782", "Title": "Hard Boiled", "Casts": [], "PlotId": "11782", + "ThumbnailIds": ["/8CfUC5QPTNe5NHlXWvYzpKIl3xs.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.6, "NumRating": 290}, + {"MovieId": "3604", "Title": "Flash Gordon", "Casts": [], "PlotId": "3604", + "ThumbnailIds": ["/9c5vKUX7MB1yixVp0ZFAUDRTaeE.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 398}, {"MovieId": "9303", "Title": "Bound", "Casts": [], "PlotId": "9303", + "ThumbnailIds": ["/xfITNjW2sunPiB7BNotJJsCxhdA.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 341}, + {"MovieId": "143", "Title": "All Quiet on the Western Front", "Casts": [], "PlotId": "143", + "ThumbnailIds": ["/yAU6jklJLUjZot3WyvyJrxVdLKb.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 290}, {"MovieId": "336004", "Title": "Heist", "Casts": [], "PlotId": "336004", + "ThumbnailIds": ["/t5tGykRvvlLBULIPsAJEzGg1ylm.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 492}, + {"MovieId": "10057", "Title": "The Three Musketeers", "Casts": [], "PlotId": "10057", + "ThumbnailIds": ["/mk8UH7JRmK8adcqJJpB1ygP7B1C.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 385}, {"MovieId": "4437", "Title": "2010", "Casts": [], "PlotId": "4437", + "ThumbnailIds": ["/9Rcz2n16HEYRi2EKGliByP6ESYR.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 426}, + {"MovieId": "3063", "Title": "Duck Soup", "Casts": [], "PlotId": "3063", + "ThumbnailIds": ["/jy4DJN8pKmEz4UNjCGiFaJuMfDJ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 320}, {"MovieId": "457136", "Title": "Mary Queen of Scots", "Casts": [], "PlotId": "457136", + "ThumbnailIds": ["/b5RMzLAyq5QW6GtN9sIeAEMLlBI.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 530}, + {"MovieId": "41602", "Title": "Charlie Countryman", "Casts": [], "PlotId": "41602", + "ThumbnailIds": ["/jG95rLdI1V1pTEtAwSkKZR74AGm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 364}, {"MovieId": "333385", "Title": "Mr. Right", "Casts": [], "PlotId": "333385", + "ThumbnailIds": ["/y1VT2NoBOx3aC2exhkyN9AGUkMR.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 727}, + {"MovieId": "9992", "Title": "Arthur and the Invisibles", "Casts": [], "PlotId": "9992", + "ThumbnailIds": ["/idbP413bzKWrQXJyElrDazcJUFM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 1440}, + {"MovieId": "1494", "Title": "Curse of the Golden Flower", "Casts": [], "PlotId": "1494", + "ThumbnailIds": ["/8JGOTkKOCqSfAwIMPQKF0K44lUW.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 346}, {"MovieId": "172533", "Title": "Drinking Buddies", "Casts": [], "PlotId": "172533", + "ThumbnailIds": ["/zongyslIHQmfnf9rgUioPkDaHmq.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 522}, + {"MovieId": "674", "Title": "Harry Potter and the Goblet of Fire", "Casts": [], "PlotId": "674", + "ThumbnailIds": ["/6sASqcdrEHXxUhA3nFpjrRecPD2.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 10908}, {"MovieId": "12192", "Title": "Pathology", "Casts": [], "PlotId": "12192", + "ThumbnailIds": ["/7VjVRhlM7GyqwTc3OH8Itfx4jaB.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.7, "NumRating": 207}, + {"MovieId": "1825", "Title": "Over the Top", "Casts": [], "PlotId": "1825", + "ThumbnailIds": ["/rnAxGFrFaHemdjjFDQDtjOzV8z5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 664}, {"MovieId": "8587", "Title": "The Lion King", "Casts": [], "PlotId": "8587", + "ThumbnailIds": ["/bKPtXn9n4M4s8vvZrbw40mYsefB.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.2, "NumRating": 9974}, + {"MovieId": "10477", "Title": "Driven", "Casts": [], "PlotId": "10477", + "ThumbnailIds": ["/vtPvxgQBoNBFnnnYjS1lb6pLXtv.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.7, + "NumRating": 315}, + {"MovieId": "131634", "Title": "The Hunger Games: Mockingjay - Part 2", "Casts": [], "PlotId": "131634", + "ThumbnailIds": ["/w93GAiq860UjmgR6tU9h2T24vaV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 7485}, + {"MovieId": "324552", "Title": "John Wick: Chapter 2", "Casts": [], "PlotId": "324552", + "ThumbnailIds": ["/zkXnKIwX5pYorKJp2fjFSfNyKT0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 5487}, + {"MovieId": "6279", "Title": "Sister Act 2: Back in the Habit", "Casts": [], "PlotId": "6279", + "ThumbnailIds": ["/tLbvfUYw8Pr7DMI5TGL5DX1bx5S.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 811}, {"MovieId": "24150", "Title": "Halloween II", "Casts": [], "PlotId": "24150", + "ThumbnailIds": ["/vSHPM4LQDpWdQrD5KZWK6wNqSOD.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.2, "NumRating": 555}, + {"MovieId": "564394", "Title": "Crypto", "Casts": [], "PlotId": "564394", "ThumbnailIds": [None], + "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "435577", "Title": "The 12th Man", "Casts": [], "PlotId": "435577", + "ThumbnailIds": ["/dxZ8iGf5jQoLcNqBJL6uzNTivp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 118}, {"MovieId": "87499", "Title": "The East", "Casts": [], "PlotId": "87499", + "ThumbnailIds": ["/nGoFIX5WmthZZ3eLBd0QwDSkWNy.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 433}, + {"MovieId": "443055", "Title": "Love of My Life", "Casts": [], "PlotId": "443055", + "ThumbnailIds": ["/7b19Sh0Aef5vGa0OFtvJxLe2SK9.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 8}, {"MovieId": "238", "Title": "The Godfather", "Casts": [], "PlotId": "238", + "ThumbnailIds": ["/rPdtLWNsZmAtoZl9PK7S2wE3qiS.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.6, "NumRating": 9858}, + {"MovieId": "8869", "Title": "Eight Legged Freaks", "Casts": [], "PlotId": "8869", + "ThumbnailIds": ["/udEgrVBvcXV5tGdozogGsOpD1Rs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 504}, {"MovieId": "4964", "Title": "Knocked Up", "Casts": [], "PlotId": "4964", + "ThumbnailIds": ["/b4OaXw2MW97VvIiZE0Sbn1NfxSh.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 2118}, + {"MovieId": "49017", "Title": "Dracula Untold", "Casts": [], "PlotId": "49017", + "ThumbnailIds": ["/4oy4e0DP6LRwRszfx8NY8EYBj8V.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 3679}, {"MovieId": "11527", "Title": "Excalibur", "Casts": [], "PlotId": "11527", + "ThumbnailIds": ["/j8UmbdA1TrIVY4FANymwBSmUuCH.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.1, "NumRating": 431}, + {"MovieId": "39210", "Title": "Somewhere", "Casts": [], "PlotId": "39210", + "ThumbnailIds": ["/d3A0bbRp6sHN4uz6n3ddbtyL1tt.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 370}, + {"MovieId": "166426", "Title": "Pirates of the Caribbean: Dead Men Tell No Tales", "Casts": [], + "PlotId": "166426", "ThumbnailIds": ["/xbpSDU3p7YUGlu9Mr6Egg2Vweto.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 6804}, + {"MovieId": "12445", "Title": "Harry Potter and the Deathly Hallows: Part 2", "Casts": [], + "PlotId": "12445", "ThumbnailIds": ["/fTplI1NCSuEDP4ITLcTps739fcC.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.1, "NumRating": 11352}, + {"MovieId": "8689", "Title": "Cannibal Holocaust", "Casts": [], "PlotId": "8689", + "ThumbnailIds": ["/r0VmAFHWAqlaraVg0krIEhDCPWH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 612}, {"MovieId": "22949", "Title": "Old Dogs", "Casts": [], "PlotId": "22949", + "ThumbnailIds": ["/2B4tOJ71vJQktyjsG0iqGM0Yh10.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 351}, + {"MovieId": "156022", "Title": "The Equalizer", "Casts": [], "PlotId": "156022", + "ThumbnailIds": ["/2eQfjqlvPAxd9aLDs8DvsKLnfed.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 4883}, {"MovieId": "190469", "Title": "Redirected", "Casts": [], "PlotId": "190469", + "ThumbnailIds": ["/dbmXS7kEJxhAqVVmm5vpmDYpiqh.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.8, "NumRating": 211}, + {"MovieId": "18148", "Title": "Tokyo Story", "Casts": [], "PlotId": "18148", + "ThumbnailIds": ["/g2YbTYKpY7N2yDSk7BfXZ18I5QV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.3, + "NumRating": 338}, + {"MovieId": "10539", "Title": "James and the Giant Peach", "Casts": [], "PlotId": "10539", + "ThumbnailIds": ["/kyAl5UfUtGJC1wHrPVieqKKCpn8.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 635}, {"MovieId": "11932", "Title": "Bride of Chucky", "Casts": [], "PlotId": "11932", + "ThumbnailIds": ["/u5Lc1Li0Hpc452o57E2KaToezZX.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.6, "NumRating": 702}, + {"MovieId": "103620", "Title": "Maniac", "Casts": [], "PlotId": "103620", + "ThumbnailIds": ["/ag1IgAqYartblOy0IiDIMNoJUVI.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 461}, {"MovieId": "10163", "Title": "The Lawnmower Man", "Casts": [], "PlotId": "10163", + "ThumbnailIds": ["/3tWLM3zMyh3KZOactn8mfjHml05.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 317}, + {"MovieId": "9815", "Title": "Goal! II: Living the Dream", "Casts": [], "PlotId": "9815", + "ThumbnailIds": ["/sKKej989oVbZahldAkrMVmyB8pU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 313}, + {"MovieId": "295699", "Title": "Everybody Wants Some!!", "Casts": [], "PlotId": "295699", + "ThumbnailIds": ["/mIpd0rGxruvxCnMSmh4gd8wuhmv.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 669}, {"MovieId": "9289", "Title": "The Longest Day", "Casts": [], "PlotId": "9289", + "ThumbnailIds": ["/7fnuirXJpuRHggi2lOCBEwZ3eWU.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 406}, + {"MovieId": "177572", "Title": "Big Hero 6", "Casts": [], "PlotId": "177572", + "ThumbnailIds": ["/9gLu47Zw5ertuFTZaxXOvNfy78T.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 10037}, {"MovieId": "25643", "Title": "Love Happens", "Casts": [], "PlotId": "25643", + "ThumbnailIds": ["/pN51u0l8oSEsxAYiHUzzbMrMXH7.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.9, "NumRating": 345}, + {"MovieId": "11212", "Title": "Baby's Day Out", "Casts": [], "PlotId": "11212", + "ThumbnailIds": ["/21U2jwl36hoTHsXB3fDuIQkcchu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 565}, {"MovieId": "65759", "Title": "Happy Feet Two", "Casts": [], "PlotId": "65759", + "ThumbnailIds": ["/2gWiQ4mn85jcXtREVePlVViupeV.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 736}, + {"MovieId": "38234", "Title": "Undisputed III: Redemption", "Casts": [], "PlotId": "38234", + "ThumbnailIds": ["/vLSnWLCnqk5j3oLnMnBHJGL3bO0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 296}, {"MovieId": "5375", "Title": "Fred Claus", "Casts": [], "PlotId": "5375", + "ThumbnailIds": ["/vFniWmciRi4tAjAYGp2wrK0P8dJ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 406}, + {"MovieId": "8409", "Title": "A Man Apart", "Casts": [], "PlotId": "8409", + "ThumbnailIds": ["/l9UIm6rCHzfbMy6KY8ynjV4kLHX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 406}, {"MovieId": "8090", "Title": "Untraceable", "Casts": [], "PlotId": "8090", + "ThumbnailIds": ["/ySUwDRDEn01lKIMPQorpFCMLWqE.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 392}, + {"MovieId": "93", "Title": "Anatomy of a Murder", "Casts": [], "PlotId": "93", + "ThumbnailIds": ["/kDFnM2zlHZirR2ItTo56lyrxuAS.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 347}, {"MovieId": "553100", "Title": "Wild and Free", "Casts": [], "PlotId": "553100", + "ThumbnailIds": ["/jLGNqaymD0ygyhafhv5fM3nXcge.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 10.0, "NumRating": 2}, + {"MovieId": "200505", "Title": "Draft Day", "Casts": [], "PlotId": "200505", + "ThumbnailIds": ["/pb5FXL6pypVQbcs3TCzp5GqyTYr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 454}, {"MovieId": "10610", "Title": "The Medallion", "Casts": [], "PlotId": "10610", + "ThumbnailIds": ["/9iJrg37ceCBeQkQHnIy97VekINb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 501}, + {"MovieId": "16577", "Title": "Astro Boy", "Casts": [], "PlotId": "16577", + "ThumbnailIds": ["/4kQczIhUFTnDWwG6HKsgCxLoi6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 706}, {"MovieId": "330947", "Title": "Song to Song", "Casts": [], "PlotId": "330947", + "ThumbnailIds": ["/rEvtGhNY2DQ4L8Ma6rpMhL6IbKM.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 463}, + {"MovieId": "514692", "Title": "Neon Heart", "Casts": [], "PlotId": "514692", + "ThumbnailIds": ["/RgniFwvK2vL3yapRrz4GdGZAnE.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "449985", "Title": "Triple Threat", "Casts": [], "PlotId": "449985", + "ThumbnailIds": ["/cSpM3QxmoSLp4O1WAMQpUDcaB7R.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 75}, + {"MovieId": "10731", "Title": "The Client", "Casts": [], "PlotId": "10731", + "ThumbnailIds": ["/bCWpxGGcP9DsCLwfNpnfcl1vLk8.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 500}, {"MovieId": "209276", "Title": "Starred Up", "Casts": [], "PlotId": "209276", + "ThumbnailIds": ["/cP5vXTItgkNahGEsaIHudbdIDRG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 521}, + {"MovieId": "10623", "Title": "Cradle 2 the Grave", "Casts": [], "PlotId": "10623", + "ThumbnailIds": ["/v8iPcn54TNsSPabD9ZYQVQUWbXk.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 379}, {"MovieId": "9741", "Title": "Unbreakable", "Casts": [], "PlotId": "9741", + "ThumbnailIds": ["/pvL37V88plePxFSszCbV3wRHiBm.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 4835}, + {"MovieId": "862", "Title": "Toy Story", "Casts": [], "PlotId": "862", + "ThumbnailIds": ["/rhIRbceoE9lR4veEXuwCC2wARtG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 9986}, {"MovieId": "505948", "Title": "I Am Mother", "Casts": [], "PlotId": "505948", + "ThumbnailIds": ["/eItrj5GcjvCI3oD3bIcz1A2IL9t.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 0.0, "NumRating": 1}, + {"MovieId": "438740", "Title": "Salyut-7", "Casts": [], "PlotId": "438740", + "ThumbnailIds": ["/s2ktgF2ze4aVt9bXBUvxFJllqyd.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 146}, {"MovieId": "369972", "Title": "First Man", "Casts": [], "PlotId": "369972", + "ThumbnailIds": ["/i91mfvFcPPlaegcbOyjGgiWfZzh.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 2180}, + {"MovieId": "14536", "Title": "New in Town", "Casts": [], "PlotId": "14536", + "ThumbnailIds": ["/3C3WWizgIKWQ5O6k3xN5pkwXEYJ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 211}, {"MovieId": "10518", "Title": "Marathon Man", "Casts": [], "PlotId": "10518", + "ThumbnailIds": ["/uPNgubSiri2yvBQRPtP77ViYjN.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 397}, + {"MovieId": "9053", "Title": "DOA: Dead or Alive", "Casts": [], "PlotId": "9053", + "ThumbnailIds": ["/w1DbRfGKWWS3JR74q4vawHVQy5B.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.0, + "NumRating": 362}, {"MovieId": "426563", "Title": "Holmes & Watson", "Casts": [], "PlotId": "426563", + "ThumbnailIds": ["/orEUlKndjV1rEcWqXbbjegjfv97.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.2, "NumRating": 181}, + {"MovieId": "9972", "Title": "Lock Up", "Casts": [], "PlotId": "9972", + "ThumbnailIds": ["/wRWZDNzebz2a52GtdhN1bx3ujE7.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 382}, {"MovieId": "4961", "Title": "Mimic", "Casts": [], "PlotId": "4961", + "ThumbnailIds": ["/tkmSgkekKleTx46a1ERny967Ws5.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 418}, + {"MovieId": "249", "Title": "The War of the Roses", "Casts": [], "PlotId": "249", + "ThumbnailIds": ["/9VWwYsuXhRImUtrJGvN6bYJB2He.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 499}, {"MovieId": "884", "Title": "Crash", "Casts": [], "PlotId": "884", + "ThumbnailIds": ["/4PDLGJsS5uVBQlYjLMZ7t85dU0P.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 390}, + {"MovieId": "9356", "Title": "Look Who's Talking Too", "Casts": [], "PlotId": "9356", + "ThumbnailIds": ["/7HtSFUYBPOUd9ylUXU0LyPsxpRm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.2, + "NumRating": 655}, {"MovieId": "324670", "Title": "Spectral", "Casts": [], "PlotId": "324670", + "ThumbnailIds": ["/oXV2ayQYUQfHwpuMdWnZF0Geng5.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 813}, + {"MovieId": "1280", "Title": "3-Iron", "Casts": [], "PlotId": "1280", + "ThumbnailIds": ["/hr8ghKbdo3UGUROYafpN38Sohfe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 444}, {"MovieId": "8879", "Title": "Pale Rider", "Casts": [], "PlotId": "8879", + "ThumbnailIds": ["/7C18VUCvZH5O0ibZogZP4TQJTiu.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 420}, + {"MovieId": "12160", "Title": "Wyatt Earp", "Casts": [], "PlotId": "12160", + "ThumbnailIds": ["/zSGDgcWHqmQxmX4mLJZgT5UgLjj.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 341}, {"MovieId": "327331", "Title": "The Dirt", "Casts": [], "PlotId": "327331", + "ThumbnailIds": ["/xGY5rr8441ib0lT9mtHZn7e8Aay.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 265}, + {"MovieId": "10328", "Title": "Cocoon", "Casts": [], "PlotId": "10328", + "ThumbnailIds": ["/foIhEPQoqDctfwsHmmYwbNz5A2g.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 564}, {"MovieId": "9696", "Title": "Ichi the Killer", "Casts": [], "PlotId": "9696", + "ThumbnailIds": ["/mkmwESVpq7KrcxdPMreTFqlNF0S.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 379}, + {"MovieId": "11565", "Title": "Big Momma's House 2", "Casts": [], "PlotId": "11565", + "ThumbnailIds": ["/tOl6hzfFHdNcL7SxopC2Vbs4mgK.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 777}, + {"MovieId": "321612", "Title": "Beauty and the Beast", "Casts": [], "PlotId": "321612", + "ThumbnailIds": ["/tWqifoYuwLETmmasnGHO7xBjEtt.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 10886}, {"MovieId": "2163", "Title": "Breakdown", "Casts": [], "PlotId": "2163", + "ThumbnailIds": ["/uj5VmM4jrn3HHeuFfEH8XQizA2g.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.6, "NumRating": 348}, + {"MovieId": "77805", "Title": "Lovelace", "Casts": [], "PlotId": "77805", + "ThumbnailIds": ["/6J4zI97usxdjhzobBkqFW1d9OQ5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 360}, {"MovieId": "50725", "Title": "Take Me Home Tonight", "Casts": [], "PlotId": "50725", + "ThumbnailIds": ["/glRBoVKClP7qYQO0gQi5keCQ6ko.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 326}, + {"MovieId": "643", "Title": "Battleship Potemkin", "Casts": [], "PlotId": "643", + "ThumbnailIds": ["/tjnaRiHUsxBADaOwrQpnTnjHVwi.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 458}, {"MovieId": "916", "Title": "Bullitt", "Casts": [], "PlotId": "916", + "ThumbnailIds": ["/oyhnoFu2oKQfAIdu9YU8I8Ne0pX.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 437}, + {"MovieId": "336050", "Title": "Son of Saul", "Casts": [], "PlotId": "336050", + "ThumbnailIds": ["/AcjoM9JielY0Yi42GnICNBntpND.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 493}, + {"MovieId": "335988", "Title": "Transformers: The Last Knight", "Casts": [], "PlotId": "335988", + "ThumbnailIds": ["/s5HQf2Gb3lIO2cRcFwNL9sn1o1o.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 3204}, {"MovieId": "16558", "Title": "Duplicity", "Casts": [], "PlotId": "16558", + "ThumbnailIds": ["/vpWQs3CjwG6Er3DgZlv4L3NFjXg.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.9, "NumRating": 346}, + {"MovieId": "9594", "Title": "Double Impact", "Casts": [], "PlotId": "9594", + "ThumbnailIds": ["/sRxg7BI6y5dXa5SX0RZS3zlhxwc.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 371}, {"MovieId": "10712", "Title": "Far from Heaven", "Casts": [], "PlotId": "10712", + "ThumbnailIds": ["/7lzlqQmtv4z2CEiiBWU55blk1zo.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 249}, + {"MovieId": "11074", "Title": "Striking Distance", "Casts": [], "PlotId": "11074", + "ThumbnailIds": ["/1bLazrQWJQhO6u58vJZXJZ3ZFDh.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 363}, {"MovieId": "399055", "Title": "The Shape of Water", "Casts": [], "PlotId": "399055", + "ThumbnailIds": ["/k4FwHlMhuRR5BISY2Gm2QZHlH5Q.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 7081}, + {"MovieId": "13416", "Title": "Friday Night Lights", "Casts": [], "PlotId": "13416", + "ThumbnailIds": ["/8HIqpOgqShRc3TAleabnYispDl1.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 248}, {"MovieId": "300668", "Title": "Annihilation", "Casts": [], "PlotId": "300668", + "ThumbnailIds": ["/d3qcpfNwbAMCNqWDHzPQsUYiUgS.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 4359}, + {"MovieId": "119450", "Title": "Dawn of the Planet of the Apes", "Casts": [], "PlotId": "119450", + "ThumbnailIds": ["/2EUAUIu5lHFlkj5FRryohlH6CRO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 6984}, {"MovieId": "11386", "Title": "The Crying Game", "Casts": [], "PlotId": "11386", + "ThumbnailIds": ["/9Qqk3svM0I9QuabOPDNrJZdv3XJ.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.0, "NumRating": 307}, + {"MovieId": "10589", "Title": "After the Sunset", "Casts": [], "PlotId": "10589", + "ThumbnailIds": ["/seEavscJqc1TuHfSZMeXQwohkNf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 340}, {"MovieId": "49026", "Title": "The Dark Knight Rises", "Casts": [], "PlotId": "49026", + "ThumbnailIds": ["/dEYnvnUfXrqvqeRSqvIEtmzhoA8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.7, "NumRating": 13731}, + {"MovieId": "1552", "Title": "Parenthood", "Casts": [], "PlotId": "1552", + "ThumbnailIds": ["/e51tNNQBJpJi9xkyuj0QFhyBcz7.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 267}, {"MovieId": "13944", "Title": "Passengers", "Casts": [], "PlotId": "13944", + "ThumbnailIds": ["/fGI0FFre8W454JqHgexV6zDWC9H.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 344}, + {"MovieId": "524309", "Title": "The Gift", "Casts": [], "PlotId": "524309", + "ThumbnailIds": ["/oiymQKFIqbCbWamrQ5EbOYdmjvn.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, + {"MovieId": "9385", "Title": "The Twelve Tasks of Asterix", "Casts": [], "PlotId": "9385", + "ThumbnailIds": ["/7cZQZOYZFJcLjZnxWjO5PcUtmDZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 501}, {"MovieId": "36419", "Title": "After.Life", "Casts": [], "PlotId": "36419", + "ThumbnailIds": ["/8mtT1Hqp63s3DiGxZckxQj0Mioy.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.6, "NumRating": 406}, + {"MovieId": "775", "Title": "A Trip to the Moon", "Casts": [], "PlotId": "775", + "ThumbnailIds": ["/zztHYAfSecYuaDyIjQudjKaOLLY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 672}, {"MovieId": "931", "Title": "Don't Look Now", "Casts": [], "PlotId": "931", + "ThumbnailIds": ["/qRUMQN3fa43ZuEldhNG7UYoURDG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 336}, + {"MovieId": "77875", "Title": "Playing for Keeps", "Casts": [], "PlotId": "77875", + "ThumbnailIds": ["/eOBekVFBGK4TwETQSwfuk22B41o.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 410}, {"MovieId": "10876", "Title": "Quills", "Casts": [], "PlotId": "10876", + "ThumbnailIds": ["/4fz5LDFsapXDJVLsy2jh5VPZtR0.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 238}, + {"MovieId": "10675", "Title": "Frantic", "Casts": [], "PlotId": "10675", + "ThumbnailIds": ["/wOaBEIhiEA832ifEQ4CNxVHrh1c.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 414}, {"MovieId": "543343", "Title": "Guava Island", "Casts": [], "PlotId": "543343", + "ThumbnailIds": ["/noE2O4XaRSrNZ75MUShaQD0pFN0.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 75}, + {"MovieId": "9796", "Title": "Turistas", "Casts": [], "PlotId": "9796", + "ThumbnailIds": ["/lwj9HLVIfLaFYyF7ZIxBlntT9jM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.3, + "NumRating": 354}, {"MovieId": "11817", "Title": "Bulletproof Monk", "Casts": [], "PlotId": "11817", + "ThumbnailIds": ["/hzk3kf5h54cCP3MYDptEjmLKVLF.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 439}, + {"MovieId": "11901", "Title": "High Plains Drifter", "Casts": [], "PlotId": "11901", + "ThumbnailIds": ["/557vwDdOIQ07Q1QvTwHVmxHJpXU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 468}, {"MovieId": "49521", "Title": "Man of Steel", "Casts": [], "PlotId": "49521", + "ThumbnailIds": ["/xWlaTLnD8NJMTT9PGOD9z5re1SL.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 9624}, + {"MovieId": "228203", "Title": "McFarland, USA", "Casts": [], "PlotId": "228203", + "ThumbnailIds": ["/kV3Bk0PGwYhHLy1JppK3ZbVh7IB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 353}, {"MovieId": "261", "Title": "Cat on a Hot Tin Roof", "Casts": [], "PlotId": "261", + "ThumbnailIds": ["/tkHug8LLo9dBgkVQqvmC4sib9HB.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.8, "NumRating": 306}, + {"MovieId": "257445", "Title": "Goosebumps", "Casts": [], "PlotId": "257445", + "ThumbnailIds": ["/9yOnWCvpNr6RRhc4zhJdVqR7GKw.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 2087}, {"MovieId": "1730", "Title": "Inland Empire", "Casts": [], "PlotId": "1730", + "ThumbnailIds": ["/s5f0FbVAABEnJYKaApWORTxhiFC.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.1, "NumRating": 489}, + {"MovieId": "10632", "Title": "The Hunted", "Casts": [], "PlotId": "10632", + "ThumbnailIds": ["/k8fV0lYH9DqGtB7M681DTa8hRuN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 312}, {"MovieId": "11864", "Title": "Enemy Mine", "Casts": [], "PlotId": "11864", + "ThumbnailIds": ["/hKsZY8jHagbiTtvq8mwHV36a9ki.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 397}, + {"MovieId": "506", "Title": "Marnie", "Casts": [], "PlotId": "506", + "ThumbnailIds": ["/mwEuBWMJyebtJ1OP4W2jeRcVf3k.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 413}, + {"MovieId": "9644", "Title": "National Lampoon's Loaded Weapon 1", "Casts": [], "PlotId": "9644", + "ThumbnailIds": ["/tIdlDgiVQ4kbgVXXIlP8LswubkN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 284}, {"MovieId": "9093", "Title": "The Four Feathers", "Casts": [], "PlotId": "9093", + "ThumbnailIds": ["/1mr4V13SFK4En8f4ZdyBgnZmVar.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 259}, + {"MovieId": "419430", "Title": "Get Out", "Casts": [], "PlotId": "419430", + "ThumbnailIds": ["/1SwAVYpuLj8KsHxllTF8Dt9dSSX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 8505}, {"MovieId": "44945", "Title": "Trust", "Casts": [], "PlotId": "44945", + "ThumbnailIds": ["/oWEy6b2LxwcuZtWLmrrdBWp3hda.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.6, "NumRating": 546}, + {"MovieId": "10303", "Title": "The Jewel of the Nile", "Casts": [], "PlotId": "10303", + "ThumbnailIds": ["/iQXxVWuXmaEPhieETkThDgGGKCb.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 492}, {"MovieId": "468", "Title": "My Own Private Idaho", "Casts": [], "PlotId": "468", + "ThumbnailIds": ["/xgWJPruxsL2TGVwhZDM8YiarbEY.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 426}, + {"MovieId": "16804", "Title": "Departures", "Casts": [], "PlotId": "16804", + "ThumbnailIds": ["/aiRKdQ3CqzMv88Zlk69utBBbseO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 308}, {"MovieId": "1396", "Title": "Mirror", "Casts": [], "PlotId": "1396", + "ThumbnailIds": ["/9PknVc5uubhVLZ6ofvfJAprM9UZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.1, "NumRating": 300}, + {"MovieId": "2830", "Title": "My Boss's Daughter", "Casts": [], "PlotId": "2830", + "ThumbnailIds": ["/du7joY11DJZeXXph7FwkJ3uo7om.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.6, + "NumRating": 350}, + {"MovieId": "442062", "Title": "Goosebumps 2: Haunted Halloween", "Casts": [], "PlotId": "442062", + "ThumbnailIds": ["/kOrUF0EH2C3KHoI7tqANZMFZaTN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 395}, {"MovieId": "1714", "Title": "Fahrenheit 451", "Casts": [], "PlotId": "1714", + "ThumbnailIds": ["/78NFjrD9onl9ciKSjsENUfnTHbT.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 386}, + {"MovieId": "10160", "Title": "A Nightmare on Elm Street: The Dream Child", "Casts": [], + "PlotId": "10160", "ThumbnailIds": ["/vnorsG2pKAVqIMYsDJbKoOr4CsX.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 506}, + {"MovieId": "17130", "Title": "Crossroads", "Casts": [], "PlotId": "17130", + "ThumbnailIds": ["/egIs4cWxn0iBErFKrQ7STckjj49.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.9, + "NumRating": 287}, + {"MovieId": "102382", "Title": "The Amazing Spider-Man 2", "Casts": [], "PlotId": "102382", + "ThumbnailIds": ["/mUjWof8LHDgCZC9mFp0UYKBf1Dm.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 7085}, {"MovieId": "301", "Title": "Rio Bravo", "Casts": [], "PlotId": "301", + "ThumbnailIds": ["/gyEfGVDe5puz4wgIq6073fn8pHc.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.9, "NumRating": 471}, + {"MovieId": "3085", "Title": "His Girl Friday", "Casts": [], "PlotId": "3085", + "ThumbnailIds": ["/fmQLvnDEL9wlE6FzB1S84yskdkT.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 308}, {"MovieId": "11184", "Title": "Kinsey", "Casts": [], "PlotId": "11184", + "ThumbnailIds": ["/aOmsDieEAsRDgDOYbY8tSUoTdnA.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 214}, + {"MovieId": "10658", "Title": "Howard the Duck", "Casts": [], "PlotId": "10658", + "ThumbnailIds": ["/f2pj3SSj1GdFSrS5bUojT56umL6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 416}, {"MovieId": "9626", "Title": "Red Sonja", "Casts": [], "PlotId": "9626", + "ThumbnailIds": ["/lvRlHJeMAYlUzSD8cebfIfNOhyl.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.1, "NumRating": 352}, + {"MovieId": "140420", "Title": "Paperman", "Casts": [], "PlotId": "140420", + "ThumbnailIds": ["/3TpMBcAYH4cxCw5WoRacWodMTCG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 1107}, {"MovieId": "205321", "Title": "Sharknado", "Casts": [], "PlotId": "205321", + "ThumbnailIds": ["/6nxewN7l5XGxDqCcGkLaXJ3ljdz.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 3.8, "NumRating": 863}, + {"MovieId": "9945", "Title": "Vampires", "Casts": [], "PlotId": "9945", + "ThumbnailIds": ["/qHGawU64MeGvtU86s6V0MA7MqFV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 456}, {"MovieId": "12142", "Title": "Alone in the Dark", "Casts": [], "PlotId": "12142", + "ThumbnailIds": ["/3HsprIjUEwYfnlEf7jumGm037Bk.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 3.1, "NumRating": 276}, + {"MovieId": "38360", "Title": "The Cranes Are Flying", "Casts": [], "PlotId": "38360", + "ThumbnailIds": ["/xFE2hx2kGYcsQHttJobRajuyA6Q.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 110}, {"MovieId": "11137", "Title": "The Prince & Me", "Casts": [], "PlotId": "11137", + "ThumbnailIds": ["/nB75Ht9rppAVMj0lZWZQ51PPYUe.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 516}, + {"MovieId": "10923", "Title": "Agent Cody Banks", "Casts": [], "PlotId": "10923", + "ThumbnailIds": ["/fhK0mqqirPsckxkNisvi32A4lf6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.2, + "NumRating": 560}, + {"MovieId": "9728", "Title": "Friday the 13th Part III", "Casts": [], "PlotId": "9728", + "ThumbnailIds": ["/5wg2NZyIhcMbIBAahBODXHyJ54S.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 516}, {"MovieId": "10577", "Title": "Dracula 2000", "Casts": [], "PlotId": "10577", + "ThumbnailIds": ["/6YF56U91zP1mQvle83KA1A7CPop.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.7, "NumRating": 275}, + {"MovieId": "1649", "Title": "Bill & Ted's Bogus Journey", "Casts": [], "PlotId": "1649", + "ThumbnailIds": ["/q8ssJWstfsWHmmFdigDk4r3l5gM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 474}, {"MovieId": "9825", "Title": "Lake Placid", "Casts": [], "PlotId": "9825", + "ThumbnailIds": ["/6lE0EahK7xDOYWRH6On5uKPnwQZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 429}, + {"MovieId": "281338", "Title": "War for the Planet of the Apes", "Casts": [], "PlotId": "281338", + "ThumbnailIds": ["/ijQHiImv16vNSeZQsmih04kwn0C.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 5166}, {"MovieId": "512239", "Title": "The Corrupted", "Casts": [], "PlotId": "512239", + "ThumbnailIds": ["/tNGJw2R1l3XuLRi749GQaFua9yZ.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "10197", "Title": "Nine", "Casts": [], "PlotId": "10197", + "ThumbnailIds": ["/2PSmbXIUWgUVsXq2U1MoVj3f38g.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 308}, + {"MovieId": "24438", "Title": "Did You Hear About the Morgans?", "Casts": [], "PlotId": "24438", + "ThumbnailIds": ["/yBrceyep5ZWaU9XuodM1X5c67K6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.3, + "NumRating": 496}, {"MovieId": "8427", "Title": "I Spy", "Casts": [], "PlotId": "8427", + "ThumbnailIds": ["/6mtUJKyedvQwEKXfWzJt3vtWx1M.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 475}, + {"MovieId": "347969", "Title": "The Ridiculous 6", "Casts": [], "PlotId": "347969", + "ThumbnailIds": ["/k77xcsBtZMq9zIlTVQV7UBemzXD.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.9, + "NumRating": 661}, {"MovieId": "649", "Title": "Belle de Jour", "Casts": [], "PlotId": "649", + "ThumbnailIds": ["/rHvKWARrhNwSjbTMJrn5v4LtUJE.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 310}, + {"MovieId": "7548", "Title": "The Libertine", "Casts": [], "PlotId": "7548", + "ThumbnailIds": ["/a0gI62QrqJ7PjTsUhJlEsWOPHeU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 236}, + {"MovieId": "35552", "Title": "The Extraordinary Adventures of Ad\u00e8le Blanc-Sec", "Casts": [], + "PlotId": "35552", "ThumbnailIds": ["/og6Jv55bF0uzF1F1kpUW6Je7z0H.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 655}, + {"MovieId": "3525", "Title": "Working Girl", "Casts": [], "PlotId": "3525", + "ThumbnailIds": ["/bzPuHOl8DjfZDzwGDCIHonnHUT6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 420}, {"MovieId": "13", "Title": "Forrest Gump", "Casts": [], "PlotId": "13", + "ThumbnailIds": ["/yE5d3BUhE8hCnkMUJOo1QDoOGNz.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.4, "NumRating": 14523}, + {"MovieId": "100402", "Title": "Captain America: The Winter Soldier", "Casts": [], "PlotId": "100402", + "ThumbnailIds": ["/5TQ6YDmymBpnF005OyoB7ohZps9.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 10760}, {"MovieId": "936", "Title": "The Pink Panther", "Casts": [], "PlotId": "936", + "ThumbnailIds": ["/azIoCxiH9wIPCCGnqaDW8DJwCLl.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.1, "NumRating": 436}, + {"MovieId": "56288", "Title": "Spy Kids: All the Time in the World", "Casts": [], "PlotId": "56288", + "ThumbnailIds": ["/eHldUGDNxb4ZPQSPDnGolyFDECa.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.7, + "NumRating": 475}, {"MovieId": "9549", "Title": "The Right Stuff", "Casts": [], "PlotId": "9549", + "ThumbnailIds": ["/df1gdyE9S3DxmTfAeN3pNdSm64J.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 378}, + {"MovieId": "11185", "Title": "See No Evil, Hear No Evil", "Casts": [], "PlotId": "11185", + "ThumbnailIds": ["/psB1Zb6sqnzHyCgtJrEJqphflVx.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 402}, {"MovieId": "10155", "Title": "U Turn", "Casts": [], "PlotId": "10155", + "ThumbnailIds": ["/mG4GerNC6ZVnsFTXcsTeKuj8OG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 303}, + {"MovieId": "1412", "Title": "sex, lies, and videotape", "Casts": [], "PlotId": "1412", + "ThumbnailIds": ["/kOXATYKziiKmoMxgKA10JOO13JZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 328}, {"MovieId": "11559", "Title": "Tideland", "Casts": [], "PlotId": "11559", + "ThumbnailIds": ["/xR8vo6xGJbk8QljOlcjLNyLrwB5.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 245}, + {"MovieId": "1554", "Title": "Down by Law", "Casts": [], "PlotId": "1554", + "ThumbnailIds": ["/r4HxZPAhfg2akFaixpVIAM83GCY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 280}, {"MovieId": "11416", "Title": "The Mission", "Casts": [], "PlotId": "11416", + "ThumbnailIds": ["/oTU13XXz4WHisDWKX3X6dFWEjC0.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 574}, + {"MovieId": "11253", "Title": "Hellboy II: The Golden Army", "Casts": [], "PlotId": "11253", + "ThumbnailIds": ["/fFcZqnWDeQsImDAAIyAimc3SGEl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 2761}, {"MovieId": "12154", "Title": "3 Men and a Baby", "Casts": [], "PlotId": "12154", + "ThumbnailIds": ["/q4O2ravqDhUTAsshllGv7orgJOO.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.9, "NumRating": 447}, + {"MovieId": "209403", "Title": "Bad Words", "Casts": [], "PlotId": "209403", + "ThumbnailIds": ["/cEnpV3XLvgTp3eVXhY2eyQKZk4r.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 444}, {"MovieId": "9443", "Title": "Chariots of Fire", "Casts": [], "PlotId": "9443", + "ThumbnailIds": ["/Ae5ABhyD30jY9rkciOVCG8nJDwO.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 369}, + {"MovieId": "206647", "Title": "Spectre", "Casts": [], "PlotId": "206647", + "ThumbnailIds": ["/hE24GYddaxB9MVZl1CaiI86M3kp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 6648}, {"MovieId": "98548", "Title": "People Like Us", "Casts": [], "PlotId": "98548", + "ThumbnailIds": ["/lMAHB1r1vSUOZRKCYR143mm6VMk.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.8, "NumRating": 355}, + {"MovieId": "44629", "Title": "Animal Kingdom", "Casts": [], "PlotId": "44629", + "ThumbnailIds": ["/zhj8YPQKuRev5N3KoHacsPnF4mB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 395}, {"MovieId": "63493", "Title": "The Ledge", "Casts": [], "PlotId": "63493", + "ThumbnailIds": ["/7iHQhaF2LRGlHpZcjcdODDn6pj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 183}, + {"MovieId": "1710", "Title": "Copycat", "Casts": [], "PlotId": "1710", + "ThumbnailIds": ["/80czeJGSoik22fhtUM9WzyjUU4r.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 398}, {"MovieId": "10398", "Title": "Double Jeopardy", "Casts": [], "PlotId": "10398", + "ThumbnailIds": ["/9Pl7lqUzU7lxQHYjcmTT6ZvbbDY.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 503}, + {"MovieId": "9607", "Title": "Super Mario Bros.", "Casts": [], "PlotId": "9607", + "ThumbnailIds": ["/bmv7fmcBFzjnJvirfAdZm75qERY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.1, + "NumRating": 478}, {"MovieId": "3132", "Title": "Bad Company", "Casts": [], "PlotId": "3132", + "ThumbnailIds": ["/v4Jz1ALH2LdIrpr681WVIsYbRQL.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 360}, + {"MovieId": "9879", "Title": "Striptease", "Casts": [], "PlotId": "9879", + "ThumbnailIds": ["/4zMy6R7acotCmGoDk4sjzRtDwKn.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.7, + "NumRating": 351}, {"MovieId": "11929", "Title": "Dolores Claiborne", "Casts": [], "PlotId": "11929", + "ThumbnailIds": ["/ewmVWV0TP8LTlYZ4OzCcguwC9d1.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 310}, + {"MovieId": "9664", "Title": "Flyboys", "Casts": [], "PlotId": "9664", + "ThumbnailIds": ["/ap7UcuDPhyaY165YppEZJdbU1sA.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 414}, {"MovieId": "36819", "Title": "Time Bandits", "Casts": [], "PlotId": "36819", + "ThumbnailIds": ["/4VZtpwdhHQSa4LUkvujyGAHb1hG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 414}, + {"MovieId": "9566", "Title": "The Fan", "Casts": [], "PlotId": "9566", + "ThumbnailIds": ["/fdW8T9kkYlPyOGh0V5eodFr8SQq.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 344}, {"MovieId": "9587", "Title": "Little Women", "Casts": [], "PlotId": "9587", + "ThumbnailIds": ["/tD9YDE8pjtSvvKUpkmooE6YDSm8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 466}, + {"MovieId": "19255", "Title": "Away We Go", "Casts": [], "PlotId": "19255", + "ThumbnailIds": ["/zHDoca5jdrJQ8rwgN9HsWk8HRaG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 299}, {"MovieId": "14636", "Title": "The Condemned", "Casts": [], "PlotId": "14636", + "ThumbnailIds": ["/dYocAyHDOUg4y4pcetRWqHWH0fi.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 335}, + {"MovieId": "14013", "Title": "BASEketball", "Casts": [], "PlotId": "14013", + "ThumbnailIds": ["/pEg0tYYhSfsyjLIYpvD9lCWLhcH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 249}, {"MovieId": "9542", "Title": "The Hitcher", "Casts": [], "PlotId": "9542", + "ThumbnailIds": ["/pdPKlwAOX66Zqktp1amFooovAjT.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 337}, + {"MovieId": "141043", "Title": "A Long Way Down", "Casts": [], "PlotId": "141043", + "ThumbnailIds": ["/vIa83hicQj1ZFDG2bWfZaoUoa2e.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 522}, {"MovieId": "10784", "Title": "Cabaret", "Casts": [], "PlotId": "10784", + "ThumbnailIds": ["/1RmUfX3LcS897GhUWrWp5nRADo4.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 389}, + {"MovieId": "113833", "Title": "The Normal Heart", "Casts": [], "PlotId": "113833", + "ThumbnailIds": ["/fIf4nLpWHK8BsbH76fPgMbLSjuU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 557}, {"MovieId": "11515", "Title": "Goya's Ghosts", "Casts": [], "PlotId": "11515", + "ThumbnailIds": ["/x04CKIV43Y9K5iTD98fB25lB0F2.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 214}, + {"MovieId": "71864", "Title": "The Odd Life of Timothy Green", "Casts": [], "PlotId": "71864", + "ThumbnailIds": ["/nwWtTWWOJIDU1mM3cBYvSURB03B.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 599}, {"MovieId": "58232", "Title": "Chalet Girl", "Casts": [], "PlotId": "58232", + "ThumbnailIds": ["/64AQrBEKJVSIRaocOCtkMqExToz.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 365}, + {"MovieId": "10588", "Title": "The Cat in the Hat", "Casts": [], "PlotId": "10588", + "ThumbnailIds": ["/cyfCsdxGYIRlMd2z3dncOZWrgvk.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 715}, {"MovieId": "12155", "Title": "Alice in Wonderland", "Casts": [], "PlotId": "12155", + "ThumbnailIds": ["/pvEE5EN5N1yjmHmldfL4aJWm56l.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 8557}, + {"MovieId": "4599", "Title": "Raising Helen", "Casts": [], "PlotId": "4599", + "ThumbnailIds": ["/oROgudstoH3i4KyI5ZJbBVW9mKK.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 371}, {"MovieId": "28597", "Title": "Problem Child 2", "Casts": [], "PlotId": "28597", + "ThumbnailIds": ["/npr7j2HuRgvsKrXLIxIiXevTH8A.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 218}, + {"MovieId": "2362", "Title": "Westworld", "Casts": [], "PlotId": "2362", + "ThumbnailIds": ["/cOJsaT8jEmG9s1MziVIPpHBRpQ7.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 583}, {"MovieId": "557", "Title": "Spider-Man", "Casts": [], "PlotId": "557", + "ThumbnailIds": ["/A9BYH1DSetvC7bjbHWCaL17Qbp5.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 9635}, + {"MovieId": "401469", "Title": "Widows", "Casts": [], "PlotId": "401469", + "ThumbnailIds": ["/d31SGJSaX29ba5ZUbZcesGoDE7I.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 878}, {"MovieId": "11634", "Title": "Show Me Love", "Casts": [], "PlotId": "11634", + "ThumbnailIds": ["/gkM27kmi9woapo1oQ2lho59u5Hn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 214}, + {"MovieId": "10403", "Title": "The Player", "Casts": [], "PlotId": "10403", + "ThumbnailIds": ["/eoQ9H9CEySIYSPElKn2VRVY0MEa.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 301}, {"MovieId": "126889", "Title": "Alien: Covenant", "Casts": [], "PlotId": "126889", + "ThumbnailIds": ["/zecMELPbU5YMQpC81Z8ImaaXuf9.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 4894}, + {"MovieId": "21734", "Title": "Shadow of a Doubt", "Casts": [], "PlotId": "21734", + "ThumbnailIds": ["/sqHoIsWkGdPpX6zdPwX6HOMOWjj.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 379}, {"MovieId": "184314", "Title": "Young & Beautiful", "Casts": [], "PlotId": "184314", + "ThumbnailIds": ["/fqsab9s1yv60bDpak8p7b7R9Dn9.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 581}, + {"MovieId": "9597", "Title": "Vidocq", "Casts": [], "PlotId": "9597", + "ThumbnailIds": ["/7VQgyNCJV1TGpT8yTVDXaADQ5F2.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 236}, {"MovieId": "10468", "Title": "28 Days", "Casts": [], "PlotId": "10468", + "ThumbnailIds": ["/qTrrElHcONoix4hb4IpLiqEnjlb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 401}, + {"MovieId": "76341", "Title": "Mad Max: Fury Road", "Casts": [], "PlotId": "76341", + "ThumbnailIds": ["/kqjL17yufvn9OVLyXYpvtyrFfak.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 14217}, {"MovieId": "543540", "Title": "The Perfect Date", "Casts": [], "PlotId": "543540", + "ThumbnailIds": ["/pi7J3iH3Z1NO9q2E13ChY7imyKb.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.4, "NumRating": 1114}, + {"MovieId": "51300", "Title": "The Barber of Siberia", "Casts": [], "PlotId": "51300", + "ThumbnailIds": ["/xganmHk2g5PnzENlkGvg7brbbbM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 54}, {"MovieId": "10808", "Title": "Dr. Dolittle 2", "Casts": [], "PlotId": "10808", + "ThumbnailIds": ["/aecUc1LobMV56Doj99ZvJR2dXgG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.1, "NumRating": 862}, + {"MovieId": "62046", "Title": "Flypaper", "Casts": [], "PlotId": "62046", + "ThumbnailIds": ["/czpYP8p0HBXNL3cIsGFMd0a9rSu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 395}, {"MovieId": "106", "Title": "Predator", "Casts": [], "PlotId": "106", + "ThumbnailIds": ["/gUpto7r2XwoM5eW7MUvd8hl1etB.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 3658}, + {"MovieId": "204", "Title": "The Wages of Fear", "Casts": [], "PlotId": "204", + "ThumbnailIds": ["/3IGuAr1xsErR4XqmHZcfyQ4f8KY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 314}, + {"MovieId": "106646", "Title": "The Wolf of Wall Street", "Casts": [], "PlotId": "106646", + "ThumbnailIds": ["/vK1o5rZGqxyovfIhZyMELhk03wO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 12171}, + {"MovieId": "13067", "Title": "In the Land of Women", "Casts": [], "PlotId": "13067", + "ThumbnailIds": ["/vyt7OcWLFc0OsnNSh8XY7Z2bCur.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 265}, {"MovieId": "209263", "Title": "Enough Said", "Casts": [], "PlotId": "209263", + "ThumbnailIds": ["/gPJ7KyzdHVpCaxdiq22sPEmNJZV.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 461}, + {"MovieId": "484247", "Title": "A Simple Favor", "Casts": [], "PlotId": "484247", + "ThumbnailIds": ["/5EJWZQ8dh99hfgXP9zAD5Ak5Hrn.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 1675}, + {"MovieId": "567604", "Title": "Once Upon a Deadpool", "Casts": [], "PlotId": "567604", + "ThumbnailIds": ["/5Ka49BWWyKMXr93YMbH5wLN7aAM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 213}, {"MovieId": "13576", "Title": "This Is It", "Casts": [], "PlotId": "13576", + "ThumbnailIds": ["/4ZgT8FRJxxpG0mgOIPeCgjzAsBR.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 420}, + {"MovieId": "34480", "Title": "The Descent: Part 2", "Casts": [], "PlotId": "34480", + "ThumbnailIds": ["/4rZtrCSvDjK43kO2rK2z9FaytlW.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 468}, {"MovieId": "44147", "Title": "Wild Target", "Casts": [], "PlotId": "44147", + "ThumbnailIds": ["/x1O8MOIY41fpKbCUt1I4sKeLwwr.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 352}, + {"MovieId": "1023", "Title": "Adam's Apples", "Casts": [], "PlotId": "1023", + "ThumbnailIds": ["/ysLgwV21cnoI9nkWIvdZWqJuwjE.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 296}, {"MovieId": "150540", "Title": "Inside Out", "Casts": [], "PlotId": "150540", + "ThumbnailIds": ["/aAmfIX3TT40zUHGcCKrlOZRKC7u.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.0, "NumRating": 12454}, + {"MovieId": "234200", "Title": "Pride", "Casts": [], "PlotId": "234200", + "ThumbnailIds": ["/fk81iMvZTYo7MaYWGirPc0uzA55.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 725}, {"MovieId": "360920", "Title": "The Grinch", "Casts": [], "PlotId": "360920", + "ThumbnailIds": ["/gpkHvkCtZOeCQ2DelnJ2LB1WjZ5.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 1005}, + {"MovieId": "2665", "Title": "Airplane II: The Sequel", "Casts": [], "PlotId": "2665", + "ThumbnailIds": ["/pjI6j5sVTxJXuxnr2JM2FgvyFXS.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 459}, + {"MovieId": "318121", "Title": "The Fundamentals of Caring", "Casts": [], "PlotId": "318121", + "ThumbnailIds": ["/k1JZUr1wwpEdTwSVQdZWheRAIui.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 1181}, {"MovieId": "24056", "Title": "The Tournament", "Casts": [], "PlotId": "24056", + "ThumbnailIds": ["/ldqexoGgxnuV1hpX64MR6WnyEVG.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.8, "NumRating": 282}, + {"MovieId": "11562", "Title": "Crimes and Misdemeanors", "Casts": [], "PlotId": "11562", + "ThumbnailIds": ["/8I7Dzaah4FD6PTUjwIFm4H6Md0U.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 375}, {"MovieId": "394741", "Title": "Stan & Ollie", "Casts": [], "PlotId": "394741", + "ThumbnailIds": ["/8qDBDXA8Od8gc4IQMnoXUKyj8Pf.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 207}, + {"MovieId": "73937", "Title": "The Big Year", "Casts": [], "PlotId": "73937", + "ThumbnailIds": ["/kBnR6jbFRnbSQTqLEcDpKd7FxK5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 383}, {"MovieId": "10207", "Title": "Message in a Bottle", "Casts": [], "PlotId": "10207", + "ThumbnailIds": ["/majQar87QNbrC47qYMcmK1oCohZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 373}, + {"MovieId": "10757", "Title": "Kabhi Khushi Kabhie Gham", "Casts": [], "PlotId": "10757", + "ThumbnailIds": ["/1WfrkYkYL52JQCMtoeMbWdjRmv6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 194}, {"MovieId": "1833", "Title": "Rent", "Casts": [], "PlotId": "1833", + "ThumbnailIds": ["/c3GY40OxnPQZZPJk6WOShw63HGn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 284}, + {"MovieId": "2742", "Title": "Naked Lunch", "Casts": [], "PlotId": "2742", + "ThumbnailIds": ["/tMninSG4OAAhx9SNjdnMJM1R3Wn.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 404}, {"MovieId": "10762", "Title": "Without a Paddle", "Casts": [], "PlotId": "10762", + "ThumbnailIds": ["/q1jdD13xunSf22Ts4pAhVbVJ6IH.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 324}, + {"MovieId": "250066", "Title": "American Heist", "Casts": [], "PlotId": "250066", + "ThumbnailIds": ["/eqqf7DvhOBaEHiqLMkMMp0e8wzp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.6, + "NumRating": 210}, {"MovieId": "245706", "Title": "True Story", "Casts": [], "PlotId": "245706", + "ThumbnailIds": ["/hBG9DzRbzId4uC4aUv3XTDZCn4i.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 881}, + {"MovieId": "6471", "Title": "The Jerk", "Casts": [], "PlotId": "6471", + "ThumbnailIds": ["/wb6mB6R9vscPhyAegbbNDFZUAIs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 410}, {"MovieId": "2062", "Title": "Ratatouille", "Casts": [], "PlotId": "2062", + "ThumbnailIds": ["/xVxxSYHAfrEbllyWFQG5df5nwH4.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.6, "NumRating": 8847}, + {"MovieId": "11030", "Title": "Zelig", "Casts": [], "PlotId": "11030", + "ThumbnailIds": ["/5dgjgBMWHcOrXL7EbGnU73WpXNB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 380}, {"MovieId": "1613", "Title": "The 51st State", "Casts": [], "PlotId": "1613", + "ThumbnailIds": ["/hLtjOggvtlpBQ9tRNp5OwMw5mIk.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 279}, + {"MovieId": "10495", "Title": "The Karate Kid, Part III", "Casts": [], "PlotId": "10495", + "ThumbnailIds": ["/2Z0EJl11kOSPMMvHqZ4r5Csh7Ph.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 609}, + {"MovieId": "50837", "Title": "Martha Marcy May Marlene", "Casts": [], "PlotId": "50837", + "ThumbnailIds": ["/f68uBuxkEfesHmw5eJxFOnNalTY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 409}, {"MovieId": "2039", "Title": "Moonstruck", "Casts": [], "PlotId": "2039", + "ThumbnailIds": ["/2OIxyH2zMPYBzaECdIlX81Qc398.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 347}, + {"MovieId": "11453", "Title": "Deuce Bigalow: European Gigolo", "Casts": [], "PlotId": "11453", + "ThumbnailIds": ["/1P8fWex0NzgtPE4cgIUDHrVSARM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.7, + "NumRating": 406}, {"MovieId": "85", "Title": "Raiders of the Lost Ark", "Casts": [], "PlotId": "85", + "ThumbnailIds": ["/44sKJOGP3fTm4QXBcIuqu0RkdP7.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.9, "NumRating": 6505}, + {"MovieId": "332", "Title": "Inspector Gadget", "Casts": [], "PlotId": "332", + "ThumbnailIds": ["/kvlKBGtyyNVyGJB72aD1vwPk2d4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.4, + "NumRating": 602}, {"MovieId": "4133", "Title": "Blow", "Casts": [], "PlotId": "4133", + "ThumbnailIds": ["/yCLLbZzAa7jreGus7pvjZmL0bj7.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 2314}, + {"MovieId": "49950", "Title": "The Roommate", "Casts": [], "PlotId": "49950", + "ThumbnailIds": ["/yvKZBncLbuNd3HX7dcqcepq83qy.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.3, + "NumRating": 401}, {"MovieId": "11006", "Title": "Smokey and the Bandit", "Casts": [], "PlotId": "11006", + "ThumbnailIds": ["/5uTIEUSBVzmEZ8TnFEZbCCOVHPj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 306}, + {"MovieId": "10060", "Title": "Get Rich or Die Tryin'", "Casts": [], "PlotId": "10060", + "ThumbnailIds": ["/wKeSnhQfdwrycHorc9OPQ5KxVxJ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 351}, {"MovieId": "11324", "Title": "Shutter Island", "Casts": [], "PlotId": "11324", + "ThumbnailIds": ["/aZqKsvpJDFy2UzUMsdskNFbfkOd.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.1, "NumRating": 12391}, + {"MovieId": "576071", "Title": "Unplanned", "Casts": [], "PlotId": "576071", + "ThumbnailIds": ["/hQvf3RHgmp4XXXl2y6zhMe4G4kg.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 22}, + {"MovieId": "11658", "Title": "Tae Guk Gi: The Brotherhood of War", "Casts": [], "PlotId": "11658", + "ThumbnailIds": ["/1SEDI2qeNZgBK5XiJfKwWTmhZqC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 217}, + {"MovieId": "11850", "Title": "Invasion of the Body Snatchers", "Casts": [], "PlotId": "11850", + "ThumbnailIds": ["/skS02wdeH2C0nrbCQP3qKwJdZtZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 384}, + {"MovieId": "98566", "Title": "Teenage Mutant Ninja Turtles", "Casts": [], "PlotId": "98566", + "ThumbnailIds": ["/oDL2ryJ0sV2bmjgshVgJb3qzvwp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 4192}, {"MovieId": "10488", "Title": "Nim's Island", "Casts": [], "PlotId": "10488", + "ThumbnailIds": ["/wNSQsaz1btAHSZ6vNJpjbVmQWQG.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.8, "NumRating": 630}, + {"MovieId": "2000", "Title": "Aguirre: The Wrath of God", "Casts": [], "PlotId": "2000", + "ThumbnailIds": ["/uHP3AtAnudp5w8d3jx2el1AtV6a.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 400}, {"MovieId": "11566", "Title": "Dave", "Casts": [], "PlotId": "11566", + "ThumbnailIds": ["/nb2tSTxjhuiJqn9R6BEcwopq0dW.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 329}, + {"MovieId": "10907", "Title": "The Adventures of Robin Hood", "Casts": [], "PlotId": "10907", + "ThumbnailIds": ["/lEx5IKjqwYS7PcGnIXivYaDMzQr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 286}, {"MovieId": "10412", "Title": "Romper Stomper", "Casts": [], "PlotId": "10412", + "ThumbnailIds": ["/wW9J9InM3gdZS6vcoJVQJo9pIVj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 224}, + {"MovieId": "30197", "Title": "The Producers", "Casts": [], "PlotId": "30197", + "ThumbnailIds": ["/xgKikS0QQcWS1CvhjbemeoBFd32.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 293}, {"MovieId": "402900", "Title": "Ocean's Eight", "Casts": [], "PlotId": "402900", + "ThumbnailIds": ["/MvYpKlpFukTivnlBhizGbkAe3v.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 3548}, + {"MovieId": "14292", "Title": "Miracle", "Casts": [], "PlotId": "14292", + "ThumbnailIds": ["/u2OCExn2VMgEQnVeoGaHl9bSrtQ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 264}, {"MovieId": "5820", "Title": "The Sentinel", "Casts": [], "PlotId": "5820", + "ThumbnailIds": ["/wUE2EsKkktVz8IiTzrpO6GbI6gh.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 355}, + {"MovieId": "16899", "Title": "Easy Virtue", "Casts": [], "PlotId": "16899", + "ThumbnailIds": ["/1VO09so985ZSbEj0JnuebHMqTip.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 191}, + {"MovieId": "6878", "Title": "Homeward Bound: The Incredible Journey", "Casts": [], "PlotId": "6878", + "ThumbnailIds": ["/el6dJEpK97OJRQiQhuiSGk2jkV5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 412}, {"MovieId": "472451", "Title": "Boy Erased", "Casts": [], "PlotId": "472451", + "ThumbnailIds": ["/oZbhTdi0ZQY7iiSQ0L7h3ya6NDF.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 374}, + {"MovieId": "8764", "Title": "Top Secret!", "Casts": [], "PlotId": "8764", + "ThumbnailIds": ["/2ArC4IAAGXf3hTVJCzQfE0G5Vbf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 428}, {"MovieId": "13595", "Title": "Airheads", "Casts": [], "PlotId": "13595", + "ThumbnailIds": ["/jx06ghWxAC0BdrxUa03rti81RcY.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 341}, + {"MovieId": "83", "Title": "Open Water", "Casts": [], "PlotId": "83", + "ThumbnailIds": ["/hua2eluUhiLvKqwHFPV2aTiY8pp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 558}, {"MovieId": "2313", "Title": "Prime", "Casts": [], "PlotId": "2313", + "ThumbnailIds": ["/dQxtI83slU5fAq6WZjEmIDAtYvM.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 456}, + {"MovieId": "11787", "Title": "Harvey", "Casts": [], "PlotId": "11787", + "ThumbnailIds": ["/dgd82hYmpiXDM1G867HqNaWe8wj.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 274}, {"MovieId": "636", "Title": "THX 1138", "Casts": [], "PlotId": "636", + "ThumbnailIds": ["/8cie5mojY6MlIrYMs9EtNSyterv.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 414}, + {"MovieId": "13193", "Title": "Saved!", "Casts": [], "PlotId": "13193", + "ThumbnailIds": ["/sh61XwFfemE6MrDzAVNpJb6ZRgR.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 223}, {"MovieId": "12093", "Title": "Lilya 4-ever", "Casts": [], "PlotId": "12093", + "ThumbnailIds": ["/1uqtBLEXFO6qxofV9Hm1SigE3WH.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.8, "NumRating": 239}, + {"MovieId": "604", "Title": "The Matrix Reloaded", "Casts": [], "PlotId": "604", + "ThumbnailIds": ["/ezIurBz2fdUc68d98Fp9dRf5ihv.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 5597}, {"MovieId": "44716", "Title": "In a Better World", "Casts": [], "PlotId": "44716", + "ThumbnailIds": ["/qxCTUrnC5ZdsKQepBMPt09ihse.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 185}, + {"MovieId": "588001", "Title": "Despite Everything", "Casts": [], "PlotId": "588001", + "ThumbnailIds": ["/1GH4KCS8IgWcDt5toXYFYX5AmX4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 60}, + {"MovieId": "57158", "Title": "The Hobbit: The Desolation of Smaug", "Casts": [], "PlotId": "57158", + "ThumbnailIds": ["/gQCiuxGsfiXH1su6lp9n0nd0UeH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 7602}, {"MovieId": "500904", "Title": "A Vigilante", "Casts": [], "PlotId": "500904", + "ThumbnailIds": ["/avoKZfgBzyBGJsrAr2WQOwZK978.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 4.7, "NumRating": 53}, + {"MovieId": "595680", "Title": "Il grande spirito", "Casts": [], "PlotId": "595680", + "ThumbnailIds": ["/hFuxBhwfj75c0fy1byTheD1aAKC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "260", "Title": "The 39 Steps", "Casts": [], "PlotId": "260", + "ThumbnailIds": ["/9v283GWj9a0AC8wwC4zriNqY1lZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 376}, + {"MovieId": "17295", "Title": "The Battle of Algiers", "Casts": [], "PlotId": "17295", + "ThumbnailIds": ["/zsNc43QfSqeMBW186o9Fozfmkst.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 279}, + {"MovieId": "234", "Title": "The Cabinet of Dr. Caligari", "Casts": [], "PlotId": "234", + "ThumbnailIds": ["/myK9DeIsXWGKgUTZyGXg2IfFk0W.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 536}, {"MovieId": "12690", "Title": "Appaloosa", "Casts": [], "PlotId": "12690", + "ThumbnailIds": ["/ar26XwTJz6BPFRricdNpMctyjB0.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 475}, + {"MovieId": "1694", "Title": "Re-Animator", "Casts": [], "PlotId": "1694", + "ThumbnailIds": ["/hJnP2O0uTuh8HsR094WLTi3sQwc.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 472}, + {"MovieId": "780", "Title": "The Passion of Joan of Arc", "Casts": [], "PlotId": "780", + "ThumbnailIds": ["/5HL0dEJfd7PF0eRiKz8BiNfe8Tf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 319}, {"MovieId": "19833", "Title": "In the Loop", "Casts": [], "PlotId": "19833", + "ThumbnailIds": ["/jL6txnziFSeEifQkqnPBtaPaiXU.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 293}, + {"MovieId": "405774", "Title": "Bird Box", "Casts": [], "PlotId": "405774", + "ThumbnailIds": ["/rGfGfgL2pEPCfhIvqHXieXFn7gp.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 4562}, {"MovieId": "452832", "Title": "Serenity", "Casts": [], "PlotId": "452832", + "ThumbnailIds": ["/hgWAcic93phg4DOuQ8NrsgQWiqu.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.1, "NumRating": 282}, + {"MovieId": "10136", "Title": "The Golden Child", "Casts": [], "PlotId": "10136", + "ThumbnailIds": ["/nVJIyS2gh0hBvVEr8s9SrpSBTxL.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 495}, {"MovieId": "531", "Title": "The Wrong Trousers", "Casts": [], "PlotId": "531", + "ThumbnailIds": ["/O3fFWazkIL1QrmH5t9numsUgmR.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.6, "NumRating": 457}, + {"MovieId": "581585", "Title": "A Regular Woman", "Casts": [], "PlotId": "581585", + "ThumbnailIds": ["/eXArCCQj3W4b55SohTo5fBHiu7G.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "41154", "Title": "Men in Black 3", "Casts": [], "PlotId": "41154", + "ThumbnailIds": ["/l9hrvXyGq19f6jPRZhSVRibTMwW.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 6297}, + {"MovieId": "956", "Title": "Mission: Impossible III", "Casts": [], "PlotId": "956", + "ThumbnailIds": ["/qjy8ABAbWooV4jLG6UjzDHlv4RB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 3513}, {"MovieId": "13532", "Title": "Fanboys", "Casts": [], "PlotId": "13532", + "ThumbnailIds": ["/ywI92p2x7D96nuEayGVOMll35SF.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.2, "NumRating": 436}, + {"MovieId": "351044", "Title": "Welcome to Marwen", "Casts": [], "PlotId": "351044", + "ThumbnailIds": ["/o45VIAUYDcVCGuzd43l8Sr5Dfti.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 241}, + {"MovieId": "9876", "Title": "Stop! Or My Mom Will Shoot", "Casts": [], "PlotId": "9876", + "ThumbnailIds": ["/sQXdlCaX6demEdOgI1FbV9fX8aO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.9, + "NumRating": 410}, + {"MovieId": "595188", "Title": "Pariban : Idola Dari Tanah Jawa", "Casts": [], "PlotId": "595188", + "ThumbnailIds": ["/yxiDlS5tKpuPHHl4eOLgJCdTqU6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "10985", "Title": "The New Guy", "Casts": [], "PlotId": "10985", + "ThumbnailIds": ["/uqUVToyQmcqBqm7USWEQIYDNERd.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.6, "NumRating": 251}, + {"MovieId": "11469", "Title": "Black Knight", "Casts": [], "PlotId": "11469", + "ThumbnailIds": ["/xJnGEOnYUelD6wLgpQfNg5tq8jV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.2, + "NumRating": 533}, {"MovieId": "11377", "Title": "House on Haunted Hill", "Casts": [], "PlotId": "11377", + "ThumbnailIds": ["/yedchBbI23FgDjWP1tvahOZgiks.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 373}, + {"MovieId": "13291", "Title": "Traitor", "Casts": [], "PlotId": "13291", + "ThumbnailIds": ["/euLk9RxsPHfpyWUHzTDNY6Dksch.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 321}, + {"MovieId": "10242", "Title": "What Ever Happened to Baby Jane?", "Casts": [], "PlotId": "10242", + "ThumbnailIds": ["/t2hPlHc2pFweBqQgrsNfSLNIv1j.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 418}, + {"MovieId": "887", "Title": "The Best Years of Our Lives", "Casts": [], "PlotId": "887", + "ThumbnailIds": ["/fxjWVRlmD0EKn8ZgFKREqpkTiRH.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 236}, {"MovieId": "21755", "Title": "The Brothers Bloom", "Casts": [], "PlotId": "21755", + "ThumbnailIds": ["/xIz8iwzyjgWGzcIwHKpqhBs77ML.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 318}, + {"MovieId": "4105", "Title": "Black Rain", "Casts": [], "PlotId": "4105", + "ThumbnailIds": ["/dmMIMuByEbzE0x73gjc9YcDjjlx.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 434}, {"MovieId": "5961", "Title": "Fanny & Alexander", "Casts": [], "PlotId": "5961", + "ThumbnailIds": ["/zZVkPy2PJuWWZbYGXG38a1nZp7l.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.8, "NumRating": 275}, + {"MovieId": "10494", "Title": "Perfect Blue", "Casts": [], "PlotId": "10494", + "ThumbnailIds": ["/sxBzVuwqIABKIbdij7lOrRvDb15.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 638}, {"MovieId": "353576", "Title": "The Con Is On", "Casts": [], "PlotId": "353576", + "ThumbnailIds": ["/d4ZzzFOokK356jf6L6vUp3RKNvM.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.0, "NumRating": 54}, + {"MovieId": "181533", "Title": "Night at the Museum: Secret of the Tomb", "Casts": [], "PlotId": "181533", + "ThumbnailIds": ["/tWwASv4CU1Au1IukacdSUewDCV3.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 3467}, {"MovieId": "854", "Title": "The Mask", "Casts": [], "PlotId": "854", + "ThumbnailIds": ["/v8x8p441l1Bep8p82pAG6rduBoK.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.8, "NumRating": 5123}, + {"MovieId": "449924", "Title": "Ip Man 4", "Casts": [], "PlotId": "449924", + "ThumbnailIds": ["/mAWBfTDAmfpvQGMVFuzuVl49N1P.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "607", "Title": "Men in Black", "Casts": [], "PlotId": "607", + "ThumbnailIds": ["/f24UVKq3UiQWLqGWdqjwkzgB8j8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 7726}, + {"MovieId": "499533", "Title": "Float Like A Butterfly", "Casts": [], "PlotId": "499533", + "ThumbnailIds": ["/qOLSeKboDzZ1lPfuMTFWOj9Xl0z.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "6623", "Title": "The Peacemaker", "Casts": [], "PlotId": "6623", + "ThumbnailIds": ["/hc3p6pvIrfO4AmrVLb6qTviOwW2.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 446}, + {"MovieId": "10681", "Title": "WALL\u00b7E", "Casts": [], "PlotId": "10681", + "ThumbnailIds": ["/9cJETuLMc6R0bTWRA5i7ctY9bxk.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 10623}, + {"MovieId": "16869", "Title": "Inglourious Basterds", "Casts": [], "PlotId": "16869", + "ThumbnailIds": ["/ai0LXkzVM3hMjDhvFdKMUemoBe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 11739}, {"MovieId": "49012", "Title": "Arthur", "Casts": [], "PlotId": "49012", + "ThumbnailIds": ["/28HW1Kc4OwQH50M0XmcEuIYrCWk.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.4, "NumRating": 395}, + {"MovieId": "9612", "Title": "Coneheads", "Casts": [], "PlotId": "9612", + "ThumbnailIds": ["/9vuFI0xV1yZfsWr23evJlkRDL8j.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.0, + "NumRating": 394}, {"MovieId": "218", "Title": "The Terminator", "Casts": [], "PlotId": "218", + "ThumbnailIds": ["/q8ffBuxQlYOHrvPniLgCbmKK4Lv.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 6691}, + {"MovieId": "4244", "Title": "The Kid", "Casts": [], "PlotId": "4244", + "ThumbnailIds": ["/bTsn3l3QLmtKGPnOkKWpLObOgu7.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 429}, + {"MovieId": "10152", "Title": "Dumb and Dumberer: When Harry Met Lloyd", "Casts": [], "PlotId": "10152", + "ThumbnailIds": ["/eizaKEnF108gQq89f1XsAyVxjq6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.1, + "NumRating": 395}, {"MovieId": "2925", "Title": "The First Wives Club", "Casts": [], "PlotId": "2925", + "ThumbnailIds": ["/k4gb8CcRgvYM0XB0SBEiWzBhe3f.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 331}, + {"MovieId": "103328", "Title": "Holy Motors", "Casts": [], "PlotId": "103328", + "ThumbnailIds": ["/d5amUFExQCqLkRpWQ6QspBtyWUe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 418}, + {"MovieId": "8010", "Title": "Highlander 2: The Quickening", "Casts": [], "PlotId": "8010", + "ThumbnailIds": ["/7W43CXQh6jF9NhnBnlA4BoaFp8W.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.6, + "NumRating": 352}, {"MovieId": "324786", "Title": "Hacksaw Ridge", "Casts": [], "PlotId": "324786", + "ThumbnailIds": ["/bndiUFfJxNd2fYx8XO610L9a07m.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.1, "NumRating": 6488}, + {"MovieId": "424139", "Title": "Halloween", "Casts": [], "PlotId": "424139", + "ThumbnailIds": ["/lNkDYKmrVem1J0aAfCnQlJOCKnT.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 1913}, + {"MovieId": "285", "Title": "Pirates of the Caribbean: At World's End", "Casts": [], "PlotId": "285", + "ThumbnailIds": ["/bXb00CkHqx7TPchTGG131sWV59y.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 8203}, {"MovieId": "584804", "Title": "Kleine Germanen", "Casts": [], "PlotId": "584804", + "ThumbnailIds": ["/uM6JkQRqAd4Q4MVAxAn8rayrI1.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "483157", "Title": "Morning Has Broken", "Casts": [], "PlotId": "483157", + "ThumbnailIds": [None], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "11560", "Title": "High Crimes", "Casts": [], "PlotId": "11560", + "ThumbnailIds": ["/adujr0eiutryiWmK8i0DPGTrOpU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 321}, {"MovieId": "10956", "Title": "Joe Dirt", "Casts": [], "PlotId": "10956", + "ThumbnailIds": ["/8FKSjl4cEg4OEyWdCTjqAT8M6qv.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 360}, + {"MovieId": "585", "Title": "Monsters, Inc.", "Casts": [], "PlotId": "585", + "ThumbnailIds": ["/93Y9BGx8blzmZOPSoivkFfaifqU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.8, + "NumRating": 10614}, + {"MovieId": "1865", "Title": "Pirates of the Caribbean: On Stranger Tides", "Casts": [], "PlotId": "1865", + "ThumbnailIds": ["/wNUDAq5OUMOtxMlz64YaCp7gZma.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 8421}, {"MovieId": "8092", "Title": "This Boy\u2019s Life", "Casts": [], "PlotId": "8092", + "ThumbnailIds": ["/xs8ebrGRGu6Y9ebu2dJFdm9yaZP.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.1, "NumRating": 376}, + {"MovieId": "569064", "Title": "Just Say Goodbye", "Casts": [], "PlotId": "569064", + "ThumbnailIds": ["/fHVlfvADGo9EMfEU6iMYsrTkusW.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "101173", "Title": "Coriolanus", "Casts": [], "PlotId": "101173", + "ThumbnailIds": ["/yE026snGPK1Cm61y6qmPdX5Hh8h.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 210}, + {"MovieId": "9087", "Title": "The American President", "Casts": [], "PlotId": "9087", + "ThumbnailIds": ["/yObOAYFIHXHkFPQ3jhgkN2ezaD.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 318}, {"MovieId": "9464", "Title": "Buffalo '66", "Casts": [], "PlotId": "9464", + "ThumbnailIds": ["/taVFuUhUWoX9YE7bb2bWkSPjC9P.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 336}, + {"MovieId": "11260", "Title": "Meet Dave", "Casts": [], "PlotId": "11260", + "ThumbnailIds": ["/gQKUAQGaNIsUhGL07zdhcrPyWdV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 704}, {"MovieId": "8882", "Title": "Gomorrah", "Casts": [], "PlotId": "8882", + "ThumbnailIds": ["/6aVJi30jxywKXbrpcBD11AuVoKv.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.9, "NumRating": 519}, + {"MovieId": "9515", "Title": "The Matador", "Casts": [], "PlotId": "9515", + "ThumbnailIds": ["/u4Pfeuz52JPBdxwxpMpyMApWltB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 220}, {"MovieId": "34769", "Title": "Defendor", "Casts": [], "PlotId": "34769", + "ThumbnailIds": ["/bGwWUN9aqglafPmZRNlW3tmspuZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 290}, + {"MovieId": "11197", "Title": "Evil", "Casts": [], "PlotId": "11197", + "ThumbnailIds": ["/1ULoIF7u3hVoxOij4GMaqlYZRxc.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 142}, {"MovieId": "97051", "Title": "Would You Rather", "Casts": [], "PlotId": "97051", + "ThumbnailIds": ["/yOAU7HHVw4RrDGK97i6ll46JVfj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 456}, + {"MovieId": "983", "Title": "The Man Who Would Be King", "Casts": [], "PlotId": "983", + "ThumbnailIds": ["/21BANIzXEKyZDUFOr9NdUEgP4EA.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 242}, {"MovieId": "445651", "Title": "The Darkest Minds", "Casts": [], "PlotId": "445651", + "ThumbnailIds": ["/94RaS52zmsqaiAe1TG20pdbJCZr.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 1119}, + {"MovieId": "447332", "Title": "A Quiet Place", "Casts": [], "PlotId": "447332", + "ThumbnailIds": ["/nAU74GmpUk7t5iklEp3bufwDq4n.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 5654}, {"MovieId": "11686", "Title": "Love and Death", "Casts": [], "PlotId": "11686", + "ThumbnailIds": ["/qRNisGLmcHoEvMjzcvVlcdCnOhO.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.8, "NumRating": 349}, + {"MovieId": "426543", "Title": "The Nutcracker and the Four Realms", "Casts": [], "PlotId": "426543", + "ThumbnailIds": ["/tysit5m7HSEvf1wknPR0DeEhR7e.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 838}, {"MovieId": "646", "Title": "Dr. No", "Casts": [], "PlotId": "646", + "ThumbnailIds": ["/gRdfLVVf6FheOw6mw6wOsKhZG1l.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 1578}, + {"MovieId": "293", "Title": "A River Runs Through It", "Casts": [], "PlotId": "293", + "ThumbnailIds": ["/4nfpZVadu7nCJETENNhVWfR3okU.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 433}, {"MovieId": "7300", "Title": "One Fine Day", "Casts": [], "PlotId": "7300", + "ThumbnailIds": ["/yJETLNpYeEpDar7tJK1KedYGLgx.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 308}, + {"MovieId": "11374", "Title": "Edtv", "Casts": [], "PlotId": "11374", + "ThumbnailIds": ["/sF3YBtYn6yAHMPDbiC58WArIWMl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 286}, + {"MovieId": "63815", "Title": "Something Something... Unakkum Enakkum", "Casts": [], "PlotId": "63815", + "ThumbnailIds": ["/7bkeiY70hQZ3xVVNLwkmezpa0FC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.9, + "NumRating": 9}, {"MovieId": "679", "Title": "Aliens", "Casts": [], "PlotId": "679", + "ThumbnailIds": ["/nORMXEkYEbzkU5WkMWMgRDJwjSZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.9, "NumRating": 5025}, + {"MovieId": "11051", "Title": "The Last Temptation of Christ", "Casts": [], "PlotId": "11051", + "ThumbnailIds": ["/dutPJWBeQgdfjRqeKojqmyIdXFd.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 401}, {"MovieId": "238628", "Title": "Tangerines", "Casts": [], "PlotId": "238628", + "ThumbnailIds": ["/5PINEEPwh11MjUKw4M58NcVoCCb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.8, "NumRating": 188}, + {"MovieId": "268896", "Title": "Pacific Rim: Uprising", "Casts": [], "PlotId": "268896", + "ThumbnailIds": ["/v5HlmJK9bdeHxN2QhaFP1ivjX3U.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 2153}, + {"MovieId": "631", "Title": "Sunrise: A Song of Two Humans", "Casts": [], "PlotId": "631", + "ThumbnailIds": ["/hEDMD8Lu7tMurqIglE8mo4GKBN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 301}, {"MovieId": "38363", "Title": "Fair Game", "Casts": [], "PlotId": "38363", + "ThumbnailIds": ["/yE8uba2FkmFagMpeNNoo0cPyJ7D.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 379}, + {"MovieId": "1880", "Title": "Red Dawn", "Casts": [], "PlotId": "1880", + "ThumbnailIds": ["/hVyeN1aFmqLsRK9VNElETYBDtnf.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.3, + "NumRating": 338}, {"MovieId": "11141", "Title": "Laws of Attraction", "Casts": [], "PlotId": "11141", + "ThumbnailIds": ["/raE9gFRS2oH4jBBjwIJvJresOQn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 197}, + {"MovieId": "11158", "Title": "Honey I Blew Up the Kid", "Casts": [], "PlotId": "11158", + "ThumbnailIds": ["/gwda8CnPAfHGwygclGiKddt60fX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.0, + "NumRating": 545}, {"MovieId": "10186", "Title": "The Rocker", "Casts": [], "PlotId": "10186", + "ThumbnailIds": ["/yQfng0iKQVuGDdI5JeoKPINlXuH.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 274}, + {"MovieId": "4520", "Title": "Sleuth", "Casts": [], "PlotId": "4520", + "ThumbnailIds": ["/5659i917XvBIf3i5fCPrOmCBja5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 261}, {"MovieId": "1792", "Title": "Stuck on You", "Casts": [], "PlotId": "1792", + "ThumbnailIds": ["/gUbNzcIdKCoqp9amNFpFQfpBzrB.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 324}, + {"MovieId": "10122", "Title": "Flight of the Navigator", "Casts": [], "PlotId": "10122", + "ThumbnailIds": ["/69kD2bpkwSadpN30JB9vJrlz8HW.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 341}, {"MovieId": "9909", "Title": "Dangerous Minds", "Casts": [], "PlotId": "9909", + "ThumbnailIds": ["/y5Jee3QmYOlpqfaPPbfvtdVc5wj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 425}, + {"MovieId": "119675", "Title": "Behind the Candelabra", "Casts": [], "PlotId": "119675", + "ThumbnailIds": ["/46J5VoqnneIz3hs50Ptk2o5bmXB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 450}, {"MovieId": "185", "Title": "A Clockwork Orange", "Casts": [], "PlotId": "185", + "ThumbnailIds": ["/4sHeTAp65WrSSuc05nRBKddhBxO.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.2, "NumRating": 6563}, + {"MovieId": "9966", "Title": "The Messengers", "Casts": [], "PlotId": "9966", + "ThumbnailIds": ["/x2Z5iXhHaoK22vwfxju5vq25apS.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 387}, {"MovieId": "64639", "Title": "Straw Dogs", "Casts": [], "PlotId": "64639", + "ThumbnailIds": ["/hSW5Msz5Cr6vUQxChPSPdxufnss.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 246}, + {"MovieId": "1406", "Title": "City Slickers", "Casts": [], "PlotId": "1406", + "ThumbnailIds": ["/tdOHDekHHGyjcR3Ay6WQ6uiFHoz.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 358}, + {"MovieId": "259316", "Title": "Fantastic Beasts and Where to Find Them", "Casts": [], "PlotId": "259316", + "ThumbnailIds": ["/1M91Bt3oGspda75H9eLqYZkJzgO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 11951}, + {"MovieId": "512954", "Title": "Leaving Afghanistan", "Casts": [], "PlotId": "512954", + "ThumbnailIds": ["/aqNiKKr5fvJqCR4LEb1pcw48k1y.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "940", "Title": "The Lady Vanishes", "Casts": [], "PlotId": "940", + "ThumbnailIds": ["/edL8YmyR1BjICz3mp1fhUqSOPnF.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.6, "NumRating": 313}, + {"MovieId": "11854", "Title": "Kuch Kuch Hota Hai", "Casts": [], "PlotId": "11854", + "ThumbnailIds": ["/hS1BvHAi29RXhKTyCoOEen0uU6j.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.6, + "NumRating": 162}, {"MovieId": "2771", "Title": "American Splendor", "Casts": [], "PlotId": "2771", + "ThumbnailIds": ["/qIJHt9PO13mAuFnHgrJUAq5p8Bf.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 192}, + {"MovieId": "704", "Title": "A Hard Day's Night", "Casts": [], "PlotId": "704", + "ThumbnailIds": ["/raJc1rxX0SQGzU1sRBAAFLkKGv1.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 268}, {"MovieId": "257344", "Title": "Pixels", "Casts": [], "PlotId": "257344", + "ThumbnailIds": ["/ktyVmIqfoaJ8w0gDSZyjhhOPpD6.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.6, "NumRating": 4370}, + {"MovieId": "16642", "Title": "Days of Heaven", "Casts": [], "PlotId": "16642", + "ThumbnailIds": ["/rjw7tQbiBnbIeufyEt02oVickDM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 391}, {"MovieId": "175", "Title": "The Big Blue", "Casts": [], "PlotId": "175", + "ThumbnailIds": ["/RgvRBAD5LTGsePMzjaqaNPkSYf.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.6, "NumRating": 691}, + {"MovieId": "71668", "Title": "Piranha 3DD", "Casts": [], "PlotId": "71668", + "ThumbnailIds": ["/xua3x47piJTIRCVtVGjzXPDrbsN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.2, + "NumRating": 498}, {"MovieId": "608", "Title": "Men in Black II", "Casts": [], "PlotId": "608", + "ThumbnailIds": ["/qWjRfBwr4VculczswwojXgoU0mq.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 5582}, + {"MovieId": "10909", "Title": "Kalifornia", "Casts": [], "PlotId": "10909", + "ThumbnailIds": ["/5KGQYEsJvQdWZQH6o1zIyzkZZRC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 354}, {"MovieId": "287", "Title": "Bull Durham", "Casts": [], "PlotId": "287", + "ThumbnailIds": ["/wZwXLiR1cisTXaJiuBLvgkS5HWw.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 255}, + {"MovieId": "10758", "Title": "Waitress", "Casts": [], "PlotId": "10758", + "ThumbnailIds": ["/ux3SPTHHmJytSjWEaV7xlo9nbOZ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 228}, {"MovieId": "953", "Title": "Madagascar", "Casts": [], "PlotId": "953", + "ThumbnailIds": ["/2YiESGB68BGQSAFvfJxBi774sc4.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.8, "NumRating": 6092}, + {"MovieId": "621", "Title": "Grease", "Casts": [], "PlotId": "621", + "ThumbnailIds": ["/iMHdFTrCYhue74sBnXkdO39AJ3R.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 3712}, {"MovieId": "11824", "Title": "Teen Wolf", "Casts": [], "PlotId": "11824", + "ThumbnailIds": ["/3TKJbKNpHvRP8YVnwbgfok41AAC.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.0, "NumRating": 475}, + {"MovieId": "10414", "Title": "The Mighty Ducks", "Casts": [], "PlotId": "10414", + "ThumbnailIds": ["/4qXfjDlDEGuN3xRNawh4WZo5o96.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 335}, {"MovieId": "262500", "Title": "Insurgent", "Casts": [], "PlotId": "262500", + "ThumbnailIds": ["/6w1VjTPTjTaA5oNvsAg0y4H6bou.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 6665}, + {"MovieId": "9766", "Title": "Gridiron Gang", "Casts": [], "PlotId": "9766", + "ThumbnailIds": ["/8UlDsCPjAfg15L5Sgkcmy8rv1rg.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 366}, {"MovieId": "14072", "Title": "Rab Ne Bana Di Jodi", "Casts": [], "PlotId": "14072", + "ThumbnailIds": ["/m8x6I2qf3R98HtF4DmJXcdxCU64.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 101}, + {"MovieId": "27585", "Title": "Rabbit Hole", "Casts": [], "PlotId": "27585", + "ThumbnailIds": ["/qBhDlqdLZzxqKIXO53vMjpqLgZu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 287}, {"MovieId": "632", "Title": "Stalag 17", "Casts": [], "PlotId": "632", + "ThumbnailIds": ["/2PtDbmN7HAu4W5WqrUocBV8lgiZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.8, "NumRating": 239}, + {"MovieId": "575766", "Title": "Hatsukoi: Otosan, Chibi ga Inaku Narimashita", "Casts": [], + "PlotId": "575766", "ThumbnailIds": ["/yMeXxSZ4IEitEWySboQaYAeEtXQ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "372058", "Title": "Your Name.", "Casts": [], "PlotId": "372058", + "ThumbnailIds": ["/xq1Ugd62d23K2knRUx6xxuALTZB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.6, + "NumRating": 4017}, {"MovieId": "225886", "Title": "Sex Tape", "Casts": [], "PlotId": "225886", + "ThumbnailIds": ["/2BMmIPGu5ZbTrwXwomdSUuTB2Ul.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.3, "NumRating": 2644}, + {"MovieId": "593325", "Title": "Sodemacom Killer", "Casts": [], "PlotId": "593325", + "ThumbnailIds": ["/loTuS8ryototYPvGJaNsmlHCvIG.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "293167", "Title": "Kong: Skull Island", "Casts": [], "PlotId": "293167", + "ThumbnailIds": ["/r2517Vz9EhDhj88qwbDVj8DCRZN.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 5937}, + {"MovieId": "132316", "Title": "Jab Tak Hai Jaan", "Casts": [], "PlotId": "132316", + "ThumbnailIds": ["/oGoPWxz4v8HT8ydr2VdAa1OVohS.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 105}, {"MovieId": "11178", "Title": "My Sassy Girl", "Casts": [], "PlotId": "11178", + "ThumbnailIds": ["/jhlthtFzzEA8RYqRHy1y8dus4Q8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 266}, + {"MovieId": "493922", "Title": "Hereditary", "Casts": [], "PlotId": "493922", + "ThumbnailIds": ["/lHV8HHlhwNup2VbpiACtlKzaGIQ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 2370}, {"MovieId": "64586", "Title": "Sleeping Beauty", "Casts": [], "PlotId": "64586", + "ThumbnailIds": ["/9fc7K82At056IlrK9dOzPSQYhSY.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.3, "NumRating": 297}, + {"MovieId": "18570", "Title": "Food, Inc.", "Casts": [], "PlotId": "18570", + "ThumbnailIds": ["/gWYCM3YVYwZQcpfye6biAlPy9Xt.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 324}, {"MovieId": "553141", "Title": "The Head Hunter", "Casts": [], "PlotId": "553141", + "ThumbnailIds": ["/ol0DSLOIN8Rq1BcWDTsk6NNwas6.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.3, "NumRating": 35}, + {"MovieId": "37414", "Title": "The Killer Inside Me", "Casts": [], "PlotId": "37414", + "ThumbnailIds": ["/9iaV9bNNFBxEWLjnMGvSmnNY4Uj.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 299}, {"MovieId": "129", "Title": "Spirited Away", "Casts": [], "PlotId": "129", + "ThumbnailIds": ["/oRvMaJOmapypFUcQqpgHMZA6qL9.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.5, "NumRating": 7271}, + {"MovieId": "336843", "Title": "Maze Runner: The Death Cure", "Casts": [], "PlotId": "336843", + "ThumbnailIds": ["/2zYfzA3TBwrMC8tfFbpiTLODde0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 3791}, {"MovieId": "273248", "Title": "The Hateful Eight", "Casts": [], "PlotId": "273248", + "ThumbnailIds": ["/fqe8JxDNO8B8QfOGTdjh6sPCdSC.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.7, "NumRating": 7887}, + {"MovieId": "10720", "Title": "Down with Love", "Casts": [], "PlotId": "10720", + "ThumbnailIds": ["/c5fcbQM7uPObficCZjyTvwOcU1l.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 367}, {"MovieId": "438", "Title": "Cube Zero", "Casts": [], "PlotId": "438", + "ThumbnailIds": ["/m6Vn1rysx6vmgJuy78Ih9QxFoOy.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 482}, + {"MovieId": "14506", "Title": "The Adventures of Baron Munchausen", "Casts": [], "PlotId": "14506", + "ThumbnailIds": ["/75SlrEn0RCb2Ng18cCE82S00Hi5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 356}, {"MovieId": "8398", "Title": "The Hitcher", "Casts": [], "PlotId": "8398", + "ThumbnailIds": ["/mIPCt79baN62Xv6TAEwLDxLUqBz.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 422}, + {"MovieId": "348", "Title": "Alien", "Casts": [], "PlotId": "348", + "ThumbnailIds": ["/2h00HrZs89SL3tXB4nbkiM7BKHs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 7439}, {"MovieId": "812", "Title": "Aladdin", "Casts": [], "PlotId": "812", + "ThumbnailIds": ["/7f53XAE4nPiGe9XprpGAeWHuKPw.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.6, "NumRating": 6232}, + {"MovieId": "401981", "Title": "Red Sparrow", "Casts": [], "PlotId": "401981", + "ThumbnailIds": ["/vLCogyfQGxVLDC1gqUdNAIkc29L.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 3321}, {"MovieId": "8491", "Title": "Weekend at Bernie's", "Casts": [], "PlotId": "8491", + "ThumbnailIds": ["/h3hjMREZEUPtDkZBiYDzLq0THk0.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.4, "NumRating": 414}, + {"MovieId": "590927", "Title": "Solo cose belle", "Casts": [], "PlotId": "590927", + "ThumbnailIds": ["/htp3wtXbB0b4qrBmGLCpjvXKvrS.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "8386", "Title": "How High", "Casts": [], "PlotId": "8386", + "ThumbnailIds": ["/AdQDz4LF6BzFfmyIJkMtrj707BZ.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 407}, + {"MovieId": "447200", "Title": "Skyscraper", "Casts": [], "PlotId": "447200", + "ThumbnailIds": ["/5LYSsOPzuP13201qSzMjNxi8FxN.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 2089}, {"MovieId": "449443", "Title": "Den of Thieves", "Casts": [], "PlotId": "449443", + "ThumbnailIds": ["/AfybH6GbGFw1F9bcETe2yu25mIE.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.5, "NumRating": 1208}, + {"MovieId": "2928", "Title": "Michael", "Casts": [], "PlotId": "2928", + "ThumbnailIds": ["/xof1HmoI3NrSrmzHByjK6W7dU8E.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.6, + "NumRating": 283}, {"MovieId": "9333", "Title": "Last Man Standing", "Casts": [], "PlotId": "9333", + "ThumbnailIds": ["/fkURS96D2ceuocZIBuyiIBGHilF.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 399}, + {"MovieId": "11078", "Title": "National Security", "Casts": [], "PlotId": "11078", + "ThumbnailIds": ["/5YFPkMgy5NCzUSYl5Ep8p0vK9qB.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 407}, {"MovieId": "857", "Title": "Saving Private Ryan", "Casts": [], "PlotId": "857", + "ThumbnailIds": ["/miDoEMlYDJhOCvxlzI0wZqBs9Yt.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.1, "NumRating": 8292}, + {"MovieId": "376565", "Title": "The Duelist", "Casts": [], "PlotId": "376565", + "ThumbnailIds": ["/zlVkUY2bBJ5XYTDiry16u70oayQ.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 40}, {"MovieId": "10326", "Title": "Forever Young", "Casts": [], "PlotId": "10326", + "ThumbnailIds": ["/AvkLZ5EqehwUlJD93ZeobesDmHB.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 401}, + {"MovieId": "9777", "Title": "Proof", "Casts": [], "PlotId": "9777", + "ThumbnailIds": ["/eGVCVdbUR4gaAKPk1P4mar91qSX.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 335}, {"MovieId": "399360", "Title": "Alpha", "Casts": [], "PlotId": "399360", + "ThumbnailIds": ["/afdZAIcAQscziqVtsEoh2PwsYTW.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 1117}, + {"MovieId": "11797", "Title": "Fright Night", "Casts": [], "PlotId": "11797", + "ThumbnailIds": ["/jE0YbuFlmaZUWeVTyYNpzYXjIbn.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 515}, {"MovieId": "6691", "Title": "Priceless", "Casts": [], "PlotId": "6691", + "ThumbnailIds": ["/7GtzgK56NeIDzA6DksPihYVqTFm.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 393}, + {"MovieId": "21334", "Title": "Children of Heaven", "Casts": [], "PlotId": "21334", + "ThumbnailIds": ["/ik83L4ap4gYzlzGMsm2UqlIlsNe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.0, + "NumRating": 210}, {"MovieId": "9589", "Title": "Christiane F.", "Casts": [], "PlotId": "9589", + "ThumbnailIds": ["/sM989XlpNVK8ITU8sGa1I4Fw1in.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 549}, + {"MovieId": "4515", "Title": "Lions for Lambs", "Casts": [], "PlotId": "4515", + "ThumbnailIds": ["/iLuo4swOCHF2a8BlaxXZGdqETUs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 373}, {"MovieId": "8592", "Title": "Dick Tracy", "Casts": [], "PlotId": "8592", + "ThumbnailIds": ["/4UfKEGnj9jPWV11LNKBXTgge3We.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 406}, + {"MovieId": "76487", "Title": "The Devil Inside", "Casts": [], "PlotId": "76487", + "ThumbnailIds": ["/xClxu7PoHWMpCHHaqd3ZRdwOCnr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.7, + "NumRating": 431}, {"MovieId": "23706", "Title": "All About Steve", "Casts": [], "PlotId": "23706", + "ThumbnailIds": ["/Ap0Lx5mv2tvE3LL8U3VQfCNdziL.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.8, "NumRating": 525}, + {"MovieId": "626", "Title": "Un Chien Andalou", "Casts": [], "PlotId": "626", + "ThumbnailIds": ["/obvE7ElAvCUhKtWFwDSvNbPw9PV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 540}, {"MovieId": "8046", "Title": "Gigli", "Casts": [], "PlotId": "8046", + "ThumbnailIds": ["/hy4HOGwrWc8et7jUJFzE9getbsp.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 3.6, "NumRating": 187}, + {"MovieId": "19123", "Title": "Knock on Wood", "Casts": [], "PlotId": "19123", + "ThumbnailIds": ["/30dsG6akb4UYJiNDVdSTBSQsrTe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 235}, {"MovieId": "19426", "Title": "Nights of Cabiria", "Casts": [], "PlotId": "19426", + "ThumbnailIds": ["/xF4oCG3PLNbcrtPZbqB3BtkIbKg.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.0, "NumRating": 235}, + {"MovieId": "12877", "Title": "Dead Man's Shoes", "Casts": [], "PlotId": "12877", + "ThumbnailIds": ["/jGjlrDooATSSdmkrjH7lqFy6sWy.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 261}, {"MovieId": "545919", "Title": "Singel 39", "Casts": [], "PlotId": "545919", + "ThumbnailIds": ["/3SV996rVNm7KYe6HKDB6mYbu0Mp.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "350", "Title": "The Devil Wears Prada", "Casts": [], "PlotId": "350", + "ThumbnailIds": ["/8unCRm0LeiO0fM6skWAZy3ZfXR1.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.2, + "NumRating": 6394}, {"MovieId": "9516", "Title": "Menace II Society", "Casts": [], "PlotId": "9516", + "ThumbnailIds": ["/s1uQMgbK2tfaYltK6uDXU1xoYWA.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.4, "NumRating": 288}, + {"MovieId": "769", "Title": "GoodFellas", "Casts": [], "PlotId": "769", + "ThumbnailIds": ["/hAPeXBdGDGmXRPj4OZZ0poH65Iu.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.4, + "NumRating": 5504}, {"MovieId": "508763", "Title": "A Dog's Way Home", "Casts": [], "PlotId": "508763", + "ThumbnailIds": ["/pZn87R7gtmMCGGO8KeaAfZDhXLg.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.5, "NumRating": 211}, + {"MovieId": "9918", "Title": "Glory Road", "Casts": [], "PlotId": "9918", + "ThumbnailIds": ["/bGRSV5tStxDNPRLCewnOeeiZzrY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 285}, {"MovieId": "51241", "Title": "Heartbeats", "Casts": [], "PlotId": "51241", + "ThumbnailIds": ["/ne84HBcScDVO9jBzgVv5qb7rZd0.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 476}, + {"MovieId": "302", "Title": "Swimming Pool", "Casts": [], "PlotId": "302", + "ThumbnailIds": ["/mHV60wGXvvkMR9ebVO5UWvxL716.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 272}, {"MovieId": "14048", "Title": "Man on Wire", "Casts": [], "PlotId": "14048", + "ThumbnailIds": ["/86kXY7u0HWSIK27RqRnu4r0cADz.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 458}, + {"MovieId": "310", "Title": "Bruce Almighty", "Casts": [], "PlotId": "310", + "ThumbnailIds": ["/lgYKHifMMLT8OxYObMKa8b4STsr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 6093}, {"MovieId": "194662", "Title": "Birdman", "Casts": [], "PlotId": "194662", + "ThumbnailIds": ["/rSZs93P0LLxqlVEbI001UKoeCQC.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.4, "NumRating": 7597}, + {"MovieId": "1487", "Title": "Hellboy", "Casts": [], "PlotId": "1487", + "ThumbnailIds": ["/3fAWzI9MUosggdGMu7EaDhn44m6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 3930}, {"MovieId": "376540", "Title": "Mathilde", "Casts": [], "PlotId": "376540", + "ThumbnailIds": ["/kFXGRh6zIgX4e9x4ym4C4l5aVLo.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.4, "NumRating": 34}, + {"MovieId": "10538", "Title": "Passenger 57", "Casts": [], "PlotId": "10538", + "ThumbnailIds": ["/mOh1t38TkW6JVg6ylQusKBPxxqa.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.7, + "NumRating": 416}, {"MovieId": "598073", "Title": "Daymohk", "Casts": [], "PlotId": "598073", + "ThumbnailIds": ["/tXx3ihWKWbOfzXUXkqo613PyETj.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "26280", "Title": "I Killed My Mother", "Casts": [], "PlotId": "26280", + "ThumbnailIds": ["/cxb02CLALt9He1Dt0BIDKj9fdvM.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 584}, + {"MovieId": "12107", "Title": "Nutty Professor II: The Klumps", "Casts": [], "PlotId": "12107", + "ThumbnailIds": ["/r1WXXXtpNBsgCnCTdjT6ERxOcEV.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.8, + "NumRating": 661}, {"MovieId": "17927", "Title": "Fired Up!", "Casts": [], "PlotId": "17927", + "ThumbnailIds": ["/bHhLZIg9VKsconJdnwIoUTqakEG.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 304}, + {"MovieId": "12508", "Title": "Rock Star", "Casts": [], "PlotId": "12508", + "ThumbnailIds": ["/eMbsUnRfRciBGLclsqmICPOXbir.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 295}, {"MovieId": "14410", "Title": "Notorious", "Casts": [], "PlotId": "14410", + "ThumbnailIds": ["/lfKALQovRf46vVC12Rnl0mSRPyu.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 285}, + {"MovieId": "9611", "Title": "Romy and Michele's High School Reunion", "Casts": [], "PlotId": "9611", + "ThumbnailIds": ["/fvgLTx3vM3dUQqytaWCRaTdAdrs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 251}, {"MovieId": "10890", "Title": "Stripes", "Casts": [], "PlotId": "10890", + "ThumbnailIds": ["/nqfLX1bJLUlZnfflqpUIWrGQwSv.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 405}, + {"MovieId": "11983", "Title": "Proof of Life", "Casts": [], "PlotId": "11983", + "ThumbnailIds": ["/qyNoOGCajNWH6heJ0kAmFFJtRQS.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 347}, + {"MovieId": "14976", "Title": "Rachel Getting Married", "Casts": [], "PlotId": "14976", + "ThumbnailIds": ["/rq1z6yiYW1LTgXZhsyNgmtmOedK.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.1, + "NumRating": 291}, {"MovieId": "84306", "Title": "Liberal Arts", "Casts": [], "PlotId": "84306", + "ThumbnailIds": ["/c9Q2to1Jpq8a0Zqto6Mqc1ujLYn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.3, "NumRating": 278}, + {"MovieId": "7862", "Title": "The Counterfeiters", "Casts": [], "PlotId": "7862", + "ThumbnailIds": ["/bRQddrgVemZtFdnrPy9AxTpkhpj.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 235}, {"MovieId": "50797", "Title": "Burnt by the Sun", "Casts": [], "PlotId": "50797", + "ThumbnailIds": ["/e7bE9BiLyKaZtNF3Q2ro8gVCEQA.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.2, "NumRating": 60}, + {"MovieId": "605", "Title": "The Matrix Revolutions", "Casts": [], "PlotId": "605", + "ThumbnailIds": ["/2aJvwc4zXqtVUDbEu62e14J0mhe.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 5033}, {"MovieId": "9957", "Title": "The Benchwarmers", "Casts": [], "PlotId": "9957", + "ThumbnailIds": ["/zQy6T5qV0lvLkkKi5S9VIYykSQD.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.4, "NumRating": 386}, + {"MovieId": "1422", "Title": "The Departed", "Casts": [], "PlotId": "1422", + "ThumbnailIds": ["/tGLO9zw5ZtCeyyEWgbYGgsFxC6i.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 7729}, {"MovieId": "13973", "Title": "Choke", "Casts": [], "PlotId": "13973", + "ThumbnailIds": ["/lltyV3NNtzm4jaI5SfOYYsrr2Cz.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.2, "NumRating": 197}, + {"MovieId": "359940", "Title": "Three Billboards Outside Ebbing, Missouri", "Casts": [], + "PlotId": "359940", "ThumbnailIds": ["/vgvw6w1CtcFkuXXn004S5wQsHRl.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 8.2, "NumRating": 5306}, + {"MovieId": "13920", "Title": "Radio", "Casts": [], "PlotId": "13920", + "ThumbnailIds": ["/t4b6Ff8N0cvMBeYFtDNDm6xvyC9.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 289}, {"MovieId": "500664", "Title": "Upgrade", "Casts": [], "PlotId": "500664", + "ThumbnailIds": ["/8fDtXi6gVw8WUMWGT9XFz7YwkuE.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 1254}, + {"MovieId": "9410", "Title": "Great Expectations", "Casts": [], "PlotId": "9410", + "ThumbnailIds": ["/djLsfl3lm7BZKWsKfVZ2wM1ZMrP.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 244}, + {"MovieId": "339964", "Title": "Valerian and the City of a Thousand Planets", "Casts": [], + "PlotId": "339964", "ThumbnailIds": ["/jfIpMh79fGRqYJ6PwZLCntzgxlF.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 4065}, + {"MovieId": "425591", "Title": "I Don't Feel at Home in This World Anymore", "Casts": [], + "PlotId": "425591", "ThumbnailIds": ["/1stdUlXBc3nxqhdWvZ6wWWEbCQW.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.6, "NumRating": 582}, + {"MovieId": "598549", "Title": "Low Budget (B.O.)", "Casts": [], "PlotId": "598549", + "ThumbnailIds": ["/qAu9YRsNdLXLH2Mg8mkpr5RBZ8f.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "449563", "Title": "Isn't It Romantic", "Casts": [], "PlotId": "449563", + "ThumbnailIds": ["/5xNBYXuv8wqiLVDhsfqCOr75DL7.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.4, "NumRating": 1509}, + {"MovieId": "10069", "Title": "Stay Alive", "Casts": [], "PlotId": "10069", + "ThumbnailIds": ["/9iPpBKxpkgxHtsyGcnSlSN5KTu6.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.4, + "NumRating": 303}, {"MovieId": "9942", "Title": "Major League", "Casts": [], "PlotId": "9942", + "ThumbnailIds": ["/nD8hYPxh4Ph12dAsWiH3nQbqYDW.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.7, "NumRating": 332}, + {"MovieId": "577109", "Title": "To Paris!", "Casts": [], "PlotId": "577109", + "ThumbnailIds": ["/m5mInKktHKCOob7mpKBN6zEZPAx.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, {"MovieId": "10823", "Title": "Children of the Corn", "Casts": [], "PlotId": "10823", + "ThumbnailIds": ["/votpiVmvic2ULYe74GES8CxYgp1.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.7, "NumRating": 401}, + {"MovieId": "31175", "Title": "Soul Kitchen", "Casts": [], "PlotId": "31175", + "ThumbnailIds": ["/vUxpCAViRHmW4iIuAiwUxisQ5Wz.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 215}, {"MovieId": "363", "Title": "Head-On", "Casts": [], "PlotId": "363", + "ThumbnailIds": ["/5cPrr36unu3TTbdLdrfV1o7t8Sp.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.5, "NumRating": 232}, + {"MovieId": "13250", "Title": "Butterfly on a Wheel", "Casts": [], "PlotId": "13250", + "ThumbnailIds": ["/wWe26HWNIogpYpGdvzizvBeH77p.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.5, + "NumRating": 238}, + {"MovieId": "12140", "Title": "Ghost in the Shell 2: Innocence", "Casts": [], "PlotId": "12140", + "ThumbnailIds": ["/1KSMfpMWMzlvR2DN2fzv31vZ66B.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 364}, {"MovieId": "297761", "Title": "Suicide Squad", "Casts": [], "PlotId": "297761", + "ThumbnailIds": ["/e1mjopzAS2KNsvpbpahQ1a6SkSn.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 13345}, + {"MovieId": "8270", "Title": "The Lookout", "Casts": [], "PlotId": "8270", + "ThumbnailIds": ["/jSJca1tqlj08fGeqngTmr4UJ9ON.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.6, + "NumRating": 325}, {"MovieId": "44129", "Title": "The Company Men", "Casts": [], "PlotId": "44129", + "ThumbnailIds": ["/xP1FWgiUT1v1GGCwd94hXLaHyg8.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 348}, + {"MovieId": "55725", "Title": "Win Win", "Casts": [], "PlotId": "55725", + "ThumbnailIds": ["/s8Q2YqsJWXdZeT8Bpuzxic8UVd1.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.8, + "NumRating": 263}, {"MovieId": "10053", "Title": "When a Stranger Calls", "Casts": [], "PlotId": "10053", + "ThumbnailIds": ["/pYxd8DM4qBV24rJ9Jf84SYtIao1.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 519}, + {"MovieId": "11252", "Title": "Psycho", "Casts": [], "PlotId": "11252", + "ThumbnailIds": ["/5emaTTGS8rnLgZrBS98zqv1XWqE.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.1, + "NumRating": 457}, {"MovieId": "11894", "Title": "Curly Sue", "Casts": [], "PlotId": "11894", + "ThumbnailIds": ["/4NE36Icn6jrt7YHHXbB7zvNu9gq.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.9, "NumRating": 225}, + {"MovieId": "2639", "Title": "Deconstructing Harry", "Casts": [], "PlotId": "2639", + "ThumbnailIds": ["/krKRthefSZlUjnzDvN4isqty0R7.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.3, + "NumRating": 345}, {"MovieId": "13446", "Title": "Withnail & I", "Casts": [], "PlotId": "13446", + "ThumbnailIds": ["/lXD5UR2dvXJF54AIBt8G2kDvYGk.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.4, "NumRating": 256}, + {"MovieId": "354912", "Title": "Coco", "Casts": [], "PlotId": "354912", + "ThumbnailIds": ["/eKi8dIrr8voobbaGzDpe8w0PVbC.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.2, + "NumRating": 8460}, {"MovieId": "808", "Title": "Shrek", "Casts": [], "PlotId": "808", + "ThumbnailIds": ["/140ewbWv8qHStD3mlBDvvGd0Zvu.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.6, "NumRating": 8427}, + {"MovieId": "315837", "Title": "Ghost in the Shell", "Casts": [], "PlotId": "315837", + "ThumbnailIds": ["/si1ZyELNHdPUZw4pXR5KjMIIsBF.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 5054}, {"MovieId": "570426", "Title": "Enterrados", "Casts": [], "PlotId": "570426", + "ThumbnailIds": ["/e8Hb4g3TEPFySwq5iao2IuUbEdb.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "10564", "Title": "Where the Heart Is", "Casts": [], "PlotId": "10564", + "ThumbnailIds": ["/rNP5NSdXzl6y5CuQ6Sh2JmEnvt5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 338}, {"MovieId": "475888", "Title": "Tell It to the Bees", "Casts": [], "PlotId": "475888", + "ThumbnailIds": ["/Rj6zpHhMU1zFW3v28U4PSRSRgP.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.4, "NumRating": 12}, + {"MovieId": "8088", "Title": "Broken Embraces", "Casts": [], "PlotId": "8088", + "ThumbnailIds": ["/9C6UiobINVB9AWKSazi0huHYyFY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.1, + "NumRating": 283}, {"MovieId": "14052", "Title": "Revenge of the Nerds", "Casts": [], "PlotId": "14052", + "ThumbnailIds": ["/wCcMAx1J4JxBviEzy5Ay2FW79Nb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.5, "NumRating": 324}, + {"MovieId": "68924", "Title": "The Ice Storm", "Casts": [], "PlotId": "68924", + "ThumbnailIds": ["/e3j1Pn67bWos1pnBDE9GJNeAtcT.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.0, + "NumRating": 300}, {"MovieId": "427641", "Title": "Rampage", "Casts": [], "PlotId": "427641", + "ThumbnailIds": ["/vvrXUbiwFl4H8pJE7a4DzOoqVNB.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.2, "NumRating": 3054}, + {"MovieId": "98", "Title": "Gladiator", "Casts": [], "PlotId": "98", + "ThumbnailIds": ["/6WBIzCgmDCYrqh64yDREGeDk9d3.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 9530}, {"MovieId": "43959", "Title": "Soul Surfer", "Casts": [], "PlotId": "43959", + "ThumbnailIds": ["/t7twsxCK3vIkn4z0w4WwHuVZPNy.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.0, "NumRating": 674}, + {"MovieId": "14197", "Title": "My Sassy Girl", "Casts": [], "PlotId": "14197", + "ThumbnailIds": ["/PuHS7IRd3tRnkscBDq9AQ8wDHr.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.2, + "NumRating": 162}, {"MovieId": "13252", "Title": "Cleaner", "Casts": [], "PlotId": "13252", + "ThumbnailIds": ["/bhZ8LZXNOAMTpziYiE4ebn7est4.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.0, "NumRating": 342}, + {"MovieId": "18079", "Title": "Nine Queens", "Casts": [], "PlotId": "18079", + "ThumbnailIds": ["/3LS2qYsmdYoSwQWJulK73lDfz52.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.7, + "NumRating": 281}, {"MovieId": "1368", "Title": "First Blood", "Casts": [], "PlotId": "1368", + "ThumbnailIds": ["/bbYNNEGLXrV3lJpHDg7CKaPscCb.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.3, "NumRating": 2723}, + {"MovieId": "10843", "Title": "After Hours", "Casts": [], "PlotId": "10843", + "ThumbnailIds": ["/s5XkBqUMwE0wQv9NY0XERs64cgs.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.5, + "NumRating": 476}, {"MovieId": "45657", "Title": "The Ward", "Casts": [], "PlotId": "45657", + "ThumbnailIds": ["/cMApNxaSq4udLTYBAd755E8VJok.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.8, "NumRating": 601}, + {"MovieId": "4967", "Title": "Keeping the Faith", "Casts": [], "PlotId": "4967", + "ThumbnailIds": ["/wbW4OnSNcuywgmJFdlvL8SvHx4.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.0, + "NumRating": 286}, {"MovieId": "37724", "Title": "Skyfall", "Casts": [], "PlotId": "37724", + "ThumbnailIds": ["/lQCkPLDxFONmgzrWLvq085v1g2d.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.1, "NumRating": 10328}, + {"MovieId": "954", "Title": "Mission: Impossible", "Casts": [], "PlotId": "954", + "ThumbnailIds": ["/1PVKS17pIBFsIhgFws2uagPDNLW.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.9, + "NumRating": 4636}, {"MovieId": "10741", "Title": "Bobby", "Casts": [], "PlotId": "10741", + "ThumbnailIds": ["/a38CTs2mJ2uDHHH6mUJmuGH8RVI.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.6, "NumRating": 194}, + {"MovieId": "598062", "Title": "Lumpkin, GA", "Casts": [], "PlotId": "598062", + "ThumbnailIds": ["/hLqsvhDrVK5my1fJVIZa0INrtB3.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, + "NumRating": 0}, + {"MovieId": "595409", "Title": "The Caring City", "Casts": [], "PlotId": "595409", "ThumbnailIds": [None], + "PhotoIds": [], "VideoIds": [], "AvgRating": 0.0, "NumRating": 0}, + {"MovieId": "12177", "Title": "The Love Guru", "Casts": [], "PlotId": "12177", + "ThumbnailIds": ["/q3g4UFCMPqbKXfc3qMOsYQBOGbl.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 4.2, + "NumRating": 384}, + {"MovieId": "1979", "Title": "Fantastic Four: Rise of the Silver Surfer", "Casts": [], "PlotId": "1979", + "ThumbnailIds": ["/fXpziQgnBnB4bLgjKhjTbLQumE5.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.5, + "NumRating": 4668}, {"MovieId": "458344", "Title": "Juliet, Naked", "Casts": [], "PlotId": "458344", + "ThumbnailIds": ["/tj4lbeWQBvPwGjadEAAjJdQolko.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.5, "NumRating": 118}, + {"MovieId": "201088", "Title": "Blackhat", "Casts": [], "PlotId": "201088", + "ThumbnailIds": ["/sW3VEsulmxMlOmQwm0h7H7lZROi.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.3, + "NumRating": 1154}, {"MovieId": "10543", "Title": "Fear", "Casts": [], "PlotId": "10543", + "ThumbnailIds": ["/reA4wlTY9qGwryVftscFT2q0ZLK.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 6.2, "NumRating": 283}, + {"MovieId": "38031", "Title": "You Will Meet a Tall Dark Stranger", "Casts": [], "PlotId": "38031", + "ThumbnailIds": ["/gbwJPEQZTSjjBAlv8DofNZVWiO.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.8, + "NumRating": 429}, {"MovieId": "11973", "Title": "Thirteen Days", "Casts": [], "PlotId": "11973", + "ThumbnailIds": ["/37yRUulECxF96eIok4ZXB8nRJ11.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 7.0, "NumRating": 314}, + {"MovieId": "564", "Title": "The Mummy", "Casts": [], "PlotId": "564", + "ThumbnailIds": ["/yhIsVvcUm7QxzLfT6HW2wLf5ajY.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.7, + "NumRating": 4854}, {"MovieId": "13159", "Title": "Georgia Rule", "Casts": [], "PlotId": "13159", + "ThumbnailIds": ["/hEf4AlcN0wuVby5ysKf2RuckUgj.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 5.8, "NumRating": 229}, + {"MovieId": "10849", "Title": "The Purple Rose of Cairo", "Casts": [], "PlotId": "10849", + "ThumbnailIds": ["/nWMT2WBzPtrXdjPlb28PFSrBGd0.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 7.4, + "NumRating": 407}, {"MovieId": "7095", "Title": "Jack", "Casts": [], "PlotId": "7095", + "ThumbnailIds": ["/nOgHa5ggt6vbmIuoNW7exrJQJq9.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 6.1, "NumRating": 596}, + {"MovieId": "103", "Title": "Taxi Driver", "Casts": [], "PlotId": "103", + "ThumbnailIds": ["/ekstpH614fwDX8DUln1a2Opz0N8.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 8.1, + "NumRating": 5125}, {"MovieId": "26900", "Title": "The Bandit", "Casts": [], "PlotId": "26900", + "ThumbnailIds": ["/u1o4yJg0xkeVV8gfJL49CTZRBiu.jpg"], "PhotoIds": [], + "VideoIds": [], "AvgRating": 7.5, "NumRating": 110}, + {"MovieId": "13279", "Title": "Lakeview Terrace", "Casts": [], "PlotId": "13279", + "ThumbnailIds": ["/iOnQK5te4gEhHpyZ5KpGCtbNn1B.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 5.9, + "NumRating": 478}, {"MovieId": "241251", "Title": "The Boy Next Door", "Casts": [], "PlotId": "241251", + "ThumbnailIds": ["/h28t2JNNGrZx0fIuAw8aHQFhIxR.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 4.4, "NumRating": 1371}, + {"MovieId": "8987", "Title": "The River Wild", "Casts": [], "PlotId": "8987", + "ThumbnailIds": ["/8NFAQxZud5Kisw1sd9WxfBG54.jpg"], "PhotoIds": [], "VideoIds": [], "AvgRating": 6.4, + "NumRating": 396}, {"MovieId": "6309", "Title": "Flood", "Casts": [], "PlotId": "6309", + "ThumbnailIds": ["/s0F2iDMtj6uYQPqbjwCPlwug2O4.jpg"], "PhotoIds": [], "VideoIds": [], + "AvgRating": 5.5, "NumRating": 63}] diff --git a/deathstar_movie_review/test_movie_review_demo.py b/deathstar_movie_review/test_movie_review_demo.py new file mode 100644 index 0000000..78e3358 --- /dev/null +++ b/deathstar_movie_review/test_movie_review_demo.py @@ -0,0 +1,101 @@ +from cascade.dataflow.dataflow import Event, InitClass, InvokeMethod, OpNode +from cascade.dataflow.optimization.dead_node_elim import dead_node_elimination +from cascade.runtime.python_runtime import PythonClientSync, PythonRuntime +from deathstar_movie_review.entities.compose_review import ComposeReview, compose_review_op +from deathstar_movie_review.entities.user import User, user_op +from deathstar_movie_review.entities.movie import MovieId, movie_id_op, movie_info_op, plot_op +from deathstar_movie_review.entities.frontend import frontend_op, text_op, unique_id_op + + + +def test_deathstar_movie_demo_python(): + print("starting") + runtime = PythonRuntime() + + print(frontend_op.dataflow.to_dot()) + dead_node_elimination([], [frontend_op]) + print(frontend_op.dataflow.to_dot()) + + runtime.add_operator(compose_review_op) + runtime.add_operator(user_op) + runtime.add_operator(movie_info_op) + runtime.add_operator(movie_id_op) + runtime.add_operator(plot_op) + runtime.add_stateless_operator(frontend_op) + runtime.add_stateless_operator(unique_id_op) + runtime.add_stateless_operator(text_op) + + runtime.run() + client = PythonClientSync(runtime) + + init_user = OpNode(User, InitClass(), read_key_from="username") + username = "username_1" + user_data = { + "userId": "user1", + "FirstName": "firstname", + "LastName": "lastname", + "Username": username, + "Password": "****", + "Salt": "salt" + } + print("testing user create") + event = Event(init_user, {"username": username, "user_data": user_data}, None) + result = client.send(event) + assert isinstance(result, User) and result.username == username + + print("testing compose review") + req_id = 1 + movie_title = "Cars 2" + movie_id = 1 + + # make the review + init_compose_review = OpNode(ComposeReview, InitClass(), read_key_from="req_id") + event = Event(init_compose_review, {"req_id": req_id}, None) + result = client.send(event) + print("review made") + + + # make the movie + init_movie = OpNode(MovieId, InitClass(), read_key_from="title") + event = Event(init_movie, {"title": movie_title, "movie_id": movie_id}, None) + result = client.send(event) + print("movie made") + + # compose the review + review_data = { + "review": req_id, + "user": username, + "title": movie_title, + "rating": 5, + "text": "good movie!" + } + + event = Event( + frontend_op.dataflow.entry, + review_data, + frontend_op.dataflow) + result = client.send(event) + print(result) + print("review composed") + + + # read the review + get_review = OpNode(ComposeReview, InvokeMethod("get_data"), read_key_from="req_id") + event = Event( + get_review, + {"req_id": req_id}, + None + ) + result = client.send(event) + expected = { + "userId": user_data["userId"], + "movieId": movie_id, + "text": review_data["text"] + } + print(result, expected) + assert "review_id" in result + del result["review_id"] # randomly generated + assert result == expected + + print("Success!") + \ No newline at end of file diff --git a/deathstar_movie_review/workload_data.py b/deathstar_movie_review/workload_data.py new file mode 100644 index 0000000..b533e36 --- /dev/null +++ b/deathstar_movie_review/workload_data.py @@ -0,0 +1,1008 @@ +charset = ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'a', 's', + 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'Q', + 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', + 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '1', '2', '3', '4', '5', + '6', '7', '8', '9', '0'] + +movie_titles = [ + "Avengers: Endgame", + "Kamen Rider Heisei Generations FOREVER", + "Captain Marvel", + "Pokémon Detective Pikachu", + "Hellboy", + "After", + "Avengers: Infinity War", + "Doraemon the Movie: Nobita's Treasure Island", + "Cold Pursuit", + "Shazam!", + "What Men Want", + "Glass", + "The Avengers", + "Black Panther", + "How to Train Your Dragon: The Hidden World", + "Tolkien", + "Fighting with My Family", + "Guardians of the Galaxy Vol. 2", + "Avengers: Age of Ultron", + "The Prodigy", + "Cars", + "Thor: Ragnarok", + "The Wandering Earth", + "Extremely Wicked, Shockingly Evil and Vile", + "Pet Sematary", + "Logan", + "Guardians of the Galaxy", + "Fall in Love at First Kiss", + "Spider-Man: Into the Spider-Verse", + "Bumblebee", + "Thor", + "Aquaman", + "Ant-Man and the Wasp", + "The Lego Movie 2: The Second Part", + "Edge of Tomorrow", + "The Hustle", + "Dumbo", + "Redcon-1", + "Instant Family", + "Spider-Man: Homecoming", + "The Hobbit: The Battle of the Five Armies", + "Little", + "The Curse of La Llorona", + "Escape Room", + "Miss Bala", + "High Life", + "The Highwaymen", + "The Fate of the Furious", + "Iron Man", + "Captain America: The First Avenger", + "Captain America: Civil War", + "Us", + "Detective Conan: The Fist of Blue Sapphire", + "Fate/stay night: Heaven’s Feel II. lost butterfly", + "Robin Hood", + "Fantastic Beasts: The Crimes of Grindelwald", + "Thor: The Dark World", + "Alita: Battle Angel", + "Happy Death Day 2U", + "John Wick", + "Mile 22", + "Iron Man 2", + "Maggie", + "Venom", + "Harry Potter and the Philosopher's Stone", + "The Kid Who Would Be King", + "Pirates of the Caribbean: The Curse of the Black Pearl", + "Bohemian Rhapsody", + "The Mule", + "Ralph Breaks the Internet", + "The Lord of the Rings: The Fellowship of the Ring", + "Death Wish", + "John Wick: Chapter 3 – Parabellum", + "The Sisters Brothers", + "We Are Your Friends", + "Vaarikkuzhiyile Kolapathakam", + "The First Purge", + "Left Behind", + "Deadpool 2", + "BlacKkKlansman", + "Jigsaw", + "The Kissing Booth", + "Shaun the Sheep Movie", + "Under Siege 2: Dark Territory", + "Truth or Dare", + "Doctor Strange", + "The Lord of the Rings: The Return of the King", + "It's a Wonderful Life", + "Interstellar", + "Bad Times at the El Royale", + "A Madea Family Funeral", + "Long Shot", + "Suspiria", + "The Dark Knight", + "Trainspotting", + "Star Wars", + "Mortal Engines", + "Loving Vincent", + "Jurassic World: Fallen Kingdom", + "Velvet Buzzsaw", + "A Wrinkle in Time", + "The Lion King 1½", + "Spider-Man: Far from Home", + "Harry Potter and the Chamber of Secrets", + "Solo: A Star Wars Story", + "The Last Summer", + "Iron Man 3", + "Eighth Grade", + "Mandy", + "Die Hard", + "Home Alone 4", + "Adrift", + "The Predator", + "The Favourite", + "Overlord", + "Camp X-Ray", + "Mary Poppins Returns", + "Master Z: Ip Man Legacy", + "Re: Zero kara Hajimeru Isekai Seikatsu - Memory Snow", + "Scooby-Doo 2: Monsters Unleashed", + "The Transporter Refueled", + "Personal Shopper", + "Endless Love", + "Ant-Man", + "Polar", + "The Silence", + "The Professor and the Madman", + "12 Strong", + "Anon", + "Brightburn", + "Outlaw King", + "Texas Chainsaw 3D", + "The Upside", + "Breakthrough", + "The Emoji Movie", + "Green Book", + "A Bad Moms Christmas", + "Deadpool", + "Herbie Fully Loaded", + "Batman v Superman: Dawn of Justice", + "Bel Ami", + "A Hologram for the King", + "The Hunger Games: Mockingjay - Part 1", + "Planes", + "Wrong Turn 2: Dead End", + "Zodiac", + "The Curious Case of Benjamin Button", + "Wonder Woman", + "A Goofy Movie", + "The Incredible Hulk", + "San Andreas", + "Creed II", + "X-Men: Days of Future Past", + "Executive Decision", + "The Man Who Knew Too Much", + "Footloose", + "Justice League", + "47 Meters Down", + "A Star Is Born", + "Incredibles 2", + "The Square", + "Raw", + "Regression", + "Red Cliff", + "Star Wars: The Last Jedi", + "Hunter Killer", + "Chain Reaction", + "The Wind That Shakes the Barley", + "Last Knights", + "Avatar", + "The Matrix", + "I Spit on Your Grave 2", + "The 100 Year-Old Man Who Climbed Out the Window and Disappeared", + "2046", + "The Smurfs 2", + "Victoria", + "The Gunman", + "Batman Begins", + "I, Daniel Blake", + "Secret in Their Eyes", + "The Man Who Knew Infinity", + "The Past", + "The Belko Experiment", + "The Last Legion", + "Free State of Jones", + "Deadfall", + "The Shawshank Redemption", + "Anthropoid", + "Pitch Perfect 3", + "The Cold Light of Day", + "March of the Penguins", + "Popstar: Never Stop Never Stopping", + "The Cat Returns", + "The Zookeeper's Wife", + "Aladdin", + "Suburbicon", + "The Water Horse", + "Inception", + "The Return of the Living Dead", + "Odd Thomas", + "Jungle", + "Pulp Fiction", + "Hard Target", + "Iron Sky: The Coming Race", + "Terminator 2: Judgment Day", + "Legend", + "Twilight", + "Stealth", + "Battle of the Sexes", + "Split", + "Adore", + "Don't Say a Word", + "A Royal Affair", + "Salò, or the 120 Days of Sodom", + "Triple Frontier", + "The Rescuers", + "Blade Runner 2049", + "Tale of Tales", + "Taken 3", + "The Tale of Despereaux", + "It's Only the End of the World", + "Dark Places", + "Poms", + "Dragged Across Concrete", + "Unsane", + "15 Minutes", + "Batman: The Dark Knight Returns, Part 1", + "Ironclad", + "Taxi", + "Basic Instinct 2", + "Florence Foster Jenkins", + "Gone Girl", + "Rough Night", + "Schindler's List", + "Alexander and the Terrible, Horrible, No Good, Very Bad Day", + "The Great Mouse Detective", + "The Ring Two", + "Toni Erdmann", + "Forbidden Planet", + "The Lazarus Effect", + "The Triplets of Belleville", + "Heartbreaker", + "Someone Great", + "Veronica Mars", + "Byzantium", + "13 Sins", + "Mission: Impossible - Fallout", + "Far from the Madding Crowd", + "Charlie Says", + "'71", + "The Proposition", + "The Railway Man", + "A Ghost Story", + "The Choice", + "A Nightmare on Elm Street 4: The Dream Master", + "Bitter Moon", + "Arrival", + "Alvin and the Chipmunks: Chipwrecked", + "The Salesman", + "Soldier", + "The Lord of the Rings: The Two Towers", + "Sicario: Day of the Soldado", + "Mia and the White Lion", + "Vice", + "Harry Potter and the Deathly Hallows: Part 1", + "Zootopia", + "Whiteout", + "The Amazing Spider-Man", + "The Santa Clause 2", + "Five Feet Apart", + "Star Wars: The Force Awakens", + "No Man's Land", + "Whiplash", + "Hot Tub Time Machine 2", + "The Babysitter", + "The Paperboy", + "The Maze Runner", + "Jason X", + "Harry Potter and the Order of the Phoenix", + "Terms of Endearment", + "Frozen", + "Invasion of the Body Snatchers", + "The Divide", + "Brawl in Cell Block 99", + "Killing Season", + "Harry Potter and the Prisoner of Azkaban", + "Man Up", + "Missing Link", + "Friday the 13th Part 2", + "Force Majeure", + "Léon: The Professional", + "Suck Me Shakespeer", + "Teenage Mutant Ninja Turtles II: The Secret of the Ooze", + "Blood Father", + "Ivan's Childhood", + "The Garden of Words", + "All Dogs Go to Heaven", + "The Hobbit: An Unexpected Journey", + "Step Up All In", + "New Nightmare", + "Slow West", + "The Intruder", + "Arctic", + "Transsiberian", + "W.", + "Mongol: The Rise of Genghis Khan", + "Man of Tai Chi", + "Welcome to the Punch", + "Paranormal Activity: The Marked Ones", + "The Imitation Game", + "Police Academy 4: Citizens on Patrol", + "Fury", + "A Bridge Too Far", + "Citizenfour", + "Wolf Children", + "The Good Girl", + "To All the Boys I've Loved Before", + "The Nun", + "Black Sea", + "Flashdance", + "Where Eagles Dare", + "Rules of Engagement", + "Ladyhawke", + "The Meg", + "How to Train Your Dragon 2", + "Harry Potter and the Half-Blood Prince", + "Che: Part One", + "The Rover", + "Student of the Year 2", + "Destroyer", + "I Give It a Year", + "Two Days, One Night", + "The Ritual", + "Underdogs", + "Ida", + "Pirates of the Caribbean: Dead Man's Chest", + "Session 9", + "BloodRayne", + "Batman: The Killing Joke", + "Frankenstein", + "Kill the Messenger", + "The Imposter", + "Replicas", + "Maps to the Stars", + "Innerspace", + "Oliver & Company", + "Ladder 49", + "Snow White and the Seven Dwarfs", + "In the Name of the King: A Dungeon Siege Tale", + "War of the Worlds", + "Life", + "Child's Play 2", + "Sneakers", + "Fight Club", + "How to Train Your Dragon", + "Pathfinder", + "Colette", + "Joe", + "Batman: Gotham Knight", + "RoboCop 3", + "Drunken Master", + "Garfield: A Tail of Two Kitties", + "Miss & Mrs. Cops", + "The Fog", + "Pawn Sacrifice", + "The Equalizer 2", + "The Legend of Drunken Master", + "American Pie Presents: The Book of Love", + "Star Trek V: The Final Frontier", + "Blade", + "Tammy", + "Young Guns", + "Labor Day", + "Police Academy 3: Back in Training", + "Hellbound: Hellraiser II", + "The Reaping", + "The Axiom", + "Sexy Beast", + "The Big Short", + "The Human Centipede 2 (Full Sequence)", + "On the Basis of Sex", + "The Haunted Mansion", + "The Rebound", + "Ghosts of Mars", + "Nightcrawler", + "Absolute Power", + "Woman in Gold", + "Ready Player One", + "Disclosure", + "The Money Pit", + "Le Samouraï", + "Murder on the Orient Express", + "The Martian", + "Exposed", + "Flight of the Phoenix", + "Out of Africa", + "The Lunchbox", + "Terminator Genisys", + "The Purge: Anarchy", + "In the House", + "The General's Daughter", + "Under the Silver Lake", + "National Lampoon's European Vacation", + "The Muppet Christmas Carol", + "Hysteria", + "X-Men: Apocalypse", + "Operation Condor", + "Tomb Raider", + "Titanic", + "The Swan Princess", + "Very Bad Things", + "Suffragette", + "The Grudge 2", + "Scouts Guide to the Zombie Apocalypse", + "My Neighbor Totoro", + "The Tall Man", + "Magnum Force", + "Spider-Man 3", + "Asterix: The Secret of the Magic Potion", + "Dragonfly", + "French Kiss", + "The Devil's Own", + "Cinderella", + "3some", + "An American Tail", + "Dog Soldiers", + "Firewall", + "Dark Phoenix", + "Scanners", + "RV", + "2 Days in New York", + "Friends with Kids", + "An Officer and a Gentleman", + "102 Dalmatians", + "Freaks", + "Jurassic World", + "Under the Tuscan Sun", + "The Colony", + "Million Dollar Arm", + "Hard Boiled", + "Flash Gordon", + "Bound", + "All Quiet on the Western Front", + "Heist", + "The Three Musketeers", + "2010", + "Duck Soup", + "Mary Queen of Scots", + "Charlie Countryman", + "Mr. Right", + "Arthur and the Invisibles", + "Curse of the Golden Flower", + "Drinking Buddies", + "Harry Potter and the Goblet of Fire", + "Pathology", + "Over the Top", + "The Lion King", + "Driven", + "The Hunger Games: Mockingjay - Part 2", + "John Wick: Chapter 2", + "Sister Act 2: Back in the Habit", + "Halloween II", + "Crypto", + "The 12th Man", + "The East", + "Love of My Life", + "The Godfather", + "Eight Legged Freaks", + "Knocked Up", + "Dracula Untold", + "Excalibur", + "Somewhere", + "Pirates of the Caribbean: Dead Men Tell No Tales", + "Harry Potter and the Deathly Hallows: Part 2", + "Cannibal Holocaust", + "Old Dogs", + "The Equalizer", + "Redirected", + "Tokyo Story", + "James and the Giant Peach", + "Bride of Chucky", + "Maniac", + "The Lawnmower Man", + "Goal! II: Living the Dream", + "Everybody Wants Some!!", + "The Longest Day", + "Big Hero 6", + "Love Happens", + "Baby's Day Out", + "Happy Feet Two", + "Undisputed III: Redemption", + "Fred Claus", + "A Man Apart", + "Untraceable", + "Anatomy of a Murder", + "Wild and Free", + "Draft Day", + "The Medallion", + "Astro Boy", + "Song to Song", + "Neon Heart", + "Triple Threat", + "The Client", + "Starred Up", + "Cradle 2 the Grave", + "Unbreakable", + "Toy Story", + "I Am Mother", + "Salyut-7", + "First Man", + "New in Town", + "Marathon Man", + "DOA: Dead or Alive", + "Holmes & Watson", + "Lock Up", + "Mimic", + "The War of the Roses", + "Crash", + "Look Who's Talking Too", + "Spectral", + "3-Iron", + "Pale Rider", + "Wyatt Earp", + "The Dirt", + "Cocoon", + "Ichi the Killer", + "Big Momma's House 2", + "Beauty and the Beast", + "Breakdown", + "Lovelace", + "Take Me Home Tonight", + "Battleship Potemkin", + "Bullitt", + "Son of Saul", + "Transformers: The Last Knight", + "Duplicity", + "Double Impact", + "Far from Heaven", + "Striking Distance", + "The Shape of Water", + "Friday Night Lights", + "Annihilation", + "Dawn of the Planet of the Apes", + "The Crying Game", + "After the Sunset", + "The Dark Knight Rises", + "Parenthood", + "Passengers", + "The Gift", + "The Twelve Tasks of Asterix", + "After.Life", + "A Trip to the Moon", + "Don't Look Now", + "Playing for Keeps", + "Quills", + "Frantic", + "Guava Island", + "Turistas", + "Bulletproof Monk", + "High Plains Drifter", + "Man of Steel", + "McFarland, USA", + "Cat on a Hot Tin Roof", + "Goosebumps", + "Inland Empire", + "The Hunted", + "Enemy Mine", + "Marnie", + "National Lampoon's Loaded Weapon 1", + "The Four Feathers", + "Get Out", + "Trust", + "The Jewel of the Nile", + "My Own Private Idaho", + "Departures", + "Mirror", + "My Boss's Daughter", + "Goosebumps 2: Haunted Halloween", + "Fahrenheit 451", + "A Nightmare on Elm Street: The Dream Child", + "Crossroads", + "The Amazing Spider-Man 2", + "Rio Bravo", + "His Girl Friday", + "Kinsey", + "Howard the Duck", + "Red Sonja", + "Paperman", + "Sharknado", + "Vampires", + "Alone in the Dark", + "The Cranes Are Flying", + "The Prince & Me", + "Agent Cody Banks", + "Friday the 13th Part III", + "Dracula 2000", + "Bill & Ted's Bogus Journey", + "Lake Placid", + "War for the Planet of the Apes", + "The Corrupted", + "Nine", + "Did You Hear About the Morgans?", + "I Spy", + "The Ridiculous 6", + "Belle de Jour", + "The Libertine", + "The Extraordinary Adventures of Adèle Blanc-Sec", + "Working Girl", + "Forrest Gump", + "Captain America: The Winter Soldier", + "The Pink Panther", + "Spy Kids: All the Time in the World", + "The Right Stuff", + "See No Evil, Hear No Evil", + "U Turn", + "sex, lies, and videotape", + "Tideland", + "Down by Law", + "The Mission", + "Hellboy II: The Golden Army", + "3 Men and a Baby", + "Bad Words", + "Chariots of Fire", + "Spectre", + "People Like Us", + "Animal Kingdom", + "The Ledge", + "Copycat", + "Double Jeopardy", + "Super Mario Bros.", + "Bad Company", + "Striptease", + "Dolores Claiborne", + "Flyboys", + "Time Bandits", + "The Fan", + "Little Women", + "Away We Go", + "The Condemned", + "BASEketball", + "The Hitcher", + "A Long Way Down", + "Cabaret", + "The Normal Heart", + "Goya's Ghosts", + "The Odd Life of Timothy Green", + "Chalet Girl", + "The Cat in the Hat", + "Alice in Wonderland", + "Raising Helen", + "Problem Child 2", + "Westworld", + "Spider-Man", + "Widows", + "Show Me Love", + "The Player", + "Alien: Covenant", + "Shadow of a Doubt", + "Young & Beautiful", + "Vidocq", + "28 Days", + "Mad Max: Fury Road", + "The Perfect Date", + "The Barber of Siberia", + "Dr. Dolittle 2", + "Flypaper", + "Predator", + "The Wages of Fear", + "The Wolf of Wall Street", + "In the Land of Women", + "Enough Said", + "A Simple Favor", + "Once Upon a Deadpool", + "This Is It", + "The Descent: Part 2", + "Wild Target", + "Adam's Apples", + "Inside Out", + "Pride", + "The Grinch", + "Airplane II: The Sequel", + "The Fundamentals of Caring", + "The Tournament", + "Crimes and Misdemeanors", + "Stan & Ollie", + "The Big Year", + "Message in a Bottle", + "Kabhi Khushi Kabhie Gham", + "Rent", + "Naked Lunch", + "Without a Paddle", + "American Heist", + "True Story", + "The Jerk", + "Ratatouille", + "Zelig", + "The 51st State", + "The Karate Kid, Part III", + "Martha Marcy May Marlene", + "Moonstruck", + "Deuce Bigalow: European Gigolo", + "Raiders of the Lost Ark", + "Inspector Gadget", + "Blow", + "The Roommate", + "Smokey and the Bandit", + "Get Rich or Die Tryin'", + "Shutter Island", + "Unplanned", + "Tae Guk Gi: The Brotherhood of War", + "Invasion of the Body Snatchers", + "Teenage Mutant Ninja Turtles", + "Nim's Island", + "Aguirre: The Wrath of God", + "Dave", + "The Adventures of Robin Hood", + "Romper Stomper", + "The Producers", + "Ocean's Eight", + "Miracle", + "The Sentinel", + "Easy Virtue", + "Homeward Bound: The Incredible Journey", + "Boy Erased", + "Top Secret!", + "Airheads", + "Open Water", + "Prime", + "Harvey", + "THX 1138", + "Saved!", + "Lilya 4-ever", + "The Matrix Reloaded", + "In a Better World", + "Despite Everything", + "The Hobbit: The Desolation of Smaug", + "A Vigilante", + "Il grande spirito", + "The 39 Steps", + "The Battle of Algiers", + "The Cabinet of Dr. Caligari", + "Appaloosa", + "Re-Animator", + "The Passion of Joan of Arc", + "In the Loop", + "Bird Box", + "Serenity", + "The Golden Child", + "The Wrong Trousers", + "A Regular Woman", + "Men in Black 3", + "Mission: Impossible III", + "Fanboys", + "Welcome to Marwen", + "Stop! Or My Mom Will Shoot", + "Pariban : Idola Dari Tanah Jawa", + "The New Guy", + "Black Knight", + "House on Haunted Hill", + "Traitor", + "What Ever Happened to Baby Jane?", + "The Best Years of Our Lives", + "The Brothers Bloom", + "Black Rain", + "Fanny & Alexander", + "Perfect Blue", + "The Con Is On", + "Night at the Museum: Secret of the Tomb", + "The Mask", + "Ip Man 4", + "Men in Black", + "Float Like A Butterfly", + "The Peacemaker", + "WALL·E", + "Inglourious Basterds", + "Arthur", + "Coneheads", + "The Terminator", + "The Kid", + "Dumb and Dumberer: When Harry Met Lloyd", + "The First Wives Club", + "Holy Motors", + "Highlander 2: The Quickening", + "Hacksaw Ridge", + "Halloween", + "Pirates of the Caribbean: At World's End", + "Kleine Germanen", + "Morning Has Broken", + "High Crimes", + "Joe Dirt", + "Monsters, Inc.", + "Pirates of the Caribbean: On Stranger Tides", + "This Boy’s Life", + "Just Say Goodbye", + "Coriolanus", + "The American President", + "Buffalo '66", + "Meet Dave", + "Gomorrah", + "The Matador", + "Defendor", + "Evil", + "Would You Rather", + "The Man Who Would Be King", + "The Darkest Minds", + "A Quiet Place", + "Love and Death", + "The Nutcracker and the Four Realms", + "Dr. No", + "A River Runs Through It", + "One Fine Day", + "Edtv", + "Something Something... Unakkum Enakkum", + "Aliens", + "The Last Temptation of Christ", + "Tangerines", + "Pacific Rim: Uprising", + "Sunrise: A Song of Two Humans", + "Fair Game", + "Red Dawn", + "Laws of Attraction", + "Honey I Blew Up the Kid", + "The Rocker", + "Sleuth", + "Stuck on You", + "Flight of the Navigator", + "Dangerous Minds", + "Behind the Candelabra", + "A Clockwork Orange", + "The Messengers", + "Straw Dogs", + "City Slickers", + "Fantastic Beasts and Where to Find Them", + "Leaving Afghanistan", + "The Lady Vanishes", + "Kuch Kuch Hota Hai", + "American Splendor", + "A Hard Day's Night", + "Pixels", + "Days of Heaven", + "The Big Blue", + "Piranha 3DD", + "Men in Black II", + "Kalifornia", + "Bull Durham", + "Waitress", + "Madagascar", + "Grease", + "Teen Wolf", + "The Mighty Ducks", + "Insurgent", + "Gridiron Gang", + "Rab Ne Bana Di Jodi", + "Rabbit Hole", + "Stalag 17", + "Hatsukoi: Otosan, Chibi ga Inaku Narimashita", + "Your Name.", + "Sex Tape", + "Sodemacom Killer", + "Kong: Skull Island", + "Jab Tak Hai Jaan", + "My Sassy Girl", + "Hereditary", + "Sleeping Beauty", + "Food, Inc.", + "The Head Hunter", + "The Killer Inside Me", + "Spirited Away", + "Maze Runner: The Death Cure", + "The Hateful Eight", + "Down with Love", + "Cube Zero", + "The Adventures of Baron Munchausen", + "The Hitcher", + "Alien", + "Aladdin", + "Red Sparrow", + "Weekend at Bernie's", + "Solo cose belle", + "How High", + "Skyscraper", + "Den of Thieves", + "Michael", + "Last Man Standing", + "National Security", + "Saving Private Ryan", + "The Duelist", + "Forever Young", + "Proof", + "Alpha", + "Fright Night", + "Priceless", + "Children of Heaven", + "Christiane F.", + "Lions for Lambs", + "Dick Tracy", + "The Devil Inside", + "All About Steve", + "Un Chien Andalou", + "Gigli", + "Knock on Wood", + "Nights of Cabiria", + "Dead Man's Shoes", + "Singel 39", + "The Devil Wears Prada", + "Menace II Society", + "GoodFellas", + "A Dog's Way Home", + "Glory Road", + "Heartbeats", + "Swimming Pool", + "Man on Wire", + "Bruce Almighty", + "Birdman", + "Hellboy", + "Mathilde", + "Passenger 57", + "Daymohk", + "I Killed My Mother", + "Nutty Professor II: The Klumps", + "Fired Up!", + "Rock Star", + "Notorious", + "Romy and Michele's High School Reunion", + "Stripes", + "Proof of Life", + "Rachel Getting Married", + "Liberal Arts", + "The Counterfeiters", + "Burnt by the Sun", + "The Matrix Revolutions", + "The Benchwarmers", + "The Departed", + "Choke", + "Three Billboards Outside Ebbing, Missouri", + "Radio", + "Upgrade", + "Great Expectations", + "Valerian and the City of a Thousand Planets", + "I Don't Feel at Home in This World Anymore", + "Low Budget (B.O.)", + "Isn't It Romantic", + "Stay Alive", + "Major League", + "To Paris!", + "Children of the Corn", + "Soul Kitchen", + "Head-On", + "Butterfly on a Wheel", + "Ghost in the Shell 2: Innocence", + "Suicide Squad", + "The Lookout", + "The Company Men", + "Win Win", + "When a Stranger Calls", + "Psycho", + "Curly Sue", + "Deconstructing Harry", + "Withnail & I", + "Coco", + "Shrek", + "Ghost in the Shell", + "Enterrados", + "Where the Heart Is", + "Tell It to the Bees", + "Broken Embraces", + "Revenge of the Nerds", + "The Ice Storm", + "Rampage", + "Gladiator", + "Soul Surfer", + "My Sassy Girl", + "Cleaner", + "Nine Queens", + "First Blood", + "After Hours", + "The Ward", + "Keeping the Faith", + "Skyfall", + "Mission: Impossible", + "Bobby", + "Lumpkin, GA", + "The Caring City", + "The Love Guru", + "Fantastic Four: Rise of the Silver Surfer", + "Juliet, Naked", + "Blackhat", + "Fear", + "You Will Meet a Tall Dark Stranger", + "Thirteen Days", + "The Mummy", + "Georgia Rule", + "The Purple Rose of Cairo", + "Jack", + "Taxi Driver", + "The Bandit", + "Lakeview Terrace", + "The Boy Next Door", + "The River Wild", + "Flood" +] diff --git a/display_results.ipynb b/display_results.ipynb index fbe5cf4..f47e1d5 100644 --- a/display_results.ipynb +++ b/display_results.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 77, + "execution_count": 45, "metadata": {}, "outputs": [ { @@ -10,101 +10,33 @@ "output_type": "stream", "text": [ " event_id sent \\\n", - "0 701 Event(target=OpNode(User, InvokeMethod('order_... \n", - "1 702 Event(target=OpNode(User, InvokeMethod('order_... \n", - "2 703 Event(target=OpNode(User, InvokeMethod('order_... \n", - "3 704 Event(target=OpNode(User, InvokeMethod('order_... \n", - "4 705 Event(target=OpNode(User, InvokeMethod('order_... \n", + "0 701 Event(target=OpNode(User, InvokeMethod('login'... \n", + "1 702 Event(target=OpNode(User, InvokeMethod('login'... \n", + "2 703 Event(target=OpNode(User, InvokeMethod('login'... \n", + "3 704 Event(target=OpNode(User, InvokeMethod('login'... \n", + "4 705 Event(target=OpNode(User, InvokeMethod('login'... \n", "\n", " sent_t ret \\\n", - "0 (2, 1738342392523) EventResult(event_id=701, result=True, metadat... \n", - "1 (2, 1738342392523) EventResult(event_id=702, result=True, metadat... \n", - "2 (2, 1738342392523) EventResult(event_id=703, result=True, metadat... \n", - "3 (2, 1738342392523) EventResult(event_id=704, result=True, metadat... \n", - "4 (2, 1738342392523) EventResult(event_id=705, result=True, metadat... \n", + "0 (2, 1739358574443) EventResult(event_id=701, result=True, metadat... \n", + "1 (2, 1739358574443) EventResult(event_id=702, result=True, metadat... \n", + "2 (2, 1739358574443) EventResult(event_id=703, result=True, metadat... \n", + "3 (2, 1739358574443) EventResult(event_id=704, result=True, metadat... \n", + "4 (2, 1739358574443) EventResult(event_id=705, result=True, metadat... \n", "\n", - " ret_t roundtrip flink_time \\\n", - "0 (2, 1738342395748) 3.218930 1.794561 \n", - "1 (2, 1738342394543) 2.013558 1.205442 \n", - "2 (2, 1738342395450) 2.919337 1.445957 \n", - "3 (2, 1738342395450) 2.919065 1.537150 \n", - "4 (2, 1738342395650) 3.012420 1.574120 \n", + " ret_t roundtrip flink_time deser_times loops \\\n", + "0 (2, 1739358574525) 0.075989 0.075989 [5.435943603515625e-05] 1 \n", + "1 (2, 1739358574503) 0.053916 0.053916 [3.266334533691406e-05] 1 \n", + "2 (2, 1739358574563) 0.113311 0.113311 [2.6702880859375e-05] 1 \n", + "3 (2, 1739358574563) 0.113266 0.113266 [2.0503997802734375e-05] 1 \n", + "4 (2, 1739358574563) 0.113229 0.113229 [1.5020370483398438e-05] 1 \n", "\n", - " deser_times loops latency \n", - "0 [0.00010704994201660156, 3.4332275390625e-05, ... 5 3225 \n", - "1 [3.7670135498046875e-05, 6.389617919921875e-05... 5 2020 \n", - "2 [2.956390380859375e-05, 3.0994415283203125e-05... 5 2927 \n", - "3 [2.6941299438476562e-05, 5.507469177246094e-05... 5 2927 \n", - "4 [2.6941299438476562e-05, 2.7418136596679688e-0... 5 3127 \n" + " latency \n", + "0 82 \n", + "1 60 \n", + "2 120 \n", + "3 120 \n", + "4 120 \n" ] - } - ], - "source": [ - "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", - "\n", - "# Read the CSV file\n", - "df = pd.read_pickle('test2.pkl')\n", - "\n", - "# Display the first few rows of the dataframe to understand its structure\n", - "print(df.head())" - ] - }, - { - "cell_type": "code", - "execution_count": 78, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - " event_id sent \\\n", - "0 701 Event(target=OpNode(User, InvokeMethod('order_... \n", - "1 702 Event(target=OpNode(User, InvokeMethod('order_... \n", - "2 703 Event(target=OpNode(User, InvokeMethod('order_... \n", - "3 704 Event(target=OpNode(User, InvokeMethod('order_... \n", - "4 705 Event(target=OpNode(User, InvokeMethod('order_... \n", - "\n", - " sent_t ret \\\n", - "0 (2, 1738342392523) EventResult(event_id=701, result=True, metadat... \n", - "1 (2, 1738342392523) EventResult(event_id=702, result=True, metadat... \n", - "2 (2, 1738342392523) EventResult(event_id=703, result=True, metadat... \n", - "3 (2, 1738342392523) EventResult(event_id=704, result=True, metadat... \n", - "4 (2, 1738342392523) EventResult(event_id=705, result=True, metadat... \n", - "\n", - " ret_t roundtrip flink_time \\\n", - "0 (2, 1738342395748) 3.218930 1.794561 \n", - "1 (2, 1738342394543) 2.013558 1.205442 \n", - "2 (2, 1738342395450) 2.919337 1.445957 \n", - "3 (2, 1738342395450) 2.919065 1.537150 \n", - "4 (2, 1738342395650) 3.012420 1.574120 \n", - "\n", - " deser_times loops latency \\\n", - "0 [0.00010704994201660156, 3.4332275390625e-05, ... 5 3225 \n", - "1 [3.7670135498046875e-05, 6.389617919921875e-05... 5 2020 \n", - "2 [2.956390380859375e-05, 3.0994415283203125e-05... 5 2927 \n", - "3 [2.6941299438476562e-05, 5.507469177246094e-05... 5 2927 \n", - "4 [2.6941299438476562e-05, 2.7418136596679688e-0... 5 3127 \n", - "\n", - " flink_time_ms deser_times_ms \n", - "0 1794.560671 0.334024 \n", - "1 1205.441713 0.262737 \n", - "2 1445.957422 0.235319 \n", - "3 1537.150145 0.244617 \n", - "4 1574.119806 0.237703 \n" - ] - }, - { - "data": { - "text/plain": [ - "Text(0, 0.5, 'latency (ms)')" - ] - }, - "execution_count": 78, - "metadata": {}, - "output_type": "execute_result" }, { "data": { @@ -112,12 +44,12 @@ "\n", "\n", - "\n", + "\n", " \n", " \n", " \n", " \n", - " 2025-01-31T17:55:19.053555\n", + " 2025-02-12T12:21:17.409504\n", " image/svg+xml\n", " \n", " \n", @@ -132,19 +64,19 @@ " \n", " \n", " \n", - " \n", " \n", " \n", " \n", - " \n", " \n", @@ -152,17 +84,65 @@ " \n", " \n", " \n", - " \n", " \n", " \n", - " \n", + " \n", " \n", " \n", " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", - " \n", + " \n", " \n", " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", - " \n", + " \n", " \n", " \n", " \n", " \n", " \n", - " \n", - " \n", + " \n", + " \n", " \n", - " \n", + " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", + " \n", " \n", " \n", " \n", " \n", - " \n", - " \n", + " \n", + " \n", " \n", - " \n", + " \n", " \n", " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", - " \n", + " \n", " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", " \n", - " \n", " \n", - " \n", - " \n", + " \n", - " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", - " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", + " \n", + " \n", " \n", " \n", " \n", " \n", - " \n", - " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", " \n", - " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", " \n", - " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", " \n", - " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", " \n", " \n", " \n", - " \n", + " \n", " \n", " \n", - " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", - " \n", " \n", - " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", " \n", - " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", " \n", " \n", - " \n", " \n", " \n", - " \n", " \n", " \n", - " \n", " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", " \n", - " \n", - " \n", + " \n", " \n", - " \n", + " \n", " \n", - " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", @@ -2756,15 +1793,15 @@ " \n", " \n", " \n", - " \n", - " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", " \n", " \n", + " \n", " \n", + " \n", " \n", " \n", " \n", @@ -2812,61 +1869,14 @@ " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", " \n", " \n", " \n", " \n", " \n", " \n", - " \n", - " \n", + " \n", + " \n", " \n", " \n", "\n" @@ -2880,13 +1890,1092 @@ } ], "source": [ - "df['flink_time_ms'] = df['flink_time'] * 1000\n", - "df[\"deser_times_ms\"] = df[\"deser_times\"].apply(lambda x: sum(x)) * 1000\n", - "print(type(df['deser_times']))\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# Read the CSV file\n", + "df = pd.read_pickle('test2.pkl')\n", + "\n", + "# Display the first few rows of the dataframe to understand its structure\n", "print(df.head())\n", - "df.plot(x='event_id', y=['latency', 'flink_time_ms', 'deser_times_ms'], kind='line')\n", - "plt.ylim(bottom=0)\n", - "plt.ylabel('latency (ms)')" + "\n", + "df['flink_time'] = df['flink_time'] * 1000\n", + "df.plot(x='event_id', y=['latency', 'flink_time'], kind='line')\n", + "plt.xlabel('Event ID')\n", + "plt.ylabel('Latency (ms)')\n", + "# plt.yscale('log')\n", + "plt.title('Latency per Event')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2025-02-11T16:39:28.082024\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.9.0, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def preprocess_data(pickle_file_path):\n", + " # Read the DataFrame from the pickle file\n", + " df = pd.read_pickle(pickle_file_path)\n", + "\n", + " # Multiply flink_time by 1000 to convert to milliseconds\n", + " df['flink_time'] = df['flink_time'] * 1000\n", + "\n", + " # Calculate the additional Kafka overhead\n", + " df['kafka_overhead'] = df['latency'] - df['flink_time']\n", + "\n", + " # Extract median values from df\n", + " flink_time_median = df['flink_time'].median()\n", + " latency_median = df['latency'].median()\n", + "\n", + " return {\n", + " 'flink_time_median': flink_time_median,\n", + " 'kafka_overhead_median': latency_median - flink_time_median\n", + " }\n", + "\n", + "# List of pickle files\n", + "titles = ['baseline', 'pipelined', 'parallel*']\n", + "pickle_files = ['compose_basic_1mps.pkl', 'compose_pipeline_1mps.pkl', 'compose_parallel_1mps.pkl']\n", + "\n", + "titles = ['baseline', 'parallel*']\n", + "pickle_files = ['reserve_serial_1mps.pkl', 'reserve_parallel.pkl']\n", + "\n", + "# Process each file and collect median data\n", + "all_median_data = []\n", + "for title, file in zip(titles, pickle_files):\n", + " median_values = preprocess_data(file)\n", + " all_median_data.append({\n", + " 'Metric': 'Flink Time',\n", + " 'Value': median_values['flink_time_median'],\n", + " 'Title': title\n", + " })\n", + " all_median_data.append({\n", + " 'Metric': 'Kafka Overhead',\n", + " 'Value': median_values['kafka_overhead_median'],\n", + " 'Title': title\n", + " })\n", + "\n", + "# Create a DataFrame for all median values\n", + "all_median_df = pd.DataFrame(all_median_data)\n", + "\n", + "# Sort titles based on 'Flink Time' in descending order\n", + "sorted_titles = (\n", + " all_median_df[all_median_df['Metric'] == 'Flink Time']\n", + " .sort_values(by='Value', ascending=False)['Title']\n", + ")\n", + "\n", + "# Pivot the DataFrame and reindex to maintain sorted order\n", + "pivot_df = (\n", + " all_median_df.pivot(index='Title', columns='Metric', values='Value')\n", + " .reindex(sorted_titles)\n", + ")\n", + "\n", + "# Plot\n", + "fig, ax = plt.subplots()\n", + "pivot_df.plot(kind='bar', stacked=True, color=['#1f77b4', '#ff7f0e'], ax=ax)\n", + "plt.ylabel('Time (ms)')\n", + "plt.title('Median Flink Time and Kafka Overhead')\n", + "plt.legend(['Flink Time', 'Kafka Overhead'])\n", + "plt.show()\n" ] } ], diff --git a/login_10mps.png b/login_10mps.png deleted file mode 100644 index 924958a209c412d86ec003ee8514cdd3052bf670..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61636 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
TD7YX^$@kK6xje9z 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 VY3Nj5?gasV7N%C`DvjOm{txQDTrL0r diff --git a/reserve_10mps.png b/reserve_10mps.png deleted file mode 100644 index 20fdacfbc3e84f49816690055b9c53379034f66f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36356 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 Qf&mCTUHx3vIVCg!04XBsD*ylh diff --git a/reserve_10mps_parallel.png b/reserve_10mps_parallel.png deleted file mode 100644 index 7d22c450d688d7f7f2bece2a7dfc365be690c552..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34048 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 diff --git a/reserve_1mps.png b/reserve_1mps.png deleted file mode 100644 index 85ab9a5e840c875f740faad3710aa35e4e6a7c1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14193 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 R;4?YUc|(hHWd_&o|1UU@@@fD8 diff --git a/reserve_1mps_parallel.png b/reserve_1mps_parallel.png deleted file mode 100644 index d111ec8adc5571ad410fccc5e5fcb2a1515d70b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28572 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%0A**R^|Cj2zM?|tOZnVS}y&8 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 list['E return OpNode.propogate_opnode(self, event, targets, result) @staticmethod - def propogate_opnode(node: Union['OpNode', 'StatelessOpNode'], event: 'Event', targets: list[Node], result: Any) -> list['Event']: + def propogate_opnode(node: Union['OpNode', 'StatelessOpNode'], event: 'Event', targets: list[Node], + result: Any) -> list['Event']: + num_targets = 1 if node.is_conditional else len(targets) + if event.collect_target is not None: # Assign new collect targets collect_targets = [ - event.collect_target for i in range(len(targets)) + event.collect_target for i in range(num_targets) ] else: # Keep old collect targets - collect_targets = [node.collect_target for i in range(len(targets))] + collect_targets = [node.collect_target for i in range(num_targets)] if node.is_conditional: edges = event.dataflow.nodes[event.target.id].outgoing_edges @@ -90,7 +93,9 @@ def propogate_opnode(node: Union['OpNode', 'StatelessOpNode'], event: 'Event', t target_true = true_edges[0].to_node target_false = false_edges[0].to_node - + assert len(collect_targets) == 1, "num targets should be 1" + ct = collect_targets[0] + return [Event( target_true if result else target_false, event.variable_map, @@ -98,8 +103,8 @@ def propogate_opnode(node: Union['OpNode', 'StatelessOpNode'], event: 'Event', t _id=event._id, collect_target=ct, metadata=event.metadata) + ] - for ct in collect_targets] else: return [Event( target, @@ -133,6 +138,26 @@ class StatelessOpNode(Node): def propogate(self, event: 'Event', targets: List[Node], result: Any) -> List['Event']: return OpNode.propogate_opnode(self, event, targets, result) + +@dataclass +class DataflowNode(Node): + """A node in a `DataFlow` corresponding to the call of another dataflow""" + dataflow: 'DataFlow' + variable_rename: dict[str, str] + + assign_result_to: Optional[str] = None + """What variable to assign the result of this node to, if any.""" + is_conditional: bool = False + """Whether or not the boolean result of this node dictates the following path.""" + collect_target: Optional['CollectTarget'] = None + """Whether the result of this node should go to a CollectNode.""" + + def propogate(self, event: 'Event', targets: List[Node], result: Any) -> List['Event']: + # remap the variable map of event into the new event + + # add the targets as some sort of dataflow "exit nodes" + return self.dataflow + @dataclass class SelectAllNode(Node): @@ -207,35 +232,19 @@ class DataFlow: item1[Item.get_price] item2[Item.get_price] user2[User.buy_items_1] - merge{Merge} + collect{Collect} user1-- item1_key -->item1; user1-- item2_key -->item2; - item1-- item1_price -->merge; - item2-- item2_price -->merge; - merge-- [item1_price, item2_price] -->user2; - ``` - - In code, one would write: - - ```py - df = DataFlow("user.buy_items") - n0 = OpNode(User, InvokeMethod("buy_items_0")) - n1 = OpNode(Item, InvokeMethod("get_price")) - n2 = OpNode(Item, InvokeMethod("get_price")) - n3 = MergeNode() - n4 = OpNode(User, InvokeMethod("buy_items_1")) - df.add_edge(Edge(n0, n1)) - df.add_edge(Edge(n0, n2)) - df.add_edge(Edge(n1, n3)) - df.add_edge(Edge(n2, n3)) - df.add_edge(Edge(n3, n4)) + item1-- item1_price -->collect; + item2-- item2_price -->collect; + collect-- [item1_price, item2_price] -->user2; ``` """ def __init__(self, name: str): self.name: str = name self.adjacency_list: dict[int, list[int]] = {} self.nodes: dict[int, Node] = {} - self.entry: Node = None + self.entry: Union[Node, List[Node]] = None def add_node(self, node: Node): """Add a node to the Dataflow graph if it doesn't already exist.""" @@ -278,6 +287,12 @@ def remove_node(self, node: Node): # Find children (nodes that this node points to) children = self.adjacency_list[node.id] + + # Set df entry + if self.entry == node: + print(children) + assert len(children) == 1, "cannot remove entry node if it doesn't exactly one child" + self.entry = self.nodes[children[0]] # Connect each parent to each child for parent_id in parents: @@ -296,6 +311,8 @@ def remove_node(self, node: Node): for child_id in children: child_node = self.nodes[child_id] self.remove_edge(node, child_node) + + # Remove the node from the adjacency list and nodes dictionary del self.adjacency_list[node.id] @@ -322,17 +339,15 @@ def to_dot(self) -> str: lines.append("}") return "\n".join(lines) -class Result(ABC): - pass - -@dataclass -class Arrived(Result): - val: Any - -@dataclass -class NotArrived(Result): - pass - + def generate_event(self, variable_map: dict[str, Any]) -> Union['Event', list['Event']]: + if isinstance(self.entry, list): + assert len(self.entry) != 0 + first_event = Event(self.entry[0], variable_map, self) + id = first_event._id + return [first_event] + [Event(entry, variable_map, self, _id=id) for entry in self.entry[1:]] + else: + return Event(self.entry, variable_map, self) + @dataclass class CollectTarget: target_node: CollectNode diff --git a/src/cascade/dataflow/optimization/dead_node_elim.py b/src/cascade/dataflow/optimization/dead_node_elim.py index a62ac37..d1a9d06 100644 --- a/src/cascade/dataflow/optimization/dead_node_elim.py +++ b/src/cascade/dataflow/optimization/dead_node_elim.py @@ -22,9 +22,9 @@ def dead_node_elimination(stateful_ops: list[StatefulOperator], stateless_ops: l # Find dead functions dead_func_names = set() for op in stateful_ops: - for method in op._methods.values(): + for name, method in op._methods.items(): if is_no_op(method): - dead_func_names.add(method.__qualname__) + dead_func_names.add(name) # Remove them from dataflows for op in stateful_ops: @@ -41,4 +41,23 @@ def dead_node_elimination(stateful_ops: list[StatefulOperator], stateless_ops: l dataflow.remove_node(node) print(dataflow.to_dot()) + # Find dead functions + dead_func_names = set() + for op in stateless_ops: + for name, method in op._methods.items(): + if is_no_op(method): + dead_func_names.add(name) + + # Remove them from dataflows + for op in stateless_ops: + to_remove = [] + for node in op.dataflow.nodes.values(): + if hasattr(node, "method_type") and isinstance(node.method_type, InvokeMethod): + im: InvokeMethod = node.method_type + if im.method_name in dead_func_names: + to_remove.append(node) + + for node in to_remove: + op.dataflow.remove_node(node) + diff --git a/src/cascade/runtime/flink_runtime.py b/src/cascade/runtime/flink_runtime.py index e0f14c0..febfc83 100644 --- a/src/cascade/runtime/flink_runtime.py +++ b/src/cascade/runtime/flink_runtime.py @@ -1,3 +1,4 @@ +from abc import ABC from dataclasses import dataclass import os import time @@ -12,7 +13,7 @@ from pyflink.datastream.connectors.kafka import KafkaOffsetsInitializer, KafkaRecordSerializationSchema, KafkaSource, KafkaSink from pyflink.datastream import ProcessFunction, StreamExecutionEnvironment import pickle -from cascade.dataflow.dataflow import Arrived, CollectNode, CollectTarget, Event, EventResult, Filter, InitClass, InvokeMethod, Node, NotArrived, OpNode, Result, SelectAllNode, StatelessOpNode +from cascade.dataflow.dataflow import CollectNode, CollectTarget, Event, EventResult, Filter, InitClass, InvokeMethod, Node, OpNode, SelectAllNode, StatelessOpNode from cascade.dataflow.operator import StatefulOperator, StatelessOperator from confluent_kafka import Producer, Consumer import logging @@ -56,41 +57,47 @@ def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): # should be handled by filters on this FlinkOperator assert(isinstance(event.target, OpNode)) + logger.debug(f"FlinkOperator {self.operator.entity.__name__}[{ctx.get_current_key()}]: Processing: {event.target.method_type}") + assert(event.target.entity == self.operator.entity) key = ctx.get_current_key() assert(key is not None) - logger.debug(f"FlinkOperator {self.operator.entity.__name__}[{ctx.get_current_key()}]: Processing: {event.target.method_type}") if isinstance(event.target.method_type, InitClass): # TODO: compile __init__ with only kwargs, and pass the variable_map itself # otherwise, order of variable_map matters for variable assignment result = self.operator.handle_init_class(*event.variable_map.values()) # Register the created key in FlinkSelectAllOperator - register_key_event = Event( - FlinkRegisterKeyNode(key, self.operator.entity), - {}, - None, - _id = event._id - ) - logger.debug(f"FlinkOperator {self.operator.entity.__name__}[{ctx.get_current_key()}]: Registering key: {register_key_event}") - yield register_key_event + # register_key_event = Event( + # FlinkRegisterKeyNode(key, self.operator.entity), + # {}, + # None, + # _id = event._id + # ) + # logger.debug(f"FlinkOperator {self.operator.entity.__name__}[{ctx.get_current_key()}]: Registering key: {register_key_event}") + # yield register_key_event - # Pop this key from the key stack so that we exit self.state.update(pickle.dumps(result)) elif isinstance(event.target.method_type, InvokeMethod): - state = pickle.loads(self.state.value()) + state = self.state.value() + if state is None: + # try to create the state if we haven't been init'ed + state = self.operator.handle_init_class(*event.variable_map.values()) + else: + state = pickle.loads(state) + result = self.operator.handle_invoke_method(event.target.method_type, variable_map=event.variable_map, state=state) # TODO: check if state actually needs to be updated if state is not None: self.state.update(pickle.dumps(state)) - elif isinstance(event.target.method_type, Filter): - state = pickle.loads(self.state.value()) - result = event.target.method_type.filter_fn(event.variable_map, state) - if not result: - return - result = event.key_stack[-1] + # elif isinstance(event.target.method_type, Filter): + # state = pickle.loads(self.state.value()) + # result = event.target.method_type.filter_fn(event.variable_map, state) + # if not result: + # return + # result = event.key_stack[-1] if event.target.assign_result_to is not None: event.variable_map[event.target.assign_result_to] = result @@ -170,6 +177,21 @@ def process_element(self, event: Event, ctx: 'ProcessFunction.Context'): else: raise Exception(f"Unexpected target for SelectAllOperator: {event.target}") +class Result(ABC): + """A `Result` can be either `Arrived` or `NotArrived`. It is used in the + FlinkCollectOperator to determine whether all the events have completed + their computation.""" + pass + +@dataclass +class Arrived(Result): + val: Any + +@dataclass +class NotArrived(Result): + pass + + class FlinkCollectOperator(KeyedProcessFunction): """Flink implementation of a merge operator.""" def __init__(self): @@ -219,28 +241,6 @@ def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): logger.debug(f"FlinkCollectOp [{ctx.get_current_key()}]: Propogated {len(new_events)} new Events") yield from new_events -class FlinkMergeOperator(KeyedProcessFunction): - """Flink implementation of a merge operator.""" - def __init__(self) -> None: - self.other: ValueState = None # type: ignore (expect state to be initialised on .open()) - - def open(self, runtime_context: RuntimeContext): - descriptor = ValueStateDescriptor("merge_state", Types.PICKLED_BYTE_ARRAY()) - self.other = runtime_context.get_state(descriptor) - - def process_element(self, event: Event, ctx: KeyedProcessFunction.Context): - other_map = self.other.value() - logger.debug(f"FlinkMergeOp [{ctx.get_current_key()}]: Processing: {event}") - if other_map == None: - logger.debug(f"FlinkMergeOp [{ctx.get_current_key()}]: Saving variable map") - self.other.update(event.variable_map) - else: - self.other.clear() - logger.debug(f"FlinkMergeOp [{ctx.get_current_key()}]: Yielding merged variables") - event.variable_map |= other_map - new_event = event.propogate(event.key_stack, None) - yield from new_event - class ByteSerializer(SerializationSchema, DeserializationSchema): """A custom serializer which maps bytes to bytes. @@ -311,6 +311,10 @@ def timestamp_result(e: EventResult) -> EventResult: e.metadata["roundtrip"] = e.metadata["out_t"] - e.metadata["in_t"] return e +def debug(x, msg=""): + logger.debug(msg) + return x + class FlinkRuntime(): """A Runtime that runs Dataflows on Flink.""" def __init__(self, input_topic="input-topic", output_topic="output-topic", ui_port: Optional[int] = None): @@ -363,6 +367,10 @@ def init(self, kafka_broker="localhost:9092", bundle_time=1, bundle_size=5, para config.set_integer("python.fn-execution.bundle.time", bundle_time) config.set_integer("python.fn-execution.bundle.size", bundle_size) + # optimize for low latency + config.set_integer("taskmanager.memory.managed.size", 0) + config.set_integer("execution.buffer-timeout", 0) + self.env = StreamExecutionEnvironment.get_execution_environment(config) if parallelism: self.env.set_parallelism(parallelism) @@ -428,10 +436,12 @@ def init(self, kafka_broker="localhost:9092", bundle_time=1, bundle_size=5, para "Kafka Source" ) .map(lambda x: deserialize_and_timestamp(x)) + # .map(lambda x: debug(x, msg=f"entry: {x}")) .name("DESERIALIZE") # .filter(lambda e: isinstance(e, Event)) # Enforced by `send` type safety ) - + + """REMOVE SELECT ALL NODES # Events with a `SelectAllNode` will first be processed by the select # all operator, which will send out multiple other Events that can # then be processed by operators in the same steam. @@ -441,32 +451,26 @@ def init(self, kafka_broker="localhost:9092", bundle_time=1, bundle_size=5, para .key_by(lambda e: e.target.cls) .process(FlinkSelectAllOperator()).name("SELECT ALL OP") ) - """Stream that ingests events with an `SelectAllNode` or `FlinkRegisterKeyNode`""" + # Stream that ingests events with an `SelectAllNode` or `FlinkRegisterKeyNode` not_select_all_stream = ( event_stream.filter(lambda e: not (isinstance(e.target, SelectAllNode) or isinstance(e.target, FlinkRegisterKeyNode))) ) operator_stream = select_all_stream.union(not_select_all_stream) + """ + self.stateful_op_stream = event_stream + self.stateless_op_stream = event_stream - self.stateful_op_stream = ( - operator_stream - .filter(lambda e: isinstance(e.target, OpNode)) - ) - - self.stateless_op_stream = ( - operator_stream - .filter(lambda e: isinstance(e.target, StatelessOpNode)) - ) - - self.merge_op_stream = ( - event_stream.filter(lambda e: isinstance(e.target, CollectNode)) - .key_by(lambda e: e._id) # might not work in the future if we have multiple merges in one dataflow? - .process(FlinkCollectOperator()) - .name("Collect") - ) - """Stream that ingests events with an `cascade.dataflow.dataflow.CollectNode` target""" + # MOVED TO END OF OP STREAMS! + # self.merge_op_stream = ( + # event_stream.filter(lambda e: isinstance(e.target, CollectNode)) + # .key_by(lambda e: e._id) # might not work in the future if we have multiple merges in one dataflow? + # .process(FlinkCollectOperator()) + # .name("Collect") + # ) + # """Stream that ingests events with an `cascade.dataflow.dataflow.CollectNode` target""" self.stateless_op_streams = [] self.stateful_op_streams = [] @@ -480,9 +484,11 @@ def add_operator(self, op: StatefulOperator): flink_op = FlinkOperator(op) op_stream = ( - self.stateful_op_stream.filter(lambda e: e.target.entity == flink_op.operator.entity) + self.stateful_op_stream.filter(lambda e: isinstance(e.target, OpNode) and e.target.entity == flink_op.operator.entity) + # .map(lambda x: debug(x, msg=f"filtered op: {op.entity}")) .key_by(lambda e: e.variable_map[e.target.read_key_from]) .process(flink_op) + # .map(lambda x: debug(x, msg=f"processed op: {op.entity}")) .name("STATEFUL OP: " + flink_op.operator.entity.__name__) ) self.stateful_op_streams.append(op_stream) @@ -493,7 +499,7 @@ def add_stateless_operator(self, op: StatelessOperator): op_stream = ( self.stateless_op_stream - .filter(lambda e: e.target.operator.dataflow.name == flink_op.operator.dataflow.name) + .filter(lambda e: isinstance(e.target, StatelessOpNode) and e.target.operator.dataflow.name == flink_op.operator.dataflow.name) .process(flink_op) .name("STATELESS DATAFLOW: " + flink_op.operator.dataflow.name) ) @@ -520,8 +526,21 @@ def run(self, run_async=False, output: Literal["collect", "kafka", "stdout"]="ka logger.debug("FlinkRuntime merging operator streams...") # Combine all the operator streams - operator_streams = self.merge_op_stream.union(*self.stateful_op_streams).union(*self.stateless_op_streams) + # operator_streams = self.merge_op_stream.union(*self.stateful_op_streams[1:], *self.stateless_op_streams)#.map(lambda x: debug(x, msg="combined ops")) + s1 = self.stateful_op_streams[0] + rest = self.stateful_op_streams[1:] + operator_streams = s1.union(*rest, *self.stateless_op_streams)#.map(lambda x: debug(x, msg="combined ops")) + + merge_op_stream = ( + operator_streams.filter(lambda e: isinstance(e, Event) and isinstance(e.target, CollectNode)) + .key_by(lambda e: e._id) # might not work in the future if we have multiple merges in one dataflow? + .process(FlinkCollectOperator()) + .name("Collect") + ) + """Stream that ingests events with an `cascade.dataflow.dataflow.CollectNode` target""" + + """ # Add filtering for nodes with a `Filter` target full_stream_filtered = ( operator_streams @@ -533,6 +552,9 @@ def run(self, run_async=False, output: Literal["collect", "kafka", "stdout"]="ka .filter(lambda e: not (isinstance(e, Event) and isinstance(e.target, Filter))) ) ds = full_stream_filtered.union(full_stream_unfiltered) + """ + # union with EventResults or Events that don't have a CollectNode target + ds = merge_op_stream.union(operator_streams.filter(lambda e: not (isinstance(e, Event) and isinstance(e.target, CollectNode)))) # Output the stream results = ( @@ -613,7 +635,20 @@ def consume_results(self): def flush(self): self.producer.flush() - def send(self, event: Event, flush=False) -> int: + def send(self, event: Union[Event, list[Event]], flush=False) -> int: + if isinstance(event, list): + for e in event: + id = self._send(e) + else: + id = self._send(event) + + if flush: + self.producer.flush() + + return id + + + def _send(self, event: Event) -> int: """Send an event to the Kafka source and block until an EventResult is recieved. :param event: The event to send. @@ -632,8 +667,6 @@ def set_ts(ts): self._futures[event._id]["sent_t"] = ts self.producer.produce(self.input_topic, value=pickle.dumps(event), on_delivery=lambda err, msg: set_ts(msg.timestamp())) - if flush: - self.producer.flush() return event._id def close(self): diff --git a/src/cascade/runtime/python_runtime.py b/src/cascade/runtime/python_runtime.py index 8743014..a955e9c 100644 --- a/src/cascade/runtime/python_runtime.py +++ b/src/cascade/runtime/python_runtime.py @@ -16,7 +16,7 @@ def process(self, event: Event): key = event.variable_map[event.target.read_key_from] - print(f"PythonStatefulOperator: {event}") + print(f"PythonStatefulOperator[{self.operator.entity.__name__}[{key}]]: {event}") if isinstance(event.target.method_type, InitClass): result = self.operator.handle_init_class(*event.variable_map.values()) @@ -50,7 +50,7 @@ def __init__(self, operator: StatelessOperator): def process(self, event: Event): assert(isinstance(event.target, StatelessOpNode)) - + print(f"PythonStatelessOperator[{self.operator.dataflow.name}]: {event}") if isinstance(event.target.method_type, InvokeMethod): result = self.operator.handle_invoke_method(