Skip to content

Commit

Permalink
Autoload nested constants
Browse files Browse the repository at this point in the history
Reduces require time from ~160ms to ~5ms. Autoloads all constants under
`GraphQL` to optimize boot time. Also autoloads `GraphQL::Types` and
`GraphQL::Query` because they can't be required in sequence.

Autoloads are automatically managed with `GraphQL::Autoload` similar to
`ActiveSupport::Autoload`.
  • Loading branch information
gmcgibbon committed Nov 29, 2024
1 parent db1dba5 commit 11799e0
Show file tree
Hide file tree
Showing 15 changed files with 198 additions and 70 deletions.
22 changes: 22 additions & 0 deletions guides/code_loading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
layout: guide
doc_stub: false
search: true
title: Code Loading
section: Other
desc: Read this before deploying GraphQL to production.
---

## Autoloading and Eager Loading

GraphQL Ruby is autoloaded, which means most code won't be loaded until it is referenced. This is optimal for development and test environments where you want to boot your application as fast as possible. However, this is not optimal for production enviromnets.

Production environments typically include multiple workers, and need to load an application upfront as much as possible. This ensures requests are as fast as possible at the cost of increased boot time, and forked processes don't need to load additional code. Unfortunately, there is no approach to eager code loading that is accepted by all web application frameworks.

- For Rails applications, a Railtie is included that automatically eager-loads the GraphQL Ruby library for you. No action is required by the developer to opt into this behaviour.

- For Sinatra applications, please put `configure(:production) { GraphQL.eager_load! }` in your application file.

- For Hanami applications, please put `environment(:production) { GraphQL.eager_load! }` in your application file.

- Other frameworks need to manually call `GraphQL.eager_load!` when their application is booting in production mode. If this is not done properly, GraphQL Ruby will log an warning.
105 changes: 58 additions & 47 deletions lib/graphql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
require "singleton"
require "forwardable"
require "fiber/storage"
require "graphql/autoload"

module GraphQL
extend Autoload

class Error < StandardError
end

Expand Down Expand Up @@ -74,53 +77,61 @@ class << self
end

self.reject_numbers_followed_by_names = false
end

# Order matters for these:

require "graphql/execution_error"
require "graphql/runtime_type_error"
require "graphql/unresolved_type_error"
require "graphql/invalid_null_error"
require "graphql/analysis_error"
require "graphql/coercion_error"
require "graphql/invalid_name_error"
require "graphql/integer_decoding_error"
require "graphql/integer_encoding_error"
require "graphql/string_encoding_error"
require "graphql/date_encoding_error"
require "graphql/duration_encoding_error"
require "graphql/type_kinds"
require "graphql/name_validator"
require "graphql/language"

require_relative "./graphql/railtie" if defined? Rails::Railtie

require "graphql/analysis"
require "graphql/tracing"
require "graphql/dig"
require "graphql/execution"
require "graphql/pagination"
require "graphql/schema"
require "graphql/query"
require "graphql/dataloader"
require "graphql/types"
require "graphql/static_validation"
require "graphql/execution"
require "graphql/schema/built_in_types"
require "graphql/schema/loader"
require "graphql/schema/printer"
require "graphql/introspection"
require "graphql/relay"
class << self
def ensure_eager_load!
if production? && !eager_loading?
warn "GraphQL should be eager loaded in production environments!"
end
end

private

def production?
(env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || ENV["HANAMI_ENV"] || ENV["APP_ENV"]) && env.to_s.downcase == "production"
end
end

autoload :ExecutionError, "graphql/execution_error"
autoload :RuntimeTypeError, "graphql/runtime_type_error"
autoload :UnresolvedTypeError, "graphql/unresolved_type_error"
autoload :InvalidNullError, "graphql/invalid_null_error"
autoload :AnalysisError, "graphql/analysis_error"
autoload :CoercionError, "graphql/coercion_error"
autoload :InvalidNameError, "graphql/invalid_name_error"
autoload :IntegerDecodingError, "graphql/integer_decoding_error"
autoload :IntegerEncodingError, "graphql/integer_encoding_error"
autoload :StringEncodingError, "graphql/string_encoding_error"
autoload :DateEncodingError, "graphql/date_encoding_error"
autoload :DurationEncodingError, "graphql/duration_encoding_error"
autoload :TypeKinds, "graphql/type_kinds"
autoload :NameValidator, "graphql/name_validator"
autoload :Language, "graphql/language"

autoload :Analysis, "graphql/analysis"
autoload :Tracing, "graphql/tracing"
autoload :Dig, "graphql/dig"
autoload :Execution, "graphql/execution"
autoload :Pagination, "graphql/pagination"
autoload :Schema, "graphql/schema"
autoload :Query, "graphql/query"
autoload :Dataloader, "graphql/dataloader"
autoload :Types, "graphql/types"
autoload :StaticValidation, "graphql/static_validation"
autoload :Execution, "graphql/execution"
autoload :Introspection, "graphql/introspection"
autoload :Relay, "graphql/relay"
autoload :Subscriptions, "graphql/subscriptions"
autoload :ParseError, "graphql/parse_error"
autoload :Backtrace, "graphql/backtrace"

autoload :UnauthorizedError, "graphql/unauthorized_error"
autoload :UnauthorizedEnumValueError, "graphql/unauthorized_enum_value_error"
autoload :UnauthorizedFieldError, "graphql/unauthorized_field_error"
autoload :LoadApplicationObjectFailedError, "graphql/load_application_object_failed_error"
autoload :Testing, "graphql/testing"
autoload :Current, "graphql/current"
end

require "graphql/version"
require "graphql/subscriptions"
require "graphql/parse_error"
require "graphql/backtrace"

require "graphql/unauthorized_error"
require "graphql/unauthorized_enum_value_error"
require "graphql/unauthorized_field_error"
require "graphql/load_application_object_failed_error"
require "graphql/testing"
require "graphql/current"
require "graphql/railtie" if defined? Rails::Railtie
28 changes: 28 additions & 0 deletions lib/graphql/autoload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module GraphQL
module Autoload
def autoload(const_name, path)
@_eagerloaded_constants ||= []
@_eagerloaded_constants << const_name

super const_name, path
end

def eager_load!
@_eager_loading = true
if @_eagerloaded_constants
@_eagerloaded_constants.each { |const_name| const_get(const_name) }
@_eagerloaded_constants = nil
end
ensure
@_eager_loading = false
end

private

def eager_loading?
@_eager_loading ||= false
end
end
end
18 changes: 10 additions & 8 deletions lib/graphql/query.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
# frozen_string_literal: true
require "graphql/query/context"
require "graphql/query/fingerprint"
require "graphql/query/null_context"
require "graphql/query/result"
require "graphql/query/variables"
require "graphql/query/input_validation_result"
require "graphql/query/variable_validation_error"
require "graphql/query/validation_pipeline"

module GraphQL
# A combination of query string and {Schema} instance which can be reduced to a {#result}.
class Query
extend Autoload
include Tracing::Traceable
extend Forwardable

autoload :Context, "graphql/query/context"
autoload :Fingerprint, "graphql/query/fingerprint"
autoload :NullContext, "graphql/query/null_context"
autoload :Result, "graphql/query/result"
autoload :Variables, "graphql/query/variables"
autoload :InputValidationResult, "graphql/query/input_validation_result"
autoload :VariableValidationError, "graphql/query/variable_validation_error"
autoload :ValidationPipeline, "graphql/query/validation_pipeline"

class OperationNameMissingError < GraphQL::ExecutionError
def initialize(name)
msg = if name.nil?
Expand Down
3 changes: 2 additions & 1 deletion lib/graphql/query/context.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# frozen_string_literal: true
require "graphql/query/context/scoped_context"

module GraphQL
class Query
Expand Down Expand Up @@ -289,3 +288,5 @@ def set!(key, value)
end
end
end

require "graphql/query/context/scoped_context"
1 change: 1 addition & 0 deletions lib/graphql/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module GraphQL
class Railtie < Rails::Railtie
config.graphql = ActiveSupport::OrderedOptions.new
config.graphql.parser_cache = false
config.eager_load_namespaces << GraphQL

initializer("graphql.cache") do |app|
if config.graphql.parser_cache
Expand Down
6 changes: 6 additions & 0 deletions lib/graphql/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
require "graphql/schema/subscription"
require "graphql/schema/visibility"

GraphQL.ensure_eager_load!

module GraphQL
# A GraphQL schema which may be queried with {GraphQL::Query}.
#
Expand Down Expand Up @@ -1802,3 +1804,7 @@ module DefaultTraceClass
end
end
end

require "graphql/schema/built_in_types"
require "graphql/schema/loader"
require "graphql/schema/printer"
1 change: 0 additions & 1 deletion lib/graphql/schema/relay_classic_mutation.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# frozen_string_literal: true
require "graphql/types/string"

module GraphQL
class Schema
Expand Down
29 changes: 18 additions & 11 deletions lib/graphql/types.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
# frozen_string_literal: true
require "graphql/types/boolean"
require "graphql/types/big_int"
require "graphql/types/float"
require "graphql/types/id"
require "graphql/types/int"
require "graphql/types/iso_8601_date"
require "graphql/types/iso_8601_date_time"
require "graphql/types/iso_8601_duration"
require "graphql/types/json"
require "graphql/types/string"
require "graphql/types/relay"

module GraphQL
module Types
extend Autoload

autoload :Boolean, "graphql/types/boolean"
autoload :BigInt, "graphql/types/big_int"
autoload :Float, "graphql/types/float"
autoload :ID, "graphql/types/id"
autoload :Int, "graphql/types/int"
autoload :JSON, "graphql/types/json"
autoload :String, "graphql/types/string"
autoload :ISO8601Date, "graphql/types/iso_8601_date"
autoload :ISO8601DateTime, "graphql/types/iso_8601_date_time"
autoload :ISO8601Duration, "graphql/types/iso_8601_duration"
autoload :Relay, "graphql/types/relay"
end
end
6 changes: 6 additions & 0 deletions spec/fixtures/eager_module/eager_class.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

module EagerModule
module EagerClass
end
end
6 changes: 6 additions & 0 deletions spec/fixtures/eager_module/other_eager_class.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

module EagerModule
module OtherEagerClass
end
end
6 changes: 6 additions & 0 deletions spec/fixtures/lazy_module/lazy_class.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

module LazyModule
module LazyClass
end
end
33 changes: 33 additions & 0 deletions spec/graphql/autoload_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

require "spec_helper"

describe GraphQL::Autoload do
module LazyModule
extend GraphQL::Autoload
autoload(:LazyClass, "fixtures/lazy_module/lazy_class")
end

module EagerModule
extend GraphQL::Autoload
autoload(:EagerClass, "fixtures/eager_module/eager_class")
autoload(:OtherEagerClass, "fixtures/eager_module/other_eager_class")
end

describe "#autoload" do
it "sets autoload" do
assert_equal("fixtures/lazy_module/lazy_class", LazyModule.autoload?(:LazyClass))
LazyModule::LazyClass
assert_nil(LazyModule.autoload?(:LazyClass))
end
end

describe "#eager_load!" do
it "eagerly loads autoload entries" do
EagerModule.eager_load!

assert_nil(EagerModule.autoload?(:EagerClass))
assert_nil(EagerModule.autoload?(:OtherEagerClass))
end
end
end
2 changes: 1 addition & 1 deletion spec/graphql/types/iso_8601_date_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true
require "spec_helper"
require "graphql/types/iso_8601_date"

describe GraphQL::Types::ISO8601Date do
module DateTest
class DateObject < GraphQL::Schema::Object
Expand Down
2 changes: 1 addition & 1 deletion spec/graphql/types/iso_8601_date_time_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true
require "spec_helper"
require "graphql/types/iso_8601_date_time"

describe GraphQL::Types::ISO8601DateTime do
module DateTimeTest
class DateTimeObject < GraphQL::Schema::Object
Expand Down

0 comments on commit 11799e0

Please sign in to comment.