Reduce the number of times capacity growth is needed inside the StringWriter. A typical default SpringBoot Prometheus page has more than 11k characters. Best performance results when no capacity growth is needed at all, so base it on previous metrics page size plus some room for possible extra metric info.

Background

Profiling our SpringBoot application showed time spend in the StringWriter of PrometheusScrapeEndpoint, and below that some time spend in Arrays.copy of the wrapped StringBuilder.ensureCapacity. Default size is 16, so there will be multiple new array allocations and array copies during the filling of the StringWriter.

This is below TextFormat.writeOpenMetrics100: SpringBoot Unnecessary allocations in Prometheus scraping endpoint

A quick gain might be to pre size the StringWriter in PrometheusScrapeEndpoint.

A JMH benchmark on local Mac M1 using StringWriter with different initial capacity show no substantial gain in ops/s. Effect might be different/better in virtualized container environment with less cpu power? The JMH benchmark does show significant memory allocation improvement if StringWriter is created with initial actual size + some: can be around 3 times less.

Bytes allocated per op, with different initial StringWriter sizes: SpringBoot Unnecessary allocations in Prometheus scraping endpoint

Bytes allocated with StringWriter page size + 2: SpringBoot Unnecessary allocations in Prometheus scraping endpoint

First tried with fixed 14 * 1024 size, but that turns out to make it possibly worse when metrics page is a bit bigger than that and dynamic increase of backing array is 'too large' (e.g. see last column in first table). So then introduced a previousMetricsScrapeSize to hold size of last scrape and make new StringWriter that size plus some more for possible new data.

Not sure about actual improvement, but gut feeling is: less memory allocation, less objects (arrays) created to be garbage collected, less copying of arrays, and a component that is running in many places on earth will save some.

Final note: A default Prometheus metrics page is about 11k-12k characters. Adding additional metrics, e.g. via micrometer, will increase the size. We found that we used MicrometerHttpClientInterceptor that would add one line of metric info for each unique url, so that made 800k+ pages and had this issue magnified in profiling.

Comment From: mhalbritter

Hey @stokpop, thanks for the PR!

I think the idea to remember the last scrape size is very nice. But i would not set the initial size (the one before the 1st scrape) to a fixed 12kb, instead we should use the defaults of the StringWriter in that case. Can you please update the PR? Thanks a lot!

Comment From: stokpop

@mhalbritter good idea, so that would be 16, the default size of StringWriter, I have made the update.

Comment From: mhalbritter

Merged, thanks a lot for the contribution!

Comment From: wilkinsona

@mhalbritter and I discussed this a bit and we've decided that the unnecessary allocations are a performance bug.