Skip to content

dont run contentNegotiationManager.resolveMediaTypes when single produces media type is specified#37004

Closed
gtiwari333 wants to merge 1 commit into
spring-projects:mainfrom
gtiwari333:dont-run-contentNegotiationManager.resolveMediaTypes-when-single-produces-media-type-is-specified
Closed

dont run contentNegotiationManager.resolveMediaTypes when single produces media type is specified#37004
gtiwari333 wants to merge 1 commit into
spring-projects:mainfrom
gtiwari333:dont-run-contentNegotiationManager.resolveMediaTypes-when-single-produces-media-type-is-specified

Conversation

@gtiwari333

Copy link
Copy Markdown
package com.example;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * Compares old vs new {@code getAcceptableMediaTypes()} in
 * {@code AbstractMessageConverterMethodProcessor}.
 * <p>
 * OLD: always creates a new ServletWebRequest and calls
 *      contentNegotiationManager.resolveMediaTypes() — which parses the
 *      Accept header, tokenizes MIME types, and allocates MediaType objects.
 * <p>
 * NEW: checks {@code PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE} first. If it contains
 *      exactly one concrete MediaType (the common case for a single
 *      {@code produces = "application/json"} endpoint), returns it directly
 *      without any parsing or allocation.
 */
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(2)
@State(Scope.Thread)
public class AcceptableMediaTypesBenchmark {

    // Simulates the PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE on the request.
    // Set<MediaType> with a single MediaType.APPLICATION_JSON — the common case
    // for @GetMapping(produces = "application/json").
    private static final Set<String> SINGLE_CONCRETE_PRODUCIBLE = Set.of("application/json");

    // Simulates an Accept header like "*/*" — the default for browsers and curl.
    // In the old path, ContentNegotiationManager parses this into a list of MediaType.
    // We simulate that cost.
    private static final String ACCEPT_HEADER = "*/*";

    // ---- Old: always resolve via ContentNegotiationManager ----

    @Benchmark
    public void old_alwaysResolve(Blackhole bh) {
        // Old code: unconditionally create ServletWebRequest + resolve
        List<String> result = resolveMediaTypes(new ServletWebRequest(ACCEPT_HEADER));
        bh.consume(result);
    }

    // ---- New: check attribute first, short-circuit on single concrete type ----

    @Benchmark
    public void new_shortCircuitOnSingleConcrete(Blackhole bh) {
        // New code: check PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE first
        Object attribute = SINGLE_CONCRETE_PRODUCIBLE;  // request.getAttribute(...)
        List<String> result;
        if (attribute instanceof Set<?> set && set.size() == 1) {
            Object only = set.iterator().next();
            if (only instanceof String s) {  // MediaType in real code, simplified here
                result = Collections.singletonList(s);
            } else {
                result = resolveMediaTypes(new ServletWebRequest(ACCEPT_HEADER));
            }
        } else {
            result = resolveMediaTypes(new ServletWebRequest(ACCEPT_HEADER));
        }
        bh.consume(result);
    }

    // ---- New: worst case — attribute not set (no @produces), falls through ----

    @Benchmark
    public void new_fallthroughNoAttribute(Blackhole bh) {
        Object attribute = null;  // no PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE set
        List<String> result;
        if (attribute instanceof Set<?> set && set.size() == 1) {
            Object only = set.iterator().next();
            if (only instanceof String s) {
                result = Collections.singletonList(s);
            } else {
                result = resolveMediaTypes(new ServletWebRequest(ACCEPT_HEADER));
            }
        } else {
            result = resolveMediaTypes(new ServletWebRequest(ACCEPT_HEADER));
        }
        bh.consume(result);
    }

    // ---- Baseline: what the short-circuit path actually does ----

    @Benchmark
    public void baseline_singletonList(Blackhole bh) {
        bh.consume(Collections.singletonList("application/json"));
    }

    // ---- Simulated ContentNegotiationManager.resolveMediaTypes ----

    static List<String> resolveMediaTypes(ServletWebRequest request) {
        // Simulates the real work: creates a new ServletWebRequest,
        // reads the Accept header, parses MediaType from it.
        // The real impl creates MediaType objects via MediaType.parseMediaTypes()
        // which calls MimeTypeUtils.tokenize() character-by-character.
        String acceptHeader = request.getHeader("Accept");
        return parseAcceptHeader(acceptHeader);
    }

    /**
     * Simulates MediaType.parseMediaTypes() — parses an Accept header
     * into a list of media type strings. In the real code this is much
     * heavier: tokenizing, parameter parsing, q-value sorting, and
     * MediaType object allocation.
     */
    static List<String> parseAcceptHeader(String accept) {
        List<String> result = new ArrayList<>();
        for (String part : accept.split(",")) {
            String[] typeSubtype = part.trim().split(";")[0].trim().split("/");
            if (typeSubtype.length == 2) {
                result.add(typeSubtype[0] + "/" + typeSubtype[1]);
            }
        }
        return result;
    }

    // ---- Simulated ServletWebRequest (simplified) ----

    static class ServletWebRequest {
        private final String acceptHeader;
        private final String contentType;

        ServletWebRequest(String acceptHeader) {
            this(acceptHeader, null);
        }

        ServletWebRequest(String acceptHeader, String contentType) {
            this.acceptHeader = acceptHeader;
            this.contentType = contentType;
        }

        String getHeader(String name) {
            if ("Accept".equalsIgnoreCase(name)) {
                return acceptHeader;
            }
            if ("Content-Type".equalsIgnoreCase(name)) {
                return contentType;
            }
            return null;
        }
    }

    // ---- Main ----

    public static void main(String[] args) throws Exception {
        Options options = new OptionsBuilder()
                .include(AcceptableMediaTypesBenchmark.class.getSimpleName())
                .addProfiler(org.openjdk.jmh.profile.GCProfiler.class)
                .shouldFailOnError(true)
                .shouldDoGC(true)
                .jvmArgs("-server")
                .build();

        new Runner(options).run();
    }
}

@gtiwari333 gtiwari333 closed this Jul 4, 2026
@gtiwari333 gtiwari333 deleted the dont-run-contentNegotiationManager.resolveMediaTypes-when-single-produces-media-type-is-specified branch July 4, 2026 15:35
@gtiwari333 gtiwari333 restored the dont-run-contentNegotiationManager.resolveMediaTypes-when-single-produces-media-type-is-specified branch July 4, 2026 15:35
@bclozel bclozel added the status: invalid An issue that we don't feel is valid label Jul 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: invalid An issue that we don't feel is valid

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants