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/test/java/org/elasticsearch/ingest/IngestServiceTests.java
}
    }
    public void testIngestAndPipelineStats() throws Exception {
        final Processor processor = mock(Processor.class);
        when(processor.getType()).thenReturn("mock");
        when(processor.getTag()).thenReturn("mockTag");
        when(processor.isAsync()).thenReturn(true);
        
        doAnswer(args -> {
            @SuppressWarnings("unchecked")
            BiConsumer<IngestDocument, Exception> handler = (BiConsumer) args.getArguments()[1];
            handler.accept(RandomDocumentPicks.randomIngestDocument(random()), null);
            return null;
        }).when(processor).execute(any(IngestDocument.class), any());
        
        final Pipeline[] pipelineToReturn = new Pipeline[1];
        final IngestService pipelineIngestService = mock(IngestService.class);
        when(pipelineIngestService.getPipeline(anyString())).thenAnswer(inv -> pipelineToReturn[0]);
        IngestService ingestService = createWithProcessors(
            Map.of(
                "pipeline",
                (factories, tag, description, config) -> new PipelineProcessor(tag, description, (params) -> new TemplateScript(params) {
                    @Override
                    public String execute() {
                        return "_id3";
                    } 
                }, false, pipelineIngestService),
                "mock",
                (factories, tag, description, config) -> processor
            )