Skip to content

Commit 6ec11c5

Browse files
authored
Heap profiler with leak tracking (#5763)
* Heap profiler with leak tracking * Improve wording and types * Better rationalization around nested allocations * Pad the atomic flags to the cache line size * Add padding * Add tokio trace * Move backtrace to trace event format * Improve loc for spawned task when using tokio/tracing * Move mutex from tracker * Add docker ci * Refactor logger for readability * Fix typos * Fix typo
1 parent 2e36400 commit 6ec11c5

File tree

14 files changed

+1021
-34
lines changed

14 files changed

+1021
-34
lines changed

.github/workflows/publish_docker_images.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ jobs:
6262
echo "QW_COMMIT_DATE=$(TZ=UTC0 git log -1 --format=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV
6363
echo "QW_COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
6464
echo "QW_COMMIT_TAGS=$(git tag --points-at HEAD | tr '\n' ',')" >> $GITHUB_ENV
65+
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref_type }}" == "tag" && "${GITHUB_REF#refs/tags/}" == *"jemprof"* ]]; then
66+
echo "CARGO_FEATURES=release-jemalloc-profiled" >> $GITHUB_ENV
67+
else
68+
echo "CARGO_FEATURES=release-feature-set" >> $GITHUB_ENV
69+
fi
6570
6671
- name: Build and push image
6772
uses: docker/build-push-action@v6
@@ -73,6 +78,7 @@ jobs:
7378
QW_COMMIT_DATE=${{ env.QW_COMMIT_DATE }}
7479
QW_COMMIT_HASH=${{ env.QW_COMMIT_HASH }}
7580
QW_COMMIT_TAGS=${{ env.QW_COMMIT_TAGS }}
81+
CARGO_FEATURES=${{ env.CARGO_FEATURES }}
7682
labels: ${{ steps.meta.outputs.labels }}
7783
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
7884

quickwit/Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

quickwit/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ assert-json-diff = "2"
8585
async-compression = { version = "0.4", features = ["tokio", "gzip"] }
8686
async-speed-limit = "0.4"
8787
async-trait = "0.1"
88+
backtrace = "0.3"
8889
base64 = "0.22"
8990
binggan = { version = "0.14" }
9091
bytes = { version = "1", features = ["serde"] }

quickwit/quickwit-cli/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ path = "src/generate_markdown.rs"
2222

2323
[dependencies]
2424
anyhow = { workspace = true }
25+
backtrace = { workspace = true, optional = true }
2526
bytesize = { workspace = true }
2627
chrono = { workspace = true }
2728
clap = { workspace = true }
@@ -81,6 +82,11 @@ quickwit-storage = { workspace = true, features = ["testsuite"] }
8182

8283
[features]
8384
jemalloc = ["dep:tikv-jemalloc-ctl", "dep:tikv-jemallocator"]
85+
jemalloc-profiled = [
86+
"dep:backtrace",
87+
"quickwit-common/jemalloc-profiled",
88+
"quickwit-serve/jemalloc-profiled"
89+
]
8490
ci-test = []
8591
pprof = ["quickwit-serve/pprof"]
8692
openssl-support = ["openssl-probe"]
@@ -127,6 +133,10 @@ release-macos-feature-vendored-set = [
127133
"quickwit-metastore/postgres",
128134
"quickwit-doc-mapper/multilang",
129135
]
136+
release-jemalloc-profiled = [
137+
"release-feature-set",
138+
"jemalloc-profiled",
139+
]
130140

131141
[package.metadata.cargo-machete]
132142
# used to enable the `multilang` feature

quickwit/quickwit-cli/src/jemalloc.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ use tikv_jemallocator::Jemalloc;
1919
use tracing::error;
2020

2121
#[global_allocator]
22+
#[cfg(feature = "jemalloc-profiled")]
23+
pub static GLOBAL: quickwit_common::jemalloc_profiled::JemallocProfiled =
24+
quickwit_common::jemalloc_profiled::JemallocProfiled(Jemalloc);
25+
#[cfg(not(feature = "jemalloc-profiled"))]
2226
pub static GLOBAL: Jemalloc = Jemalloc;
2327

2428
const JEMALLOC_METRICS_POLLING_INTERVAL: Duration = Duration::from_secs(1);

quickwit/quickwit-cli/src/logger.rs

Lines changed: 167 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ use crate::QW_ENABLE_OPENTELEMETRY_OTLP_EXPORTER_ENV_KEY;
4040
#[cfg(feature = "tokio-console")]
4141
use crate::QW_ENABLE_TOKIO_CONSOLE_ENV_KEY;
4242

43+
/// Load the default logging filter from the environment. The filter can later
44+
/// be updated using the result callback of [setup_logging_and_tracing].
45+
fn startup_env_filter(level: Level) -> anyhow::Result<EnvFilter> {
46+
let env_filter = env::var("RUST_LOG")
47+
.map(|_| EnvFilter::from_default_env())
48+
.or_else(|_| EnvFilter::try_new(format!("quickwit={level},tantivy=WARN")))
49+
.context("failed to set up tracing env filter")?;
50+
Ok(env_filter)
51+
}
52+
4353
pub fn setup_logging_and_tracing(
4454
level: Level,
4555
ansi_colors: bool,
@@ -52,13 +62,10 @@ pub fn setup_logging_and_tracing(
5262
return Ok(quickwit_serve::do_nothing_env_filter_reload_fn());
5363
}
5464
}
55-
let env_filter = env::var("RUST_LOG")
56-
.map(|_| EnvFilter::from_default_env())
57-
.or_else(|_| EnvFilter::try_new(format!("quickwit={level},tantivy=WARN")))
58-
.context("failed to set up tracing env filter")?;
5965
global::set_text_map_propagator(TraceContextPropagator::new());
60-
let (reloadable_env_filter, reload_handle) = tracing_subscriber::reload::Layer::new(env_filter);
61-
let registry = tracing_subscriber::registry().with(reloadable_env_filter);
66+
let (reloadable_env_filter, reload_handle) =
67+
tracing_subscriber::reload::Layer::new(startup_env_filter(level)?);
68+
let registry = tracing_subscriber::registry();
6269
// Note on disabling ANSI characters: setting the ansi boolean on event format is insufficient.
6370
// It is thus set on layers, see https://github.com/tokio-rs/tracing/issues/1817
6471
if get_bool_from_env(QW_ENABLE_OPENTELEMETRY_OTLP_EXPORTER_ENV_KEY, false) {
@@ -90,6 +97,7 @@ pub fn setup_logging_and_tracing(
9097
let fmt_fields = event_format.format_fields();
9198

9299
registry
100+
.with(reloadable_env_filter)
93101
.with(telemetry_layer)
94102
.with(
95103
tracing_subscriber::fmt::layer()
@@ -102,24 +110,44 @@ pub fn setup_logging_and_tracing(
102110
} else {
103111
let event_format = EventFormat::get_from_env();
104112
let fmt_fields = event_format.format_fields();
113+
#[cfg(not(feature = "jemalloc-profiled"))]
114+
let registry = registry.with(reloadable_env_filter).with(
115+
tracing_subscriber::fmt::layer()
116+
.event_format(event_format)
117+
.fmt_fields(fmt_fields)
118+
.with_ansi(ansi_colors),
119+
);
120+
#[cfg(feature = "jemalloc-profiled")]
121+
let registry = jemalloc_profiled::configure_registry(
122+
registry,
123+
event_format,
124+
fmt_fields,
125+
ansi_colors,
126+
level,
127+
)?;
105128

106129
registry
107-
.with(
108-
tracing_subscriber::fmt::layer()
109-
.event_format(event_format)
110-
.fmt_fields(fmt_fields)
111-
.with_ansi(ansi_colors),
112-
)
113130
.try_init()
114131
.context("failed to register tracing subscriber")?;
115132
}
133+
116134
Ok(Arc::new(move |env_filter_def: &str| {
117135
let new_env_filter = EnvFilter::try_new(env_filter_def)?;
118136
reload_handle.reload(new_env_filter)?;
119137
Ok(())
120138
}))
121139
}
122140

141+
/// We do not rely on the RFC3339 implementation, because it has a nanosecond precision.
142+
/// See discussion here: https://github.com/time-rs/time/discussions/418
143+
fn time_formatter() -> UtcTime<Vec<BorrowedFormatItem<'static>>> {
144+
let time_format = time::format_description::parse(
145+
"[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z",
146+
)
147+
.expect("time format description should be valid");
148+
UtcTime::new(time_format)
149+
}
150+
123151
enum EventFormat<'a> {
124152
Full(Format<Full, UtcTime<Vec<BorrowedFormatItem<'a>>>>),
125153
Json(Format<Json>),
@@ -136,17 +164,9 @@ impl EventFormat<'_> {
136164
let json_format = tracing_subscriber::fmt::format().json();
137165
EventFormat::Json(json_format)
138166
} else {
139-
// We do not rely on the RFC3339 implementation, because it has a nanosecond precision.
140-
// See discussion here: https://github.com/time-rs/time/discussions/418
141-
let timer_format = time::format_description::parse(
142-
"[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z",
143-
)
144-
.expect("time format description should be valid");
145-
let timer = UtcTime::new(timer_format);
146-
147167
let full_format = tracing_subscriber::fmt::format()
148168
.with_target(true)
149-
.with_timer(timer);
169+
.with_timer(time_formatter());
150170

151171
EventFormat::Full(full_format)
152172
}
@@ -191,3 +211,129 @@ impl FormatFields<'_> for FieldFormat {
191211
}
192212
}
193213
}
214+
215+
/// Logger configurations specific to the jemalloc profiler.
216+
///
217+
/// A custom event formatter is used to print the backtrace of the
218+
/// profiling events.
219+
#[cfg(feature = "jemalloc-profiled")]
220+
pub(super) mod jemalloc_profiled {
221+
use std::fmt;
222+
223+
use quickwit_common::jemalloc_profiled::JEMALLOC_PROFILER_TARGET;
224+
use time::format_description::BorrowedFormatItem;
225+
use tracing::{Event, Level, Metadata, Subscriber};
226+
use tracing_subscriber::Layer;
227+
use tracing_subscriber::filter::filter_fn;
228+
use tracing_subscriber::fmt::format::{DefaultFields, Writer};
229+
use tracing_subscriber::fmt::time::{FormatTime, UtcTime};
230+
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields, FormattedFields};
231+
use tracing_subscriber::layer::SubscriberExt;
232+
use tracing_subscriber::registry::LookupSpan;
233+
234+
use super::{EventFormat, FieldFormat, startup_env_filter, time_formatter};
235+
236+
/// An event formatter specific to the memory profiler output.
237+
///
238+
/// Also displays a backtrace after spans and the fields of the tracing
239+
/// event (into separate lines).
240+
struct ProfilingFormat {
241+
time_formatter: UtcTime<Vec<BorrowedFormatItem<'static>>>,
242+
}
243+
244+
impl Default for ProfilingFormat {
245+
fn default() -> Self {
246+
Self {
247+
time_formatter: time_formatter(),
248+
}
249+
}
250+
}
251+
252+
impl<S, N> FormatEvent<S, N> for ProfilingFormat
253+
where
254+
S: Subscriber + for<'a> LookupSpan<'a>,
255+
N: for<'a> FormatFields<'a> + 'static,
256+
{
257+
fn format_event(
258+
&self,
259+
ctx: &FmtContext<'_, S, N>,
260+
mut writer: Writer<'_>,
261+
event: &Event<'_>,
262+
) -> fmt::Result {
263+
self.time_formatter.format_time(&mut writer)?;
264+
write!(writer, " {JEMALLOC_PROFILER_TARGET} ")?;
265+
if let Some(scope) = ctx.event_scope() {
266+
let mut seen = false;
267+
268+
for span in scope.from_root() {
269+
write!(writer, "{}", span.metadata().name())?;
270+
seen = true;
271+
272+
let ext = span.extensions();
273+
if let Some(fields) = &ext.get::<FormattedFields<N>>() {
274+
if !fields.is_empty() {
275+
write!(writer, "{{{}}}:", fields)?;
276+
}
277+
}
278+
}
279+
280+
if seen {
281+
writer.write_char(' ')?;
282+
}
283+
};
284+
285+
ctx.format_fields(writer.by_ref(), event)?;
286+
writeln!(writer)?;
287+
288+
// Print a backtrace to help idenify the callsite
289+
backtrace::trace(|frame| {
290+
backtrace::resolve_frame(frame, |symbol| {
291+
if let Some(symbole_name) = symbol.name() {
292+
let _ = writeln!(writer, "{}", symbole_name);
293+
} else {
294+
let _ = writeln!(writer, "symb failed");
295+
}
296+
});
297+
true
298+
});
299+
Ok(())
300+
}
301+
}
302+
303+
fn profiler_tracing_filter(metadata: &Metadata) -> bool {
304+
metadata.is_span() || (metadata.is_event() && metadata.target() == JEMALLOC_PROFILER_TARGET)
305+
}
306+
307+
/// Configures the regular logging layer and a specific layer that gathers
308+
/// extra debug information for the jemalloc profiler.
309+
///
310+
/// The the jemalloc profiler formatter disables the env filter reloading
311+
/// because the [tracing_subscriber::reload::Layer] seems to overwrite the
312+
/// TRACE level span filter even though it's applied to a separate layer.
313+
pub(super) fn configure_registry<S>(
314+
registry: S,
315+
event_format: EventFormat<'static>,
316+
fmt_fields: FieldFormat,
317+
ansi_colors: bool,
318+
level: Level,
319+
) -> anyhow::Result<impl Subscriber + for<'span> LookupSpan<'span>>
320+
where
321+
S: Subscriber + for<'span> LookupSpan<'span>,
322+
{
323+
Ok(registry
324+
.with(
325+
tracing_subscriber::fmt::layer()
326+
.event_format(ProfilingFormat::default())
327+
.fmt_fields(DefaultFields::new())
328+
.with_ansi(ansi_colors)
329+
.with_filter(filter_fn(profiler_tracing_filter)),
330+
)
331+
.with(
332+
tracing_subscriber::fmt::layer()
333+
.event_format(event_format)
334+
.fmt_fields(fmt_fields)
335+
.with_ansi(ansi_colors)
336+
.with_filter(startup_env_filter(level)?),
337+
))
338+
}
339+
}

quickwit/quickwit-common/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ license.workspace = true
1414
anyhow = { workspace = true }
1515
async-speed-limit = { workspace = true }
1616
async-trait = { workspace = true }
17+
backtrace = { workspace = true, optional = true }
1718
bytesize = { workspace = true }
1819
coarsetime = { workspace = true }
1920
dyn-clone = { workspace = true }
@@ -37,6 +38,8 @@ siphasher = { workspace = true }
3738
sysinfo = { workspace = true }
3839
tempfile = { workspace = true }
3940
thiserror = { workspace = true }
41+
tikv-jemallocator = { workspace = true, optional = true }
42+
tikv-jemalloc-ctl = { workspace = true, optional = true }
4043
tokio = { workspace = true }
4144
tokio-metrics = { workspace = true }
4245
tokio-stream = { workspace = true }
@@ -47,9 +50,16 @@ tracing = { workspace = true }
4750
[features]
4851
testsuite = []
4952
named_tasks = ["tokio/tracing"]
53+
jemalloc-profiled = [
54+
"named_tasks",
55+
"dep:backtrace",
56+
"dep:tikv-jemallocator",
57+
"dep:tikv-jemalloc-ctl"
58+
]
5059

5160
[dev-dependencies]
5261
serde_json = { workspace = true }
5362
tempfile = { workspace = true }
5463
proptest = { workspace = true }
64+
serial_test = { workspace = true }
5565
tokio = { workspace = true, features = ["test-util"] }

0 commit comments

Comments
 (0)