Authress-Engineering / openapi-explorer

OpenAPI Web component to generate a UI from the spec.
Apache License 2.0
292 stars 42 forks source link

Performance drop for extremely large specs with broken references #221

Open Xarno opened 5 months ago

Xarno commented 5 months ago

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

wparad commented 5 months ago

For posterity:

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.

Xarno commented 5 months ago

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.

wparad commented 5 months ago

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.

Xarno commented 5 months ago

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)
    }
wparad commented 5 months ago

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.

Xarno commented 5 months ago

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

wparad commented 5 months ago

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