Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement codegen configuration for field merging #2560

Closed
pm-dev opened this issue Oct 10, 2022 · 53 comments
Closed

Implement codegen configuration for field merging #2560

pm-dev opened this issue Oct 10, 2022 · 53 comments
Assignees
Labels
codegen Issues related to or arising from code generation feature New addition or enhancement to existing solutions

Comments

@pm-dev
Copy link

pm-dev commented Oct 10, 2022

Bug report

My team was excited to upgrade to the 1.0 release (thanks for all the hard work), unfortunately we've been unable to get codegen to work. Pre-v1.0 we used ApolloCLI from ApolloCodegenLib to shell out to the js codegen tool and it would take ~5 seconds to complete. After updating to 1.0 and swift-native codegen, we're able to compile and run, but codegen never finishes. It's able to find our schema and .graphql queries, and about half our queries (20 or so) are generated immediately, but then codegen starts to take minutes per query and the memory the process is using grows to multiple GBs.

Versions

Please fill in the versions you're currently using:

  • apollo-ios SDK version: 1.0
  • Xcode version: 14.0
  • Swift version: 5.7
  • Package manager: SPM

Steps to reproduce

I would gladly provide our schema and queries if there was a confidential way to share those files so we could debug this together.

I've tried playing around with the configuration a bit to see if any of these options make a difference, but no luck. Here's an example of what it looks like:

    let configuration = ApolloCodegenConfiguration(
        schemaName: "GraphQLTypes",
        input: ApolloCodegenConfiguration.FileInput(
            schemaPath: schemaFileURL.relativePath,
            operationSearchPaths: [
                projectDirectoryURL.relativePath + "/iOS/**/*.graphql",
                projectDirectoryURL.relativePath + "/iOS/**/*.graphql"
            ]
        ),
        output: ApolloCodegenConfiguration.FileOutput(
            schemaTypes: ApolloCodegenConfiguration.SchemaTypesFileOutput(
                path: projectDirectoryURL.relativePath + "/iOS/Libraries/GraphQL/GraphQLTypes/Generated Types",
                moduleType: .swiftPackageManager
            ),
            operations: .inSchemaModule,
            testMocks: .none,
            operationIdentifiersPath: operationsURL.relativePath
        ),
        options: ApolloCodegenConfiguration.OutputOptions(
            queryStringLiteralFormat: .multiline,
            deprecatedEnumCases: .include,
            schemaDocumentation: .include,
            apqs: .persistedOperationsOnly,
            cocoapodsCompatibleImportStatements: false,
            warningsOnDeprecatedUsage: .include,
            pruneGeneratedFiles: true
        )
    )
    try ApolloCodegen.build(with: configuration, withRootURL: workspaceRootURL)

I'll do a little more profiling and can update this thread with anything I find. I've already noticed the script spends a lot of time in the mergeIn function. Also the generated files are on average have 10x more lines than previously (query strings have always been multi-line so that's likely not the difference) is that expected?

@AnthonyMDev
Copy link
Contributor

Thanks for the report @pm-dev!

Then codegen starts to take minutes per query and the memory the process is using grows to multiple GBs.

It's likely that your operations are hitting some sort of edge case that is causing some serious issues here.

Also the generated files are on average have 10x more lines than previously (query strings have always been multi-line so that's likely not the difference) is that expected?

This definitely should not be the case. The new codegen is expected to be 10x smaller than the old codegen actually.

It's likely that you are doing some very complex fragment merging. We should be able to handle this, and all of our tests thus far have shown positive results. As far as the size of generated code exploding this is likely the cause, and I already intend to fix that with #2191 (coming as soon as possible!).

I would gladly provide our schema and queries if there was a confidential way to share those files so we could debug this together.

But it should not be hanging during codegen... that's for sure. If you can send us some examples to debug this with, that would be incredible. Please reach out to me at [email protected] and we can talk about how to do that.

@AnthonyMDev AnthonyMDev added bug Generally incorrect behavior codegen Issues related to or arising from code generation labels Oct 11, 2022
@AnthonyMDev AnthonyMDev self-assigned this Oct 11, 2022
@pm-dev
Copy link
Author

pm-dev commented Oct 11, 2022

Thanks for the reply. I've sent over a project which will allow you to repro.

@AnthonyMDev
Copy link
Contributor

Received! I'll look into this today and update you ASAP! I'll also ping you if I need any other info.

@calvincestari calvincestari added this to the Next Release (1.0.1) milestone Oct 11, 2022
@AnthonyMDev
Copy link
Contributor

@pm-dev Just wanted to give you an update. Profiling with your schema and queries has been really helpful. None of the tests we've run have had so many queries with so many nested fragments. The nested fragments are causing a lot of interesting problems.

We've had teams at large companies testing the code gen, but none of them had the amount of nested fragments that you do right now.

As far as memory goes, we've got a few memory leaks, but its more so that the memoization of processing nested fragments is going crazy with your operations.

For the processing time... that's an even harder one to solve. Because of the nested fragments, we are doing a lot of calculations for the merging of fields. We've done a lot to optimize that (see memoization above for one example, haha). But it's just a lot to process. I was concerned that this might be an issue for some users, but we've had some pretty big projects run codegen just fine. Yours is the first that is causing this to happen. I'm going to continue to dig into this and see if I can find some clever ways to make this better.

Because of the merging of fragment fields, the generated code is getting pretty huge. The concept of "hoisted types" that I hope to implement in #2191 will recognize when we can re-use an already generated type because it will have the same merged fields. I'm not sure if that will cause running the code gen to go slower or faster. I'm going to explore that soon.

We have a couple of high-priority bugs that are blocking a lot of other users who don't have the complex nested fragments that you have. (This situation I expect to be relatively rare). So we are going to work on getting those out and a 1.0.1 patch version released to un-block people. I will come back to this and resolve all these issues as soon as we are done there.

I'll let you know when I have more to report.

@pm-dev
Copy link
Author

pm-dev commented Oct 12, 2022

@AnthonyMDev Thanks for the update!
We certainly have a lot of nested fragments, which I consider a gql anti-pattern. For context, our app switched from REST to GQL while keeping the same data-model shared across our app. This meant we used fragments that could then be translated 1:1 into our existing model objects.

The prioritization of items for the 1.0.1 patch makes total sense. When you get back to this let me know how I can help. I'm a little curious the difference in "algorithm" vs the previous js codegen so in the meantime I'll get caught up on the difference in approach for v1

@AnthonyMDev
Copy link
Contributor

I don't really know that this is an anti-pattern. It makes sense why you are using them for your use case, and for many others honestly.

The biggest difference here is the way we are merging fragment fields into the generated operation objects. We wanted to make it easier for you to access fragment fields from the generated models without having to convert back and forth from operation model <-> fragment when you wanted to access fields from other fragments. This improves ease of use, but does generate more field accessors on each model.

Because we made the generated code for each field more concise (1 line per field instead of 5), this reduces generated code size in most cases. But in your case, the number of merged fields is exploding the generated code, causing massive processing times and massive generated files.

We're working on improvements to this, but in 1.1, it's likely we also give you the option to just turn off fragment field merging. You'll only be able to access the fields from a given fragment by converting to that fragment, but your generated code will be more reasonably sized.

@pm-dev
Copy link
Author

pm-dev commented Oct 12, 2022

Ah this makes sense, we're currently not merging fragment fields. Although merging in fields makes working with the data types more fluid, for our use-case where we are immediately translating GQL types into a different data model (and thus only accessing each field in one place), it's not that big of a deal.

@AnthonyMDev
Copy link
Contributor

Yeah, one of the primary goals here was that you wouldn't need to translate the generated models into your own data models if they are easier to use! But if you are translating to REST models, this makes perfect sense.

Once I add in that functionality, you'll have considerably smaller models. ;) Will be getting to it ASAP.

I'm am on vacation at the end of this month, so I'm going to try to get there before I leave, otherwise it will be done sometime in November...

@AnthonyMDev AnthonyMDev changed the title Codegen doesn't complete with v1.0 release Implement configuration option for merging fragment fields Oct 13, 2022
@calvincestari
Copy link
Member

+1 for this from @jimisaacs in #2625

@kylebrowning
Copy link

+1 from me, this is currently stopping us from migrating to 1.0

@tahirmt
Copy link
Contributor

tahirmt commented Jan 6, 2023

Curious what the plan is to add this option back or any ETA

@AnthonyMDev
Copy link
Contributor

This is planned to be one of the next large stories I take on! We have been fixing a bunch of smaller blockers, and then the holidays hit. There are a few large-ish features I want to add in versions 1.1-1.3, this is #3 on that list.
This is a rough estimate of our current list of goals. This is not set in stone and is subject to changes.

1.1: Generated operation model creation

  • The ability to initialize fragment (and maybe selection set) models in a type-safe way.
  • Initialize mutable selection sets to add to the cache via local cache mutations. (Currently you can only mutate fields on existing entities)
  • Create API for clearing individual fields on entities from the cache in local cache mutations.

1.2: Reduce generated schema types

  • Right now we are naively generating schema types that we don't always need. A smarter algorithm can reduce generated code for certain large schemas that are currently having every type in their schema generated.
  • Create configuration for manually indicating schema types you would like to have schema types and TestMocks generated for.

1.3: Improve fragment merging and code generation performance

  • Add configuration for disabling merging of fragment fields.
  • Recognize when multiple selection set types will end up being identical and use a shared model object with typealiases to reduce generated code.
  • Fix retain cycles and memory issues causing code generation to take very long on certain large, complex schemas with deeply nested fragment composition.
  • Optimize code generation performance by parallelizing computation and rendering of files.

@tahirmt
Copy link
Contributor

tahirmt commented Apr 16, 2024

From our team while we do have selectionSetInitializers option enabled, we don't have any uses of the initializers outside of tests and even then we use mocks more and can easily replace the uses that we have currently. I'm assuming this won't affect the initializer with JSONObject as we do use that extensively in our tests to initialize GraphQL objects using raw json files.

@scottasoutherland
Copy link

We do not use selectionSetInitializers. We initialized our usages of direct initializations due in early apollo 1.0 and never added them back.

@pm-dev
Copy link
Author

pm-dev commented Apr 24, 2024

We do use selection set initializers in tests but could switch to passing JSONObject. fwiw I just retried codegen again on my project with the latest version, wondering if this perf improvement fixed the bottleneck. It did not. So, still waiting on this fix in order to update to v1.

@AnthonyMDev
Copy link
Contributor

@pm-dev Yeah, I actually ran a test after that PR against the old example you had sent me. It did help, but it's not enough. When I finish this, you should be able to get it working though.

That perf improvement helped me to diagnose where the bottleneck is though in your large schema. I have a few ideas for some other algorithmic improvements that could unblock this. But even if we got it processing, the way your operations/fragments are set up, you would end up with massively oversized generated models. You're going to want to turn a lot of field merging off anyways.

@Mordil
Copy link
Contributor

Mordil commented May 29, 2024

We use selectionSetInitializers extensively in tests and in some of our production code due to how we have to do some cache manipulation shenanigans.

This is currently a big problem for us because we've identified that the actual binary size is bloated due to us moving from the prior version of Apollo to 1.x back at the start of the year. We used to have fields un-merged (forcing access through Fragments) and now that option is gone.

We saw a near 20mb increase in our app size that we didn't notice until recently.

@AnthonyMDev
Copy link
Contributor

@Mordil Yes, the increase in binary size is the primary reason people want the turn off merging.

Unfortunately, there isn't a way to have both selectionSetInitializers and disable fragment field merging in the same selection set. I even looked at the old legacy code generator and when field merging was disabled, selection set initializers were not generated for any type in which the calculated merged fields would be needed. It only generated initializers for types that only contained scalar fields.

Once this feature is shipped, if you want to disable field merging, you're going to need to either initialize using raw JSON directly (unsafely) or use the Test Mocks.

I have been considering a future iteration of this where we allow field merging to be enabled for fragments, but disabled for operations, which could allow you to use initializers on your fragments, but it's still unclear to me if this would really allow for the flexibility needed for the "cache manipulation shenanigans" you referred to.

@Mordil
Copy link
Contributor

Mordil commented May 29, 2024

@AnthonyMDev Allowing it on the fragments but not on the operations would work for us

@tahirmt
Copy link
Contributor

tahirmt commented Jul 12, 2024

@AnthonyMDev it's been a while since there's been any updates on this. I see there are 5 open pull requests but they have also not been updated in a while. Is there any update on any timelines?

@AnthonyMDev
Copy link
Contributor

Yeah, I got far along on this, but had to reprioritize to get Swift 6 compatibility done before the stable release of Swift 6. I'll see if I can find some time to get this over the finish line soon, but need to balance with other priorities.

@AnthonyMDev
Copy link
Contributor

I have a new apollographql/apollo-ios-dev#431 up that I hope to be a working implementation! Please feel free to test this against your projects for me and report any bugs. We'll hope to release this an experimental in the coming weeks. (Assuming no major issues are found in testing).

@pm-dev
Copy link
Author

pm-dev commented Jul 16, 2024

@AnthonyMDev I'd like to test out the branch, however I use SPM to integrate libs and the apollo-ios-dev repo doesn't have a Package.swift.

@tahirmt
Copy link
Contributor

tahirmt commented Jul 17, 2024

@AnthonyMDev as far as my early testing goes, it looks good. I was able to generate code with our schema into a building project and tried out one of those queries to get the result as well. I have not tested fully yet so I will report issues later if there are any.

@pm-dev you can find the Package.swift file under each library folder. It works if you check out the repo locally and link locally directly with the package. For example https://github.com/apollographql/apollo-ios-dev/blob/main/apollo-ios-codegen/Package.swift

@calvincestari
Copy link
Member

@AnthonyMDev I'd like to test out the branch, however I use SPM to integrate libs and the apollo-ios-dev repo doesn't have a Package.swift.

@pm-dev - our development workflow with apollo-ios-dev doesn't push corresponding branches to the subtrees until a PR is merged, which is why you aren't seeing the same branch in apollo-ios. It can be done manually though if needed, cc. @AnthonyMDev.

The suggestion from @tahirmt will work for you too though if you want to clone apollo-ios-dev and switch to a local apollo-ios dependency for your project.

@AnthonyMDev
Copy link
Contributor

A preview release that includes this feature has just been published!

Even when not using the new disabling of fragment field merging, we have made some improvements that will likely decrease the size of your generated models for users with complex, deeply nested fragments (which I expect the be the large majority of users who have been requesting this feature).

Please try out the preview release, and consider running the code generation without disabling field merging and see how much of a difference that makes before you decide to disable it!

Thank you all for your patience and the bug reports I've gotten while working on this feature.

If this preview release does not surface new bugs, we anticipate to release this work into a stable release of Apollo iOS very soon!

@AnthonyMDev
Copy link
Contributor

I'd really love to get some more feedback on this preview release before we go on to the public release of this. Has anyone here had a chance or is anticipating getting a chance to test this out this week?

@tahirmt, @Mordil, @scottasoutherland, @kylebrowning?

@tahirmt
Copy link
Contributor

tahirmt commented Aug 7, 2024

I haven't ran it since my initial testing a couple weeks ago. I was able to use our app without seeing any issues. I see that the preview release only has the CLI tool but no code changes here so I can't switch everything to this tag and have more people's eyes on it. I cannot use the CLI tool for our schema I have to use the library. I

@AnthonyMDev
Copy link
Contributor

Thanks @tahirmt. There have been some significant changes since then that it would be valuable for me to get some verification on if you have time!

There is a tag for the preview release in the apollo-ios-codegen repo. You should be able to switch your dependency on ApolloCodegenLib to use the branch preview-field-merging.1 and then re-run your codegen. It should compile and work properly with the current stable release version of Apollo. Let me know if that works for you!

@tahirmt
Copy link
Contributor

tahirmt commented Aug 7, 2024

@AnthonyMDev that's great information! I can try to work on it this week and get some more people's hands on it or at least run automation test suites on the builds. I had completely forgotten it was a different repo.

@AnthonyMDev
Copy link
Contributor

Thank you! Ideally, it should be as easy as pointing at the new branch and running your codegen script. As long as all your code compiles and your tests pass, then we're good. It shouldn't need much of a migration of your code.

There was one rare bug with queries using @include/@skip that was fixed. If you were exposed to that bug, you may need to fix a few of your call sites, but otherwise I'm hoping everything just works and that your generated code is a lot smaller now!

@Mordil
Copy link
Contributor

Mordil commented Aug 7, 2024

I won't be able to test soon, due to day-time job priorities, and the effort for us to test is a bit higher since we use a forked version of the codegen library. Apologies!

@tahirmt
Copy link
Contributor

tahirmt commented Aug 8, 2024

@AnthonyMDev I am still testing but I was able to run codegen and compile our project. Ran through the application and haven't encountered any issues there. I still have to update our unit test suite because there's quite a bit of use of selection set initializers currently. I will post back if I encounter any issues.

@tahirmt
Copy link
Contributor

tahirmt commented Aug 9, 2024

@AnthonyMDev after running unit tests and manually testing I am fairly confident there are no issues with this change with our schema. There was one instance where I had some code change resulting from our usage of @skip and @include but it was trivial. All in all a huge reduction of generated code.

@tahirmt
Copy link
Contributor

tahirmt commented Aug 9, 2024

I have some additional stats to share. Here is the difference from turning off field merging and selection set initializers to before in our schema. I used the new preview as my starting point to compare how much additional lines of code it is for each feature.

I had to double check these numbers because of the difference in lines of code in 1.14.1 vs the preview but these do indeed seem to be correct. It also explains the size difference in 1.x vs 0.5x without field merging.

Apollo Codegen Configuration LOC difference
preview-field-merging.1 Field merging, selection set initializers +0LOC
preview-field-merging.1 Field merging, selection set initializers +188,107 LOC
preview-field-merging.1 Field merging, selection set initializers +101,314 LOC
1.14.1 Field merging, selection set initializers +2,093,962 LOC

@AnthonyMDev
Copy link
Contributor

This is awesome! Thanks so much @tahirmt! Looks like we should be able to ship this very soon!

@martin-muller
Copy link

I can see many nested fragments being type aliased in our project with 566 .graphql.swift files. Only minor refactoring needed and all tests pass, looks like a great change!

@tahirmt
Copy link
Contributor

tahirmt commented Aug 20, 2024

@AnthonyMDev I think we can close this issue now with 1.15.0 release.

Copy link
Contributor

Do you have any feedback for the maintainers? Please tell us by taking a one-minute survey. Your responses will help us understand Apollo iOS usage and allow us to serve you better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
codegen Issues related to or arising from code generation feature New addition or enhancement to existing solutions
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants