I was looking at a flame graph of an ingestion workload with @dliappis, and it looks like we parse the JSON representation of the index request twice when a document has both a pipeline and a final pipeline.

It's hard to know how much we would save on this benchmark since not all data streams in this benchmark had both a pipeline and a final pipeline, but the cost of parsing index request into maps of maps via IngestService represented 4.06% of overall CPU time according to the flame graph, so this might not be negligible.
server/src/main/java/org/elasticsearch/ingest/IngestService.java
}
        });
    }
    private void executePipelines(
        final int slot,
        final Iterator<String> it,
        final boolean hasFinalPipeline,
        final IndexRequest indexRequest,
        final IntConsumer onDropped,
        final BiConsumer<Integer, Exception> onFailure,
        final ActionListener<Void> onFinished
    ) {
        assert it.hasNext();
        final String pipelineId = it.next();
        try {
            PipelineHolder holder = pipelines.get(pipelineId);
            if (holder == null) {
                throw new IllegalArgumentException("pipeline with id [" + pipelineId + "] does not exist");
            }
            Pipeline pipeline = holder.pipeline;
            String originalIndex = indexRequest.indices()[0];
            long startTimeInNanos = System.nanoTime();
            totalMetrics.preIngest();
            innerExecute(slot, indexRequest, pipeline, onDropped, e -> {
                long ingestTimeInNanos = System.nanoTime() - startTimeInNanos;
                totalMetrics.postIngest(ingestTimeInNanos);
                if (e != null) {
                    totalMetrics.ingestFailed();
                    logger.debug(
                        () -> format(