Skip to content

Commit 16d242f

Browse files
authored
Optimize text format scrape hot path (#202)
* Pre-compute metric name and help as binaries at declaration time During every scrape, metric name and help were converted from atom/list to binary via ensure_binary_or_string/1 in create_mf. With many metric families this repeated work adds up, especially the atom_to_binary calls that show up in profiling. Now, extract_common_params/1 returns pre-computed NameBin and HelpBin, stored in ETS as a 3-tuple {Labels, HelpBin, NameBin} alongside label definitions. All six built-in metric collectors (boolean, counter, gauge, histogram, quantile_summary, summary) pass these binaries directly into create_mf, avoiding per-scrape conversion entirely. A normalize_mf_row/1 function in prometheus_metric handles backward compatibility: old ETS entries with the 2-tuple {Labels, Help} format are normalized on read, so hot upgrades don't crash. * prometheus_model_helpers: Optimize hot path for metric record creation Profiling (tprof) of a RabbitMQ workload shows gauge_metric/2 at 16.32% and counter_metric/2 at 2.45% of total process time, with ~2M calls each allocating 12 words of intermediate records that the text format immediately destructures and discards. This commit applies several targeted optimizations: - Inline gauge_metric/2, counter_metric/2, and label_pair/1 to eliminate function call overhead on these ~2M-call-per-scrape functions. - Add a fast path in create_mf/4 that detects when the input list already contains #'Metric'{} records (as all built-in collectors produce), skipping the metrics_from_tuples/2 dispatch entirely. This avoids the per-element is_record check and type-based dispatch for every metric. - Replace lists:map(fun label_pair/1, Labels) with a list comprehension, which the compiler can optimize better when combined with the inline directive. - Add integer and float clauses to ensure_binary_or_string/1, avoiding the expensive io_lib:format("~p", [Val]) fallback for numeric label values. * prometheus_model_helpers: Merge filter and map into single pass metrics_from_tuples/2 previously traversed the list twice: once via filter_undefined_metrics (lists:filter) to remove undefined entries, then a list comprehension to convert tuples to records. Profiling shows lists:filter alone at 3.26% of scrape time with ~2.4M elements. Fold both operations into a single list comprehension with a guard filter. Apply the same pattern to the create_mf/4 fast path for pre-built #'Metric'{} records. * prometheus_text_format: Inline render_series/4 and render_value/2 These two functions are called once per metric series during every scrape (~2.3M calls each in profiled RabbitMQ workloads), together accounting for ~15.6% of scrape time. Inlining eliminates the function call overhead and allows the compiler to optimize the binary append operations across call boundaries. * Apply review comments
1 parent 27ebf45 commit 16d242f

13 files changed

Lines changed: 130 additions & 56 deletions

src/formats/prometheus_text_format.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ http_request_duration_milliseconds_sum{method=\"post\"} 4350
3535
-include("prometheus_model.hrl").
3636

3737
-behaviour(prometheus_format).
38-
-compile({inline, [render_label_pair/1]}).
38+
-compile({inline, [render_label_pair/1, render_series/4, render_value/2]}).
3939

4040
?DOC("""
4141
Returns content type of the latest \[text format](https://bit.ly/2cxSuJP).

src/metrics/prometheus_boolean.erl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,14 +326,16 @@ deregister_cleanup(Registry) ->
326326
-spec collect_mf(prometheus_registry:registry(), prometheus_collector:collect_mf_callback()) -> ok.
327327
collect_mf(Registry, Callback) ->
328328
[
329-
Callback(create_boolean(Name, Help, {CLabels, Labels, Registry}))
330-
|| [Name, {Labels, Help}, CLabels, _, _] <- prometheus_metric:metrics(?TABLE, Registry)
329+
Callback(create_boolean(NameBin, HelpBin, {CLabels, Labels, Registry, Name}))
330+
|| [Name, {Labels, HelpBin, NameBin}, CLabels, _, _] <- prometheus_metric:metrics(
331+
?TABLE, Registry
332+
)
331333
],
332334
ok.
333335

334336
?DOC(false).
335337
-spec collect_metrics(prometheus_metric:name(), tuple()) -> [prometheus_model:'Metric'()].
336-
collect_metrics(Name, {CLabels, Labels, Registry}) ->
338+
collect_metrics(_NameBin, {CLabels, Labels, Registry, Name}) ->
337339
[
338340
prometheus_model_helpers:boolean_metric(
339341
CLabels ++ lists:zip(Labels, LabelValues), Value

src/metrics/prometheus_counter.erl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,15 +336,17 @@ deregister_cleanup(Registry) ->
336336
-spec collect_mf(prometheus_registry:registry(), prometheus_collector:collect_mf_callback()) -> ok.
337337
collect_mf(Registry, Callback) ->
338338
[
339-
Callback(create_counter(Name, Help, {CLabels, Labels, Registry}))
340-
|| [Name, {Labels, Help}, CLabels, _, _] <- prometheus_metric:metrics(?TABLE, Registry)
339+
Callback(create_counter(NameBin, HelpBin, {CLabels, Labels, Registry, Name}))
340+
|| [Name, {Labels, HelpBin, NameBin}, CLabels, _, _] <- prometheus_metric:metrics(
341+
?TABLE, Registry
342+
)
341343
],
342344
ok.
343345

344346
?DOC(false).
345347
-spec collect_metrics(prometheus_metric:name(), tuple()) ->
346348
[prometheus_model:'Metric'()].
347-
collect_metrics(Name, {CLabels, Labels, Registry}) ->
349+
collect_metrics(_NameBin, {CLabels, Labels, Registry, Name}) ->
348350
MFValues = load_all_values(Registry, Name),
349351
LabelValues = reduce_label_values(MFValues),
350352
Fun = fun(VLabels, Value) ->

src/metrics/prometheus_gauge.erl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -529,15 +529,17 @@ deregister_cleanup(Registry) ->
529529
-spec collect_mf(prometheus_registry:registry(), prometheus_collector:collect_mf_callback()) -> ok.
530530
collect_mf(Registry, Callback) ->
531531
[
532-
Callback(create_gauge(Name, Help, {CLabels, Labels, Registry, DU}))
533-
|| [Name, {Labels, Help}, CLabels, DU, _] <- prometheus_metric:metrics(?TABLE, Registry)
532+
Callback(create_gauge(NameBin, HelpBin, {CLabels, Labels, Registry, DU, Name}))
533+
|| [Name, {Labels, HelpBin, NameBin}, CLabels, DU, _] <- prometheus_metric:metrics(
534+
?TABLE, Registry
535+
)
534536
],
535537
ok.
536538

537539
?DOC(false).
538540
-spec collect_metrics(prometheus_metric:name(), tuple()) ->
539541
[prometheus_model:'Metric'()].
540-
collect_metrics(Name, {CLabels, Labels, Registry, DU}) ->
542+
collect_metrics(_NameBin, {CLabels, Labels, Registry, DU, Name}) ->
541543
[
542544
prometheus_model_helpers:gauge_metric(
543545
CLabels ++ lists:zip(Labels, LabelValues),

src/metrics/prometheus_histogram.erl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -495,15 +495,17 @@ deregister_cleanup(Registry) ->
495495
-spec collect_mf(prometheus_registry:registry(), prometheus_collector:collect_mf_callback()) -> ok.
496496
collect_mf(Registry, Callback) ->
497497
[
498-
Callback(create_histogram(Name, Help, {CLabels, Labels, Registry, DU, Buckets}))
499-
|| [Name, {Labels, Help}, CLabels, DU, Buckets] <- prometheus_metric:metrics(?TABLE, Registry)
498+
Callback(create_histogram(NameBin, HelpBin, {CLabels, Labels, Registry, DU, Buckets, Name}))
499+
|| [Name, {Labels, HelpBin, NameBin}, CLabels, DU, Buckets] <- prometheus_metric:metrics(
500+
?TABLE, Registry
501+
)
500502
],
501503
ok.
502504

503505
?DOC(false).
504506
-spec collect_metrics(prometheus_metric:name(), tuple()) ->
505507
[prometheus_model:'Metric'()].
506-
collect_metrics(Name, {CLabels, Labels, Registry, DU, Bounds}) ->
508+
collect_metrics(_NameBin, {CLabels, Labels, Registry, DU, Bounds, Name}) ->
507509
MFValues = load_all_values(Registry, Name, Bounds),
508510
LabelValuesMap = reduce_label_values(MFValues),
509511
Fun = fun(LabelValues, Stat, L) ->

src/metrics/prometheus_quantile_summary.erl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -381,15 +381,15 @@ deregister_cleanup(Registry) ->
381381
collect_mf(Registry, Callback) ->
382382
Metrics = prometheus_metric:metrics(?TABLE, Registry),
383383
[
384-
Callback(create_summary(Name, Help, {CLabels, Labels, Registry, DU, Data}))
385-
|| [Name, {Labels, Help}, CLabels, DU, Data] <- Metrics
384+
Callback(create_summary(NameBin, HelpBin, {CLabels, Labels, Registry, DU, Data, Name}))
385+
|| [Name, {Labels, HelpBin, NameBin}, CLabels, DU, Data] <- Metrics
386386
],
387387
ok.
388388

389389
?DOC(false).
390390
-spec collect_metrics(prometheus_metric:name(), tuple()) ->
391391
[prometheus_model:'Metric'()].
392-
collect_metrics(Name, {CLabels, Labels, Registry, _DU, _Configuration}) ->
392+
collect_metrics(_NameBin, {CLabels, Labels, Registry, _DU, _Configuration, Name}) ->
393393
Fun = fun model_summary_metric/6,
394394
loop_through_keys(Name, Fun, CLabels, Labels, Registry).
395395

src/metrics/prometheus_summary.erl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,15 +376,17 @@ deregister_cleanup(Registry) ->
376376
-spec collect_mf(prometheus_registry:registry(), prometheus_collector:collect_mf_callback()) -> ok.
377377
collect_mf(Registry, Callback) ->
378378
[
379-
Callback(create_summary(Name, Help, {CLabels, Labels, Registry, DU}))
380-
|| [Name, {Labels, Help}, CLabels, DU, _] <- prometheus_metric:metrics(?TABLE, Registry)
379+
Callback(create_summary(NameBin, HelpBin, {CLabels, Labels, Registry, DU, Name}))
380+
|| [Name, {Labels, HelpBin, NameBin}, CLabels, DU, _] <- prometheus_metric:metrics(
381+
?TABLE, Registry
382+
)
381383
],
382384
ok.
383385

384386
?DOC(false).
385387
-spec collect_metrics(prometheus_metric:name(), tuple()) ->
386388
[prometheus_model:'Metric'()].
387-
collect_metrics(Name, {CLabels, Labels, Registry, DU}) ->
389+
collect_metrics(_NameBin, {CLabels, Labels, Registry, DU, Name}) ->
388390
MFValues = load_all_values(Registry, Name),
389391
ReducedMap = lists:foldl(
390392
fun([L, C, IS, FS], ResAcc) ->

src/model/prometheus_model_helpers.erl

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,15 @@ Probably will be used with `m:prometheus_collector`.
4545

4646
-ifdef(TEST).
4747
-export([
48-
filter_undefined_metrics/1,
4948
ensure_mf_type/1,
5049
ensure_binary_or_string/1
5150
]).
5251
-endif.
5352

5453
-include("prometheus_model.hrl").
5554

55+
-compile({inline, [label_pair/1, label_pair/2, gauge_metric/2, counter_metric/2]}).
56+
5657
%%%===================================================================
5758
%%% Public API
5859
%%%===================================================================
@@ -100,6 +101,16 @@ Create Metric Family of `Type`, `Name` and `Help`.
100101
MetricFamily :: prometheus_model:'MetricFamily'().
101102
create_mf(Name, Help, Type, Metrics) when is_map(Metrics) ->
102103
create_mf(Name, Help, Type, maps:to_list(Metrics));
104+
create_mf(Name, Help, Type, [First | _] = Metrics0) when
105+
is_list(Metrics0), is_record(First, 'Metric')
106+
->
107+
%% Fast path: metrics are already #'Metric'() records, skip metrics_from_tuples
108+
#'MetricFamily'{
109+
name = ensure_binary_or_string(Name),
110+
help = ensure_binary_or_string(Help),
111+
type = ensure_mf_type(Type),
112+
metric = [M || M <- Metrics0, M =/= undefined]
113+
};
103114
create_mf(Name, Help, Type, Metrics0) ->
104115
Metrics = metrics_from_tuples(Type, Metrics0),
105116
#'MetricFamily'{
@@ -340,15 +351,18 @@ fail with an error.
340351
label_pairs(B) when is_binary(B) ->
341352
B;
342353
label_pairs(Labels) when is_list(Labels) ->
343-
lists:map(fun label_pair/1, Labels);
354+
[label_pair(L) || L <- Labels];
344355
label_pairs(Labels) when is_map(Labels) ->
345-
lists:map(fun label_pair/1, maps:to_list(Labels)).
356+
[label_pair(Name, Value) || Name := Value <- Labels].
346357

347358
?DOC("""
348359
Creates `prometheus_model:`LabelPair'()' from \{Name, Value\} tuple.
349360
""").
350361
-spec label_pair(prometheus:label()) -> prometheus_model:'LabelPair'().
351362
label_pair({Name, Value}) ->
363+
label_pair(Name, Value).
364+
365+
label_pair(Name, Value) ->
352366
#'LabelPair'{
353367
name = ensure_binary_or_string(Name),
354368
value = ensure_binary_or_string(Value)
@@ -373,10 +387,7 @@ histogram_bucket({Bound, Count}) ->
373387
}.
374388

375389
metrics_from_tuples(Type, Metrics) ->
376-
[
377-
metric_from_tuple(Type, Metric)
378-
|| Metric <- filter_undefined_metrics(ensure_list(Metrics))
379-
].
390+
[metric_from_tuple(Type, M) || M <- ensure_list(Metrics), M =/= undefined].
380391

381392
metric_from_tuple(_, Metric) when is_record(Metric, 'Metric') ->
382393
Metric;
@@ -397,20 +408,14 @@ metric_from_tuple(untyped, Metric) ->
397408
ensure_list(Val) when is_list(Val) -> Val;
398409
ensure_list(Val) -> [Val].
399410

400-
?DOC(false).
401-
-spec filter_undefined_metrics([undefined | T]) -> [T].
402-
filter_undefined_metrics(Metrics) ->
403-
lists:filter(fun not_undefined/1, Metrics).
404-
405-
not_undefined(undefined) -> false;
406-
not_undefined(_) -> true.
407-
408411
?DOC(false).
409412
-spec ensure_binary_or_string(Val :: term()) -> binary() | string().
410413
ensure_binary_or_string(Val) when is_atom(Val) -> atom_to_binary(Val, utf8);
411414
%% FIXME: validate utf8
412415
ensure_binary_or_string(Val) when is_list(Val) -> Val;
413416
ensure_binary_or_string(Val) when is_binary(Val) -> Val;
417+
ensure_binary_or_string(Val) when is_integer(Val) -> integer_to_binary(Val);
418+
ensure_binary_or_string(Val) when is_float(Val) -> float_to_binary(Val, [short]);
414419
ensure_binary_or_string(Val) -> io_lib:format("~p", [Val]).
415420

416421
?DOC(false).

src/prometheus_collector.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ collect_mf(_Registry, Callback) ->
4343
Memory)),
4444
ok.
4545
46-
collect_metrics(erlang_vm_bytes_total, Memory) ->
46+
collect_metrics(_Name, Memory) ->
4747
prometheus_model_helpers:gauge_metrics(
4848
[
4949
{[{kind, system}], proplists:get_value(system, Memory)},

src/prometheus_metric.erl

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,10 @@ insert_new_mf(Table, Module, Spec) ->
147147
Module :: atom(),
148148
Spec :: spec().
149149
insert_mf(Table, Module, Spec) ->
150-
{Registry, Name, Labels, Help, CLabels, DurationUnit, Data} =
150+
{Registry, Name, Labels, _Help, CLabels, DurationUnit, Data, NameBin, HelpBin} =
151151
prometheus_metric_spec:extract_common_params(Spec),
152152
prometheus_registry:register_collector(Registry, Module),
153-
Tuple = {{Registry, mf, Name}, {Labels, Help}, CLabels, DurationUnit, Data},
153+
Tuple = {{Registry, mf, Name}, {Labels, HelpBin, NameBin}, CLabels, DurationUnit, Data},
154154
case ets:insert_new(Table, Tuple) of
155155
true ->
156156
maybe_set_default(Module, Registry, Name, Labels),
@@ -189,7 +189,8 @@ check_mf_exists(Table, Registry, Name, LabelValues) ->
189189
case ets:lookup(Table, {Registry, mf, Name}) of
190190
[] ->
191191
erlang:error({unknown_metric, Registry, Name});
192-
[{_, {Labels, _}, _, _, _} = MF] ->
192+
[{_, E2, _, _, _} = MF] ->
193+
Labels = element(1, E2),
193194
LVLength = length(LabelValues),
194195
case length(Labels) of
195196
LVLength ->
@@ -215,8 +216,7 @@ check_mf_exists(Table, Registry, Name) ->
215216
?DOC(false).
216217
-spec mf_labels(tuple()) -> dynamic().
217218
mf_labels(MF) ->
218-
{Labels, _} = element(2, MF),
219-
Labels.
219+
element(1, element(2, MF)).
220220

221221
?DOC(false).
222222
-spec mf_constant_labels(tuple()) -> dynamic().
@@ -236,7 +236,23 @@ mf_data(MF) ->
236236
?DOC(false).
237237
-spec metrics(ets:table(), prometheus_registry:registry()) -> dynamic().
238238
metrics(Table, Registry) ->
239-
ets:match(Table, {{Registry, mf, '$1'}, '$2', '$3', '$4', '$5'}).
239+
Rows = ets:match(Table, {{Registry, mf, '$1'}, '$2', '$3', '$4', '$5'}),
240+
[normalize_mf_row(Row) || Row <- Rows].
241+
242+
normalize_mf_row([Name, {Labels, HelpBin, NameBin}, C, D, Data]) when
243+
is_binary(NameBin), is_binary(HelpBin)
244+
->
245+
[Name, {Labels, HelpBin, NameBin}, C, D, Data];
246+
normalize_mf_row([Name, {Labels, Help}, C, D, Data]) ->
247+
%% Backward compat: old ETS data had 2-tuple; normalize on read
248+
[
249+
Name,
250+
{Labels, prometheus_metric_spec:normalize_to_binary(Help),
251+
prometheus_metric_spec:normalize_to_binary(Name)},
252+
C,
253+
D,
254+
Data
255+
].
240256

241257
%%====================================================================
242258
%% Private Parts

0 commit comments

Comments
 (0)