Skip to content

Commit ceb4c9f

Browse files
committed
prometheus_text_format: Eliminate ram_file, add format_into/3
Building on the work in the parent commit, now that the data being passed to the `ram_file` is a binary, we can instead build the entire output gradually within the process. We pay in terms of I/O overhead from writing and then reading from the `ram_file` since `ram_file` is a port - all data is passed between the VM and the port driver. The memory consumed by a port driver is also invisible to the VM's allocator, so large port driver resource usage should be avoided where possible. Instead this change refactors the `registry_collect_callback` to fold over collectors and build an accumulator. The `create_mf` callback's return of `ok` forces us to store this rather than pass and return it. So it's a little less hygienic but is more efficient than passing data in/out of a port. This also introduces a function `format_into/3` which can use this folding function directly. This can be used to avoid collecting the entire response in one binary. Instead the response can be streamed with `cowboy_req:stream_body/3` for example.
1 parent 458e5d1 commit ceb4c9f

File tree

1 file changed

+38
-34
lines changed

1 file changed

+38
-34
lines changed

src/formats/prometheus_text_format.erl

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ http_request_duration_milliseconds_sum{method=\"post\"} 4350
2626
```
2727
""").
2828

29-
-export([content_type/0, format/0, format/1, render_labels/1, escape_label_value/1]).
29+
-export([content_type/0, format/0, format/1, format_into/3, render_labels/1, escape_label_value/1]).
3030

3131
-ifdef(TEST).
3232
-export([escape_metric_help/1]).
@@ -57,16 +57,43 @@ format() ->
5757
?DOC("Formats `Registry` using the latest text format.").
5858
-spec format(Registry :: prometheus_registry:registry()) -> binary().
5959
format(Registry) ->
60-
{ok, Fd} = ram_file:open("", [write, read, binary]),
61-
Callback = fun(_, Collector) ->
62-
registry_collect_callback(Fd, Registry, Collector)
63-
end,
64-
prometheus_registry:collect(Registry, Callback),
65-
file:write(Fd, "\n"),
66-
{ok, Size} = ram_file:get_size(Fd),
67-
{ok, Str} = file:pread(Fd, 0, Size),
68-
ok = file:close(Fd),
69-
Str.
60+
Data = format_into(Registry, [], fun(Data, Acc) -> [Data | Acc] end),
61+
iolist_to_binary(lists:reverse(Data)).
62+
63+
?DOC("""
64+
Formats `Registry` using the latest text format, passing the binary data for
65+
each collector to the format function.
66+
""").
67+
-spec format_into(Registry :: prometheus_registry:registry(), term(), fun((binary(), term()) -> term())) -> term().
68+
format_into(Registry, State, Fmt) ->
69+
State1 = lists:foldl(
70+
fun(Collector, Acc) ->
71+
put(?MODULE, Acc),
72+
Callback = fun(#'MetricFamily'{name = Name0, help = Help, type = Type, metric = Metrics}) ->
73+
%% eagerly convert the name to a binary so we can copy more efficiently
74+
%% in `render_metrics/3`
75+
Name = iolist_to_binary(Name0),
76+
Prologue = <<
77+
"# TYPE ",
78+
Name/binary,
79+
" ",
80+
(string_type(Type))/binary,
81+
"\n# HELP ",
82+
Name/binary,
83+
" ",
84+
(escape_metric_help(Help))/binary,
85+
"\n"
86+
>>,
87+
Bin = render_metrics(Name, Metrics, Prologue),
88+
put(?MODULE, Fmt(Bin, erase(?MODULE)))
89+
end,
90+
prometheus_collector:collect_mf(Registry, Collector, Callback),
91+
erase(?MODULE)
92+
end,
93+
State,
94+
prometheus_registry:collectors(Registry)
95+
),
96+
Fmt(<<"\n">>, State1).
7097

7198
?DOC("""
7299
Escapes the backslash (\\), double-quote (\"), and line feed (\\n) characters
@@ -84,29 +111,6 @@ escape_label_value(LValue) when is_list(LValue) ->
84111
escape_label_value(Value) ->
85112
erlang:error({invalid_value, Value}).
86113

87-
registry_collect_callback(Fd, Registry, Collector) ->
88-
Callback = fun(#'MetricFamily'{name = Name0, help = Help, type = Type, metric = Metrics}) ->
89-
%% eagerly convert the name to a binary so we can copy more efficiently
90-
%% in `render_metrics/3`
91-
Name = iolist_to_binary(Name0),
92-
Prologue = <<
93-
"# TYPE ",
94-
Name/binary,
95-
" ",
96-
(string_type(Type))/binary,
97-
"\n# HELP ",
98-
Name/binary,
99-
" ",
100-
(escape_metric_help(Help))/binary,
101-
"\n"
102-
>>,
103-
%% file:write/2 is an expensive operation, as it goes through a port driver.
104-
%% Instead a large chunk of bytes is being collected here, in a
105-
%% way that triggers binary append optimization in ERTS.
106-
file:write(Fd, render_metrics(Name, Metrics, Prologue))
107-
end,
108-
prometheus_collector:collect_mf(Registry, Collector, Callback).
109-
110114
render_metrics(_Name, [], Bytes) ->
111115
Bytes;
112116
render_metrics(Name, [Metric | Rest], Bytes) ->

0 commit comments

Comments
 (0)