图片解析应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

211 lines
6.6 KiB

  1. import re
  2. class ProfileStats:
  3. """
  4. ProfileStats, runtime execution statistics of operation.
  5. """
  6. def __init__(self, records_produced, execution_time):
  7. self.records_produced = records_produced
  8. self.execution_time = execution_time
  9. class Operation:
  10. """
  11. Operation, single operation within execution plan.
  12. """
  13. def __init__(self, name, args=None, profile_stats=None):
  14. """
  15. Create a new operation.
  16. Args:
  17. name: string that represents the name of the operation
  18. args: operation arguments
  19. profile_stats: profile statistics
  20. """
  21. self.name = name
  22. self.args = args
  23. self.profile_stats = profile_stats
  24. self.children = []
  25. def append_child(self, child):
  26. if not isinstance(child, Operation) or self is child:
  27. raise Exception("child must be Operation")
  28. self.children.append(child)
  29. return self
  30. def child_count(self):
  31. return len(self.children)
  32. def __eq__(self, o: object) -> bool:
  33. if not isinstance(o, Operation):
  34. return False
  35. return self.name == o.name and self.args == o.args
  36. def __str__(self) -> str:
  37. args_str = "" if self.args is None else " | " + self.args
  38. return f"{self.name}{args_str}"
  39. class ExecutionPlan:
  40. """
  41. ExecutionPlan, collection of operations.
  42. """
  43. def __init__(self, plan):
  44. """
  45. Create a new execution plan.
  46. Args:
  47. plan: array of strings that represents the collection operations
  48. the output from GRAPH.EXPLAIN
  49. """
  50. if not isinstance(plan, list):
  51. raise Exception("plan must be an array")
  52. if isinstance(plan[0], bytes):
  53. plan = [b.decode() for b in plan]
  54. self.plan = plan
  55. self.structured_plan = self._operation_tree()
  56. def _compare_operations(self, root_a, root_b):
  57. """
  58. Compare execution plan operation tree
  59. Return: True if operation trees are equal, False otherwise
  60. """
  61. # compare current root
  62. if root_a != root_b:
  63. return False
  64. # make sure root have the same number of children
  65. if root_a.child_count() != root_b.child_count():
  66. return False
  67. # recursively compare children
  68. for i in range(root_a.child_count()):
  69. if not self._compare_operations(root_a.children[i], root_b.children[i]):
  70. return False
  71. return True
  72. def __str__(self) -> str:
  73. def aggraget_str(str_children):
  74. return "\n".join(
  75. [
  76. " " + line
  77. for str_child in str_children
  78. for line in str_child.splitlines()
  79. ]
  80. )
  81. def combine_str(x, y):
  82. return f"{x}\n{y}"
  83. return self._operation_traverse(
  84. self.structured_plan, str, aggraget_str, combine_str
  85. )
  86. def __eq__(self, o: object) -> bool:
  87. """Compares two execution plans
  88. Return: True if the two plans are equal False otherwise
  89. """
  90. # make sure 'o' is an execution-plan
  91. if not isinstance(o, ExecutionPlan):
  92. return False
  93. # get root for both plans
  94. root_a = self.structured_plan
  95. root_b = o.structured_plan
  96. # compare execution trees
  97. return self._compare_operations(root_a, root_b)
  98. def _operation_traverse(self, op, op_f, aggregate_f, combine_f):
  99. """
  100. Traverse operation tree recursively applying functions
  101. Args:
  102. op: operation to traverse
  103. op_f: function applied for each operation
  104. aggregate_f: aggregation function applied for all children of a single operation
  105. combine_f: combine function applied for the operation result and the children result
  106. """ # noqa
  107. # apply op_f for each operation
  108. op_res = op_f(op)
  109. if len(op.children) == 0:
  110. return op_res # no children return
  111. else:
  112. # apply _operation_traverse recursively
  113. children = [
  114. self._operation_traverse(child, op_f, aggregate_f, combine_f)
  115. for child in op.children
  116. ]
  117. # combine the operation result with the children aggregated result
  118. return combine_f(op_res, aggregate_f(children))
  119. def _operation_tree(self):
  120. """Build the operation tree from the string representation"""
  121. # initial state
  122. i = 0
  123. level = 0
  124. stack = []
  125. current = None
  126. def _create_operation(args):
  127. profile_stats = None
  128. name = args[0].strip()
  129. args.pop(0)
  130. if len(args) > 0 and "Records produced" in args[-1]:
  131. records_produced = int(
  132. re.search("Records produced: (\\d+)", args[-1]).group(1)
  133. )
  134. execution_time = float(
  135. re.search("Execution time: (\\d+.\\d+) ms", args[-1]).group(1)
  136. )
  137. profile_stats = ProfileStats(records_produced, execution_time)
  138. args.pop(-1)
  139. return Operation(
  140. name, None if len(args) == 0 else args[0].strip(), profile_stats
  141. )
  142. # iterate plan operations
  143. while i < len(self.plan):
  144. current_op = self.plan[i]
  145. op_level = current_op.count(" ")
  146. if op_level == level:
  147. # if the operation level equal to the current level
  148. # set the current operation and move next
  149. child = _create_operation(current_op.split("|"))
  150. if current:
  151. current = stack.pop()
  152. current.append_child(child)
  153. current = child
  154. i += 1
  155. elif op_level == level + 1:
  156. # if the operation is child of the current operation
  157. # add it as child and set as current operation
  158. child = _create_operation(current_op.split("|"))
  159. current.append_child(child)
  160. stack.append(current)
  161. current = child
  162. level += 1
  163. i += 1
  164. elif op_level < level:
  165. # if the operation is not child of current operation
  166. # go back to it's parent operation
  167. levels_back = level - op_level + 1
  168. for _ in range(levels_back):
  169. current = stack.pop()
  170. level -= levels_back
  171. else:
  172. raise Exception("corrupted plan")
  173. return stack[0]