Authress-Engineering / openapi-explorer

OpenAPI Web component to generate a UI from the spec.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Performance drop for extremely large specs with broken references

Xarno opened this issue · comments

We have quite a big openapi file and one modification to our documentation tags leads to a large number of additional Schemas to get included in the resulting file. That is correct behavior in the file generatation. But now the openapi explorer has trouble to load this file. It takes „forever“ evaluating the circular references. I tried it in Chrome, Edge, Firefox and Safari.

I sorted the openapi file to compare it with a diff tool with our previous version but only found the newly added Schemas and the changed endpoint.

I attach 3 versions of our openapi file to help investigate the problem.
The file openapi_unuseable.json is contains all generated json, just sorted. This will not render in a useable time at all.
In the file openapi_slowdown.json I have removed some internal ref via search and replace. It renders, but has a visible slowdown where the browser shows the loading indicator.
In the files openapi_normal_speed I have removed one more internal ref, for a complex type Product, than in the slowdown file. With this change the loading time is instant as I would expect.

openapi_unuseable.json
openapi_slowdown.json
openapi_normal_speed.json

For posterity:

  • The spec is fully dereferenced on load. This means that every referenced is crawled so that we can create a complete spec. The problem with these specific specs that there are very deeply nested trees of linked schemas. So it just takes a very very long time to crawl through each of these. A links to [B, C, D, E] B links to [A, C, D, E], and so on. So for a spec of of maximum references with 6 internal references schemas, that's a tree of depth 7. Or 49 schemas. And now this is done for each of the thousands of routes creates a huge problem.
  • We don't necessarily need to dereference the whole schema on load. We only need 95% of it to render things. The schema themselves for the request body, headers, response body, etc... does not need to be resolved for the whole spec, only for the one specific path we are looking at.
  • There is going to need to be some special casing to handle the Schema section which allows it to be dynamic.
  • The problem is actually present in a join issue with the Open API Resolver which has been the best we've found to handle this. We could make some serious changes there to support JIT schema rendering, which would be necessary for very large specs.

For this specific problem

On this specific topic, I'm not sure what the best solution is here. Fundamentally there are just too many deeply linked schemas, which is not exactly the norm. It's still spec compliant, just not usual. For example, in here there are ~90 tags, that's a lot and the spec is almost 4MB. Again, fully supported, just a huge amount of data, which of course takes time to crawl through.

Realistically the best case solution is pre-processing. Every time your spec loads it needs to be crawled for every user. So even with significant changes, in reality this is always going to be slow with every open api spec renderer. I don't know what the reusability of schema's between paths or tags is, but if it were possible to break this down into tag specific chunks ahead of time, then that means that the user sitting at the window wouldn't have to wait for the processing.

The library that does the processing is this one: openapi-resolver.js I would expect one to be able to call the resolver at build time, and deploy a pre-processed spec. Then since the spec has already been processed, it would be cached for all the viewers of spec and offer great improvements on speed there.


For this reason I will leave the ticket open as this sort of improvement or at least documentation on the problem is important to have.

Hello again,

thank you for looking into it. I am sorry, I put the blame to fast on your renderer.
I have found the real reason behind that big circular problem. In our code was one place, where not external data transfer objects were referenced, but internal Entity objects. And those are heavily connected by design.
I created the missing data transfer objects, generated a new openapi file and that one renders fast.

100k lines or 300 schemas or 4mb (a big chunk are spaces for pretty print) are no problem for the renderer. Only the circuclar references.

Awesome. If you could share with us what change you made here, it might be possible for us to catch this/display an error so that other people don't run into this same problem.

Let's see. We have Swagger Annotations in the code. And on the schema element in the example at the end it was
schema = Schema(type = "object-Encoding =UTF-8", implementation = JobOfferDetailWebserviceLogic.Response::class)

That Response Class was defined as follows:

class Response : AbstractResponse() {
        lateinit var jobOffer: JobOffer // <- Internal Entity Type
        var mediaBindings: List<JobOfferMediaBinding>? = null // <- Internal Entity Type
        var personBindings: List<JobOfferPersonBinding>? = null // <- Internal Entity Type
        var linkBindings: List<JobOfferLinkBinding>? = null // <- Internal Entity Type
        lateinit var rootCategories: Array<String>
        var showCategoryHierarchy = false
}

The openapi generator has had no problem to write all this, but the internal types have references which are not needed for the response. So this definition of the Response was a clear bug and violation of our way of work.

Now the Schema is on ApiJoboffer which is a simple Object which has only Strings, Booleans, Lists and nested Objects.
So thas easy to deal with in the renderer.

data class ApiJoboffer(
    val id: String,
    val name: String,
    val pictureUrl: String,
    val location: String,
    val employmentType: String, 
    val careerLevel: String, 
    val startdate: String,
    val homeOffice: Boolean,
    val description: ApiDescriptionObject,
    val requirements: String, 

    val organization: OrganizationApiObjects.ApiOrganizationVcard,

    val persons: List<ApiJobofferPerson>,

    val attachments: List<ApiAttachment>,
)
…
@Operation(
        summary = "Get Joboffer detail",
        description = "Get the detailed information of a Joboffer by id. </br>Permissions: no Permission",
        requestBody = RequestBody(
            description = "HttpRequest", content = [Content(
                mediaType = MediaType.APPLICATION_FORM_URLENCODED,
                schema = Schema(name = "payload", type = "Encoding =UTF-8")
            )]
        ),
        responses = [ApiResponse(
            responseCode = "200", description = "successful operation", content = [Content(
                mediaType = MediaType.APPLICATION_JSON,
                schema = Schema(type = "object-Encoding =UTF-8", implementation = ApiJoboffer::class),
            )]
        ), ApiResponse(responseCode = "400", description = "Invalid key supplied"),
            ApiResponse(responseCode = "404", description = "topic not found, joboffer not found")
        ]
    )
    @GET
    @Throws(
        ServletException::class, IOException::class
    )
    public override fun doGet(request: HttpServletRequest, response: HttpServletResponse) {
        …
        logic.writeResponse(request, response, apiVersionToUse)
    }

Wow, thanks for that. Would it be possible to also include the "correct spec" in here, so that we can do a comparison and potentially use that to optimize or identify problems in the future?

Thanks a lot, and I'm glad you figured this out.

Thats the working file with the correct ApiObjects.
openapi.json

Another slow spec. The problem for this one, is actually in the openapi-resolver:
index-schema.json