Skip to content

Commit

Permalink
Various performance improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
execveat committed Oct 24, 2023
1 parent f123ea7 commit 129d2ce
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 64 deletions.
2 changes: 2 additions & 0 deletions src/gqlspection/GQLArg.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# coding: utf-8
from __future__ import unicode_literals
from gqlspection.six import ensure_text, text_type
from gqlspection.utils.tools import cache_str_repr
import gqlspection


@cache_str_repr
class GQLArg(object):
name = ''
kind = None
Expand Down
2 changes: 2 additions & 0 deletions src/gqlspection/GQLEnum.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# coding: utf-8
from __future__ import unicode_literals
from gqlspection.six import ensure_text
from gqlspection.utils.tools import cache_str_repr


@cache_str_repr
class GQLEnum(object):
name = ''
description = ''
Expand Down
110 changes: 90 additions & 20 deletions src/gqlspection/GQLSubQuery.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# coding: utf-8
from __future__ import unicode_literals
from gqlspection.six import python_2_unicode_compatible, text_type
from gqlspection.utils import pad_string, format_comment
from gqlspection.utils import format_comment
from gqlspection.GQLField import GQLField
from gqlspection.GQLTypeProxy import GQLTypeProxy
import copy

import sys
if sys.platform.startswith('java'):
from multiprocessing.dummy import Pool as ThreadPool
else:
from multiprocessing import Pool as ThreadPool


# TODO: Figure out if it's better to split this monstrosity in two classes
Expand Down Expand Up @@ -91,7 +96,7 @@ def schedule_close_brace(self):
self.stack.append(None)

def schedule(self, items):
self.stack.extend(reversed(items))
self.stack.extend(items[::-1])


@python_2_unicode_compatible
Expand Down Expand Up @@ -210,53 +215,118 @@ def description(self):

return self._format_comment(self._description)

def subquery(self, field):
def subquery(self, field, depth=None):
"""Create a subquery for the given field."""
if depth is None:
depth = self.depth + 1

if isinstance(field, GQLField):
return GQLSubQuery(field, indent=self.indent, max_depth=self.max_depth, current_depth=self.depth + 1)
return GQLSubQuery(field, indent=self.indent, max_depth=self.max_depth, current_depth=depth)
elif isinstance(field, GQLTypeProxy):
return GQLSubQuery(self.field, indent=self.indent, max_depth=self.max_depth, current_depth=self.depth + 1, union=field)
return GQLSubQuery(self.field, indent=self.indent, max_depth=self.max_depth, current_depth=depth, union=field)

def to_string(self, pad=4):
def to_string(self, pad=4, query=None):
"""Generate a string representation of the GraphQL query, iteratively."""
self.builder = QueryBuilder(self, pad)
if query is None:
query = self

for query in self.builder:
builder = QueryBuilder(query, pad)

for query in builder:
# Union handling is a bit hacky, but it works
if query.union:
self.builder.add_line('... on ' + query.union.name)
self.builder.open_brace()
self.builder.add_line('__typename')
self.builder.schedule([query.subquery(field) for field in query.union.fields])
builder.add_line('... on ' + query.union.name)
builder.open_brace()
builder.add_line('__typename')
builder.schedule([query.subquery(field) for field in query.union.fields])
continue

# Print out description and field name
description_lines = query.description.splitlines()

if len(description_lines) == 1 and query.field.type.kind.is_leaf:
# Inline comment for a leaf type, like a scalar or enum, assuming description fits in a single line
self.builder.add_line(query.name_and_args + ' ' + query.description)
builder.add_line(query.name_and_args + ' ' + query.description)
else:
# Multiline comment precedes the field name
self.builder.add_lines(description_lines)
self.builder.add_field(query.name_and_args)
builder.add_lines(description_lines)
builder.add_field(query.name_and_args)

# A complicated type, like an object or union - open a curly brace and iterate over subfields
if not query.field.type.kind.is_leaf:
# Opens a curly brace and schedules a closing brace to be printed after all subfields are processed
self.builder.open_brace()
builder.open_brace()

if query.depth_limit_reached:
# If we reached the depth limit, don't add any subfields
continue

# Add subfields
if query.field.kind.kind == 'UNION':
self.builder.schedule([query.subquery(union) for union in query.field.type.unions])
builder.schedule([query.subquery(union) for union in query.field.type.unions])

else:
#if query.field.kind.kind == 'OBJECT':
self.builder.schedule([query.subquery(field) for field in query.field.type.fields])
builder.schedule([query.subquery(field) for field in query.field.type.fields])
return builder.build()

def to_string2(self, pad=None, num_threads=4):
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
pool = ThreadPool(num_threads)
queries = [self.subquery(field, depth=1) for field in self.field.type.fields]
results = pool.map(self.to_string_naive, queries)
pool.close()
pool.join()
return '\n'.join(results)


def to_string_naive(self, query=None, pad=4):
print("NAIVE")
print("NAIVE")
print("NAIVE")
print("NAIVE")
print("NAIVE")
print("NAIVE")
"""Generate a string representation of the GraphQL query, iteratively."""
if query is None:
query = self

builder = QueryBuilder(query, pad)

for query in builder:
# Union handling is a bit hacky, but it works
if query.union:
builder.add_line('... on ' + query.union.name)
builder.open_brace()
builder.add_line('__typename')
builder.schedule([query.subquery(field) for field in query.union.fields])
continue

return self.builder.build()
# Print out description and field name
description_lines = query.description.splitlines()

if len(description_lines) == 1 and query.field.type.kind.is_leaf:
# Inline comment for a leaf type, like a scalar or enum, assuming description fits in a single line
builder.add_line(query.name_and_args + ' ' + query.description)
else:
# Multiline comment precedes the field name
builder.add_lines(description_lines)
builder.add_field(query.name_and_args)

# A complicated type, like an object or union - open a curly brace and iterate over subfields
if not query.field.type.kind.is_leaf:
# Opens a curly brace and schedules a closing brace to be printed after all subfields are processed
builder.open_brace()

if query.depth_limit_reached:
# If we reached the depth limit, don't add any subfields
continue

# Add subfields
if query.field.kind.kind == 'UNION':
builder.schedule([query.subquery(union) for union in query.field.type.unions])

else:
#if query.field.kind.kind == 'OBJECT':
builder.schedule([query.subquery(field) for field in query.field.type.fields])
return builder.build()
30 changes: 27 additions & 3 deletions src/gqlspection/GQLTypeKind.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ def __init__(self, name, kind, modifiers=None):
self.name = ensure_text(name)
self.kind = kind
self.modifiers = modifiers or []
self._is_builtin_scalar = None
self._is_leaf = None
self._is_final = None
self.isbs = None
self.isl = None
self.isf = None

@staticmethod
def from_json(typedef):
Expand Down Expand Up @@ -87,12 +93,30 @@ def __repr__(self):

@property
def is_builtin_scalar(self):
return self.name in self.builtin_scalars
value = self._is_builtin_scalar
if value is not None:
return value

value = self.name in self.builtin_scalars
self._is_builtin_scalar = value
return value

@property
def is_leaf(self):
return self.kind in self.leaf_types
value = self._is_leaf
if value is not None:
return value

value = self.kind in self.leaf_types
self._is_leaf = value
return value

@property
def is_final(self):
return self.is_leaf or self.is_builtin_scalar
value = self._is_final
if value is not None:
return value

value = self.is_leaf or self.is_builtin_scalar
self._is_final = value
return value
55 changes: 14 additions & 41 deletions src/gqlspection/GQLTypeProxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,25 @@
from __future__ import unicode_literals
from gqlspection import log
from gqlspection.six import ensure_text
import gqlspection


class GQLTypeProxy(object):
name = ''
schema = None
_upstream = None
max_depth = 4

"""A proxy for GQLType that allows for lazy loading of nested objects."""
def __init__(self, name, schema):
# type: (str, GQLSchema) -> None
self.name = ensure_text(name)
self.schema = schema

@property
def upstream(self):
# use cached value if present
if self._upstream:
return self._upstream

if self.name in self.schema.types:
self._upstream = self.schema.types[self.name]
return self._upstream
else:
# TODO: expose this somehow through cli
if 'DEBUGGER' in globals():
import pdb
pdb.set_trace()
log.debug("Found an unknown type: '%s'. At this time following types are present in schema:", self.name)
for t in self.schema.types:
log.debug(" %s(%s) [%s]" % (type(t.name), t.name, t.kind.kind))

raise Exception("GQLTypeProxy: type '%s' not defined" % self.name)

def _proxy_getattr(self, item, levels):
if levels >= self.max_depth:
raise Exception("GQLTypeProxy: reached the recursion limit!")
return getattr(self, item)
self.upstream = None

def __getattr__(self, item):
proxy = getattr(self.upstream, '_proxy_getattr', None)
if proxy:
# nested object detected, pass execution to proxy
return proxy(item, 0)

return getattr(self.upstream, item)

def __dir__(self):
return super(gqlspection.GQLType, self.upstream).__dir__()
# Once the attribute is accessed, we load the actual object from the schema and replace attribute with strong reference
upstream = self.upstream
if upstream is None:
try:
upstream = self.schema.types[self.name]
self.upstream = upstream
except KeyError:
raise AttributeError("GQLTypeProxy: type '%s' not defined" % self.name)
upstream_item = getattr(upstream, item)

setattr(self, item, upstream_item) # Setting attribute to the proxy
return upstream_item
20 changes: 20 additions & 0 deletions src/gqlspection/utils/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,23 @@ def query_introspection(url, headers=None, include_metadata=False, request_fn=No
# None of the introspection queries were successful
log.warning("Introspection seems disabled for this endpoint: '%s'.", url)
raise Exception("Introspection seems disabled for this endpoint: '%s'." % url)


def cache_str_repr(cls):
"""A class decorator that caches the __str__ and __repr__ methods."""

def cached_method(method_func, cache_attribute):
"""Inner decorator to apply caching to a method."""
def wrapper(self):
cache_value = getattr(self, cache_attribute, None)
if cache_value is None:
cache_value = method_func(self)
setattr(self, cache_attribute, cache_value)
return cache_value
return wrapper

# Apply caching to the __str__ and __repr__ methods
cls.__str__ = cached_method(cls.__str__, "__cachedstrings__str__")
cls.__repr__ = cached_method(cls.__repr__, "__cachedstrings__repr__")

return cls

0 comments on commit 129d2ce

Please sign in to comment.