apollographql / apollo-ios

📱  A strongly-typed, caching GraphQL client for iOS, written in Swift.
https://www.apollographql.com/docs/ios/
MIT License
3.85k stars 708 forks source link

Slow compilation speed when migrating from 0.x to 1.12.2 #3388

Open m1entus opened 1 month ago

m1entus commented 1 month ago

Question

Hey guys,

I am currently migrating to 1.12.2 from 0.x, previously we were genarating only one file with all generation models. Now i am trying to migrate and compile project but compilation times for some files are sooo slow. I am using this kind of configuration:

{
  "schemaNamespace": "UniversalGraphQLAPI",
  "input": {
    "operationSearchPaths": [
      "./GraphQL/**/*.graphql"
    ],
    "schemaSearchPaths" : [
      "./schema.graphqls"
    ]
  },
  "output": {
    "schemaTypes": {
        "moduleType": {
          "other": {}
        },
        "path": "./Generated"
    },
    "operations": {
        "inSchemaModule": {}
    },
    "testMocks": {
        "none": {}
    }
  },
  "options": {
    "deprecatedEnumCases": "include",
    "selectionSetInitializers" : {
        "operations": false,
        "namedFragments": false,
        "localCacheMutations" : false
    },
    "operationDocumentFormat" : ["operationId"],
    "cocoapodsCompatibleImportStatements": false,
    "warningsOnDeprecatedUsage": "include",
    "pruneGeneratedFiles": true,
    "markOperationDefinitionsAsFinal": true
  }
}

In addition to that i wanted to check why i have so long compilations speed by checking function compiler times but unfortenately it does not show anything (no warning at all):

.target(
            name: "UniversalGraphQLAPI",
            dependencies: [
                .product(name: "Apollo", package: "apollo-ios")
            ],
            swiftSettings: [
                .define("DEBUG", .when(configuration: .debug)),
                .unsafeFlags(["-Xfrontend", "-debug-time-function-bodies", "-Xfrontend", "-warn-long-expression-type-checking=100", "-Xfrontend", "-warn-long-function-bodies=100"])
            ]
        ),
Zrzut ekranu 2024-05-31 o 18 55 33

As a example i can show some of files from both batch (one that compile super fast, and second one that very slow):

// @generated
// This file was automatically generated and should not be edited.

@_exported import ApolloAPI

public final class DeleteEventMutation: GraphQLMutation {
  public static let operationName: String = "DeleteEvent"
  public static let operationDocument: ApolloAPI.OperationDocument = .init(
    operationIdentifier: "f3b9b32244b9a69aa2454e280853ddf04baedfe310fde6c8119b2e663935493a"
  )

  public var eventId: ID

  public init(eventId: ID) {
    self.eventId = eventId
  }

  public var __variables: Variables? { ["eventId": eventId] }

  public struct Data: UniversalGraphQLAPI.SelectionSet {
    public let __data: DataDict
    public init(_dataDict: DataDict) { __data = _dataDict }

    public static var __parentType: ApolloAPI.ParentType { UniversalGraphQLAPI.Objects.Mutation }
    public static var __selections: [ApolloAPI.Selection] { [
      .field("deleteEvent", Bool.self, arguments: ["eventId": .variable("eventId")]),
    ] }

    /// delete an existing event, must be the event owner of a moderator in the home/room
    public var deleteEvent: Bool { __data["deleteEvent"] }
  }
}
// @generated
// This file was automatically generated and should not be edited.

@_exported import ApolloAPI

public final class ProfileRoomMemberQuery: GraphQLQuery {
  public static let operationName: String = "ProfileRoomMember"
  public static let operationDocument: ApolloAPI.OperationDocument = .init(
    operationIdentifier: "f324d1adaa2a11056e8b73443eb8a0160ef995452e0903ee90e3a3b64dd0b305"
  )

  public var profileId: String
  public var roomId: GraphQLNullable<String>

  public init(
    profileId: String,
    roomId: GraphQLNullable<String>
  ) {
    self.profileId = profileId
    self.roomId = roomId
  }

  public var __variables: Variables? { [
    "profileId": profileId,
    "roomId": roomId
  ] }

  public struct Data: UniversalGraphQLAPI.SelectionSet {
    public let __data: DataDict
    public init(_dataDict: DataDict) { __data = _dataDict }

    public static var __parentType: ApolloAPI.ParentType { UniversalGraphQLAPI.Objects.Query }
    public static var __selections: [ApolloAPI.Selection] { [
      .field("getProfile", GetProfile.self, arguments: [
        "profileId": .variable("profileId"),
        "roomId": .variable("roomId"),
        "homeId": .null,
        "includeVisibleHomes": .null
      ]),
    ] }

    /// Fetch a single user's profile
    ///
    /// If home ID is provided then the result will resolve the home specific fields
    ///
    /// Must set includeVisibleHomes to true in order to resolve the list of visible homes Cached PRIVATE with TTL 5 seconds.
    public var getProfile: GetProfile { __data["getProfile"] }

    /// GetProfile
    ///
    /// Parent Type: `Profile`
    public struct GetProfile: UniversalGraphQLAPI.SelectionSet {
      public let __data: DataDict
      public init(_dataDict: DataDict) { __data = _dataDict }

      public static var __parentType: ApolloAPI.ParentType { UniversalGraphQLAPI.Objects.Profile }
      public static var __selections: [ApolloAPI.Selection] { [
        .field("__typename", String.self),
        .field("roomMemberEdges", RoomMemberEdges?.self, arguments: ["roomId": .variable("roomId")]),
      ] }

      public var roomMemberEdges: RoomMemberEdges? { __data["roomMemberEdges"] }

      /// GetProfile.RoomMemberEdges
      ///
      /// Parent Type: `RoomMemberEdges`
      public struct RoomMemberEdges: UniversalGraphQLAPI.SelectionSet {
        public let __data: DataDict
        public init(_dataDict: DataDict) { __data = _dataDict }

        public static var __parentType: ApolloAPI.ParentType { UniversalGraphQLAPI.Objects.RoomMemberEdges }
        public static var __selections: [ApolloAPI.Selection] { [
          .field("__typename", String.self),
          .field("isRoomMember", Bool.self),
          .field("profileId", UniversalGraphQLAPI.ID.self),
          .field("roomId", String.self),
        ] }

        public var isRoomMember: Bool { __data["isRoomMember"] }
        public var profileId: UniversalGraphQLAPI.ID { __data["profileId"] }
        public var roomId: String { __data["roomId"] }
      }
    }
  }
}

Do you have idea why this is happening or how i can debug it, why compile speed are soooo long ? (i am using m1 max, so normally took few seconds for all generated files - at least before migration).

Initially i also thoutght that this is because of definitions with a lot of parameters in array for fragments, but also doesn't help turning it off.

Thanks for help.

AnthonyMDev commented 1 month ago

Hi @m1entus. Thanks for providing so much info with this report. We have never heard of this from anyone else before, and it really doesn't make sense to me why it would be taking so long. These aren't very large generated files, and they don't seem to be very complex either.

I'm at a complete loss as to why this is happening. I'm wondering if there is something else going on with either your project or your machine that is causing this. Have you tried having someone else try to compile the project on a different machine?

m1entus commented 1 month ago

Yes it is the same, i just found out that new generator - generates fragments in different manner than before coausing that some of my fragments have generated 22k lines in one file and not 3k lines for specific fragment, for exmple in API in v.0x i have:

public struct MessageWithThreadBookmarkDetails: GraphQLFragment {
  /// The raw GraphQL definition of this fragment.
  public static let fragmentDefinition: String =
    """
    fragment MessageWithThreadBookmarkDetails on Message {
      __typename
      ...MessageDetails
      bookmark {
        __typename
        ...ThreadBookmarkDetails
      }
    }
    """

  public static let possibleTypes: [String] = ["Message"]

  public static var selections: [GraphQLSelection] {
    return [
      GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
      GraphQLFragmentSpread(MessageDetails.self),
      GraphQLField("bookmark", type: .object(Bookmark.selections)),
    ]
  }

  public private(set) var resultMap: ResultMap

  public init(unsafeResultMap: ResultMap) {
    self.resultMap = unsafeResultMap
  }

  public var __typename: String {
    get {
      return resultMap["__typename"]! as! String
    }
    set {
      resultMap.updateValue(newValue, forKey: "__typename")
    }
  }

  /// set only if the message is being followed or the user is an active participant in the thread, only for home threads
  public var bookmark: Bookmark? {
    get {
      return (resultMap["bookmark"] as? ResultMap).flatMap { Bookmark(unsafeResultMap: $0) }
    }
    set {
      resultMap.updateValue(newValue?.resultMap, forKey: "bookmark")
    }
  }

  public var fragments: Fragments {
    get {
      return Fragments(unsafeResultMap: resultMap)
    }
    set {
      resultMap += newValue.resultMap
    }
  }

  public struct Fragments {
    public private(set) var resultMap: ResultMap

    public init(unsafeResultMap: ResultMap) {
      self.resultMap = unsafeResultMap
    }

    public var messageDetails: MessageDetails {
      get {
        return MessageDetails(unsafeResultMap: resultMap)
      }
      set {
        resultMap += newValue.resultMap
      }
    }
  }

  public struct Bookmark: GraphQLSelectionSet {
    public static let possibleTypes: [String] = ["ThreadBookmark"]

    public static var selections: [GraphQLSelection] {
      return [
        GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
        GraphQLFragmentSpread(ThreadBookmarkDetails.self),
      ]
    }

    public private(set) var resultMap: ResultMap

    public init(unsafeResultMap: ResultMap) {
      self.resultMap = unsafeResultMap
    }

    public init(roomId: GraphQLID, threadId: String, lastSentMessageId: String, lastViewedMessageId: String? = nil, hasNewMessages: Bool) {
      self.init(unsafeResultMap: ["__typename": "ThreadBookmark", "roomId": roomId, "threadId": threadId, "lastSentMessageId": lastSentMessageId, "lastViewedMessageId": lastViewedMessageId, "hasNewMessages": hasNewMessages])
    }

    public var __typename: String {
      get {
        return resultMap["__typename"]! as! String
      }
      set {
        resultMap.updateValue(newValue, forKey: "__typename")
      }
    }

    public var fragments: Fragments {
      get {
        return Fragments(unsafeResultMap: resultMap)
      }
      set {
        resultMap += newValue.resultMap
      }
    }

    public struct Fragments {
      public private(set) var resultMap: ResultMap

      public init(unsafeResultMap: ResultMap) {
        self.resultMap = unsafeResultMap
      }

      public var threadBookmarkDetails: ThreadBookmarkDetails {
        get {
          return ThreadBookmarkDetails(unsafeResultMap: resultMap)
        }
        set {
          resultMap += newValue.resultMap
        }
      }
    }
  }
}

where fragment is defined this way:

fragment MessageWithThreadBookmarkDetails on Message {
    ...MessageDetails
    bookmark {
        ...ThreadBookmarkDetails
    }
}

but new generator tool gives me this:

public struct MessageWithThreadBookmarkDetails: MyLongCompileTimeLibrary.SelectionSet, Fragment {
  public let __data: DataDict
  public init(_dataDict: DataDict) { __data = _dataDict }

  public static var __parentType: ApolloAPI.ParentType { MyLongCompileTimeLibrary.Objects.Message }
  public static var __selections: [ApolloAPI.Selection] { [
    .field("__typename", String.self),
    .field("bookmark", Bookmark?.self),
    .fragment(MessageDetails.self),
  ] }

  /// set only if the message is being followed or the user is an active participant in the thread, only for home threads
  public var bookmark: Bookmark? { __data["bookmark"] }
  /// used by clients to embed custom metadata in the message, unused by backend
  public var metadata: String? { __data["metadata"] }
  /// ID (snowflake) of the message, https://github.com/sony/sonyflake
  public var id: String { __data["id"] }
  /// client provided ID for the message, unused by backend
  public var clientId: String? { __data["clientId"] }
  /// link previews attached message
  public var linkPreviews: [LinkPreview] { __data["linkPreviews"] }
  /// ID of the home this message belongs to
  public var homeId: MyLongCompileTimeLibrary.ID? { __data["homeId"] }
  /// ID of the room this message belongs to
  public var channelId: MyLongCompileTimeLibrary.ID { __data["channelId"] }
  /// ID of the parent message, only set for replies/comments
  public var threadId: String? { __data["threadId"] }
  /// profile preview of the message creator
  public var profilePreview: ProfilePreview { __data["profilePreview"] }
  /// markup text version of the message content
  /// "foo @ <@jdoe-user-id> <@here> <@room> &amp; bar - foo"
  @available(*, deprecated, message: "Not used")
  public var text: String? { __data["text"] }
  /// title of the message, only for posts in a forum room
  public var title: String? { __data["title"] }
  /// plain text version of the message content, used for notifications and message previews
  @available(*, deprecated, message: "Not used")
  public var plaintext: String? { __data["plaintext"] }
  /// content blocks attached message
  public var contentBlocks: [ContentBlock]? { __data["contentBlocks"] }
  /// if embedding an event, or a meetup was assigned to this message, includes the embedded event/meetup
  public var event: Event? { __data["event"] }
  /// if embedding a poll includes the embedded poll
  public var poll: Poll? { __data["poll"] }
  /// if embedding files, includes the embedded files
  public var files: [File]? { __data["files"] }
  /// if set then the message is an audio message, points to the file (in files) that represents the audio message
  public var audioMessageFileId: MyLongCompileTimeLibrary.ID? { __data["audioMessageFileId"] }
  /// if embedding giphys, includes the embedded giphys
  public var giphys: [Giphy]? { __data["giphys"] }
  /// list of mentions in the message, i.e. user or room
  public var mentions: Mentions? { __data["mentions"] }
  /// if edited includes information on who edited the message
  public var edited: Edited? { __data["edited"] }
  /// if pinned includes information on who pinned the message
  public var pinned: Pinned? { __data["pinned"] }
  /// DateTime (RFC3339) when the message was created
  public var createdAt: String { __data["createdAt"] }
  /// DateTime (RFC3339) when the message was updated
  public var updatedAt: String { __data["updatedAt"] }
  /// if deleted includes information on who deleted the message
  public var deleted: Deleted? { __data["deleted"] }
  /// the action that triggered the last update
  public var updateActionType: GraphQLEnum<MyLongCompileTimeLibrary.MessageUpdateActionType> { __data["updateActionType"] }
  /// number of replies/comments to this message
  public var replyCount: Int { __data["replyCount"] }
  /// list of profile IDs for users that replied to/commented on the message/post
  public var replyProfileIds: [MyLongCompileTimeLibrary.ID] { __data["replyProfileIds"] }
  /// list of reactions and associated counts
  public var reactionCounts: [ReactionCount] { __data["reactionCounts"] }
  /// true if an automated message created by Geneva
  public var isPlatformMessage: Bool { __data["isPlatformMessage"] }
  /// list of URLs to suppress link previews for
  public var linkUrlsToHide: [String]? { __data["linkUrlsToHide"] }
  /// permalink for the message, points to the webapp
  public var permalink: String { __data["permalink"] }
  /// if the message has suggested actions detected within the text, will include metadata about the action
  /// that can be taken.
  public var suggestedActions: [SuggestedAction]? { __data["suggestedActions"] }
  /// the message that this message is a quick reply to
  public var inlineMessage: InlineMessage? { __data["inlineMessage"] }
  public var activityStatus: [ActivityStatus]? { __data["activityStatus"] }

  public struct Fragments: FragmentContainer {
    public let __data: DataDict
    public init(_dataDict: DataDict) { __data = _dataDict }

    public var messageDetails: MessageDetails { _toFragment() }
  }

  /// Bookmark
  ///
  /// Parent Type: `ThreadBookmark`
  public struct Bookmark: MyLongCompileTimeLibrary.SelectionSet {
    public let __data: DataDict
    public init(_dataDict: DataDict) { __data = _dataDict }

    public static var __parentType: ApolloAPI.ParentType { MyLongCompileTimeLibrary.Objects.ThreadBookmark }
    public static var __selections: [ApolloAPI.Selection] { [
      .field("__typename", String.self),
      .fragment(ThreadBookmarkDetails.self),
    ] }

    /// ID of the room
    public var roomId: MyLongCompileTimeLibrary.ID { __data["roomId"] }
    /// ID of the thread, parent message ID
    public var threadId: MyLongCompileTimeLibrary.Snowflake { __data["threadId"] }
    /// ID of the last message in the thread
    public var lastSentMessageId: MyLongCompileTimeLibrary.Snowflake { __data["lastSentMessageId"] }
    /// ID of the last thread message viewed by the user, set by calling updateThreadBookmark
    public var lastViewedMessageId: MyLongCompileTimeLibrary.Snowflake? { __data["lastViewedMessageId"] }
    /// true if there are unread messages in the thread
    public var hasNewMessages: Bool { __data["hasNewMessages"] }

    public struct Fragments: FragmentContainer {
      public let __data: DataDict
      public init(_dataDict: DataDict) { __data = _dataDict }

      public var threadBookmarkDetails: ThreadBookmarkDetails { _toFragment() }
    }
  }

  /// LinkPreview
  ///
  /// Parent Type: `LinkPreview`
  public struct LinkPreview: MyLongCompileTimeLibrary.SelectionSet {
    public let __data: DataDict
    public init(_dataDict: DataDict) { __data = _dataDict }

    public static var __parentType: ApolloAPI.ParentType { MyLongCompileTimeLibrary.Objects.LinkPreview }

    /// display title of the link, og:title
    public var title: String { __data["title"] }
    /// description of the link, og:description
    public var description: String? { __data["description"] }
    /// url of the link
    public var url: String { __data["url"] }
    /// type of link, most common types are: image, video, rich.
    public var linkType: String { __data["linkType"] }
    /// name of the provider/host of the link, ie ???
    public var providerName: String? { __data["providerName"] }
    /// url of the home page for the link provider/host
    public var providerUrl: String? { __data["providerUrl"] }
    /// link preview image thumbnail
    public var thumbnail: Thumbnail? { __data["thumbnail"] }
    /// link preview image
    public var image: Image? { __data["image"] }
    /// link preview interactive embed html
    public var media: Media? { __data["media"] }

    public struct Fragments: FragmentContainer {
      public let __data: DataDict
      public init(_dataDict: DataDict) { __data = _dataDict }

      public var linkPreviewDetails: LinkPreviewDetails { _toFragment() }
    }

    /// LinkPreview.Thumbnail
    ///
    /// Parent Type: `LinkPreviewImage`
    public struct Thumbnail: MyLongCompileTimeLibrary.SelectionSet {
      public let __data: DataDict
      public init(_dataDict: DataDict) { __data = _dataDict }

      public static var __parentType: ApolloAPI.ParentType { MyLongCompileTimeLibrary.Objects.LinkPreviewImage }

      /// URL of the link preview image
      public var url: String { __data["url"] }
      /// height of the image in pixels
      public var height: Int { __data["height"] }
      /// width of the image in pixels
      public var width: Int { __data["width"] }

      public struct Fragments: FragmentContainer {
        public let __data: DataDict
        public init(_dataDict: DataDict) { __data = _dataDict }

        public var linkPreviewImageDetails: LinkPreviewImageDetails { _toFragment() }
      }
    }

    /// LinkPreview.Image
    ///
    /// Parent Type: `LinkPreviewImage`
    public struct Image: MyLongCompileTimeLibrary.SelectionSet {
      public let __data: DataDict
      public init(_dataDict: DataDict) { __data = _dataDict }

      public static var __parentType: ApolloAPI.ParentType { MyLongCompileTimeLibrary.Objects.LinkPreviewImage }

      /// URL of the link preview image
      public var url: String { __data["url"] }
      /// height of the image in pixels
      public var height: Int { __data["height"] }
      /// width of the image in pixels
      public var width: Int { __data["width"] }

      public struct Fragments: FragmentContainer {
        public let __data: DataDict
        public init(_dataDict: DataDict) { __data = _dataDict }

        public var linkPreviewImageDetails: LinkPreviewImageDetails { _toFragment() }
      }
    }

    /// LinkPreview.Media
    ///
    /// Parent Type: `LinkPreviewHtml`
    public struct Media: MyLongCompileTimeLibrary.SelectionSet {
      public let __data: DataDict
      public init(_dataDict: DataDict) { __data = _dataDict }

      public static var __parentType: ApolloAPI.ParentType { MyLongCompileTimeLibrary.Objects.LinkPreviewHtml }

      /// raw html
      public var html: String { __data["html"] }
      /// height of the interactive embed
      public var height: Int? { __data["height"] }
      /// width of the interactive embed
      public var width: Int? { __data["width"] }

      public struct Fragments: FragmentContainer {
        public let __data: DataDict
        public init(_dataDict: DataDict) { __data = _dataDict }

        public var linkPreviewHtmlDetails: LinkPreviewHtmlDetails { _toFragment() }
      }
    }
  }

  /// ProfilePreview
  ///
  /// Parent Type: `ProfilePreview`
  public struct ProfilePreview: MyLongCompileTimeLibrary.SelectionSet {
    public let __data: DataDict
    public init(_dataDict: DataDict) { __data = _dataDict }

    public static var __parentType: ApolloAPI.ParentType { MyLongCompileTimeLibrary.Objects.ProfilePreview }

    public var id: MyLongCompileTimeLibrary.ID { __data["id"] }
    /// the user's first name
    public var firstName: String { __data["firstName"] }
    /// the user's last name
    public var lastName: String? { __data["lastName"] }
    /// globally unique username
    public var username: String { __data["username"] }
    /// location of the user as a string populated when their location is public
    public var location: String? { __data["location"] }
    /// the user's default profile color, used when there is no profile picture
    public var defaultColor: String { __data["defaultColor"] }
    /// the user's profile picture
    public var picture: Picture? { __data["picture"] }
    public var deletedAt: MyLongCompileTimeLibrary.DateTime? { __data["deletedAt"] }

    public struct Fragments: FragmentContainer {
      public let __data: DataDict
      public init(_dataDict: DataDict) { __data = _dataDict }

      public var profilePreviewDetails: ProfilePreviewDetails { _toFragment() }
    }

    /// ProfilePreview.Picture
    ///
    /// Parent Type: `File`
    public struct Picture: MyLongCompileTimeLibrary.SelectionSet {
      public let __data: DataDict
      public init(_dataDict: DataDict) { __data = _dataDict }

      public static var __parentType: ApolloAPI.ParentType { MyLongCompileTimeLibrary.Objects.File }

      public var id: MyLongCompileTimeLibrary.ID { __data["id"] }
      /// filename of the file, eg example.txt
      public var filename: String { __data["filename"] }
      /// mimetype of the file, eg image/png
      public var mimetype: String { __data["mimetype"] }
      /// s3 bucket the file is stored in
      public var s3Bucket: String { __data["s3Bucket"] }
      /// s3 key of the file in storage
      public var s3Key: String { __data["s3Key"] }
      /// size of the file in bytes
      public var size: Int { __data["size"] }
      /// image metadata, only for image files
      public var imageInfo: ImageInfo? { __data["imageInfo"] }
      /// video metadata, only for video files
      public var videoInfo: VideoInfo? { __data["videoInfo"] }
      /// audio metadata, only for audio files
      public var audioInfo: AudioInfo? { __data["audioInfo"] }

      public struct Fragments: FragmentContainer {
        public let __data: DataDict
        public init(_dataDict: DataDict) { __data = _dataDict }

        public var fileDetails: FileDetails { _toFragment() }
      }
      ...

Looks like that it reattach all embeded properties instead of keeping it in shared place, and idea how this might work in better way in 1.x and use same as previously inheritance from parent ?

We have many models where we are adding some properties to base model.

And comparing numer of lines generated previously in single file (it is around 65k in total), where new generator gives many files with aournd 22k files per file which is crazy.

I love the idea of splitting to different files but wondering how i can solve my problem

AnthonyMDev commented 1 month ago

We are currently working on #2560 to allow you to disable fragment field merging into operation models. Once that is done, you'll be able to make your generated files a lot smaller. But even with those large files, the compilation times you're seeing feel way higher than I'd anticipate. Maybe that will solve it, but I'm wondering if there is something else going on here as well.

m1entus commented 1 month ago

@AnthonyMDev If you want i can share with you sample app with some part of fragments generated code, preety lightweight but still it compile a large amount of time, around ~5-10 minutes lol