1use std::env;
13use std::sync::LazyLock;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::time::Duration;
16
17use askama::Template;
18use axum::Json;
19use axum::response::IntoResponse;
20use axum::routing::{self, Router};
21use cfg_if::cfg_if;
22use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE};
23use http::{HeaderMap, HeaderValue, StatusCode};
24use mz_build_info::BuildInfo;
25use mz_prof::StackProfileExt;
26#[cfg(all(feature = "jemalloc", not(miri)))]
27use mz_prof::jemalloc::JemallocProfCtlExt;
28use pprof_util::{ProfStartTime, StackProfile};
29use serde::{Deserialize, Serialize};
30
31cfg_if! {
32 if #[cfg(any(not(feature = "jemalloc"), miri))] {
33 use disabled::{handle_get, handle_get_heap, handle_get_mode, handle_post, handle_post_mode};
34 } else {
35 use enabled::{handle_get, handle_get_heap, handle_get_mode, handle_post, handle_post_mode};
36 }
37}
38
39static EXECUTABLE: LazyLock<String> = LazyLock::new(|| {
40 {
41 env::current_exe()
42 .ok()
43 .as_ref()
44 .and_then(|exe| exe.file_name())
45 .map(|exe| exe.to_string_lossy().into_owned())
46 .unwrap_or_else(|| "<unknown executable>".into())
47 }
48});
49
50mz_http_util::make_handle_static!(
51 dir_1: ::include_dir::include_dir!("$CARGO_MANIFEST_DIR/src/http/static"),
52 dir_2: ::include_dir::include_dir!("$OUT_DIR/src/http/static"),
53 prod_base_path: "src/http/static",
54 dev_base_path: "src/http/static-dev",
55);
56
57pub fn router(build_info: &'static BuildInfo) -> Router {
59 Router::new()
60 .route(
61 "/",
62 routing::get(move |query, headers| handle_get(query, headers, build_info)),
63 )
64 .route(
65 "/",
66 routing::post(move |form| handle_post(form, build_info)),
67 )
68 .route("/cpu", routing::post(handle_post_cpu))
69 .route(
70 "/mode",
71 routing::get(handle_get_mode).post(handle_post_mode),
72 )
73 .route("/heap", routing::get(handle_get_heap))
74 .route("/static/{*path}", routing::get(handle_static))
75}
76
77static CPU_PROFILING_ACTIVE: AtomicBool = AtomicBool::new(false);
78
79const MAX_CPU_PROFILE_TIME_SECS: u64 = 3600;
83const MAX_CPU_PROFILE_HZ: u32 = 1_000_000;
86
87#[derive(Deserialize)]
88struct CpuProfileRequest {
89 seconds: u64,
90 hz: u32,
91 #[serde(default)]
92 merge_threads: bool,
93}
94
95#[derive(Debug, Deserialize)]
97struct ModeUpdateRequest {
98 memory_active: Option<bool>,
99}
100
101#[derive(Debug, Serialize)]
102struct ModeResponse {
103 cpu_active: bool,
104 memory_available: bool,
105 memory_active: bool,
106}
107
108fn cpu_profiling_active() -> bool {
109 CPU_PROFILING_ACTIVE.load(Ordering::SeqCst)
110}
111
112struct CpuProfilingGuard;
113
114impl CpuProfilingGuard {
115 fn acquire() -> Result<Self, (StatusCode, String)> {
116 CPU_PROFILING_ACTIVE
117 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
118 .map_err(|_| {
119 (
120 StatusCode::CONFLICT,
121 "CPU profiling is already running".to_owned(),
122 )
123 })?;
124 Ok(Self)
125 }
126}
127
128impl Drop for CpuProfilingGuard {
129 fn drop(&mut self) {
130 CPU_PROFILING_ACTIVE.store(false, Ordering::SeqCst);
131 }
132}
133
134fn validate_cpu_profile_params(
135 time_secs: u64,
136 sample_freq: u32,
137) -> Result<(), (StatusCode, String)> {
138 if time_secs == 0 {
139 return Err((
140 StatusCode::BAD_REQUEST,
141 "`seconds` must be greater than zero".to_owned(),
142 ));
143 }
144 if time_secs > MAX_CPU_PROFILE_TIME_SECS {
145 return Err((
146 StatusCode::BAD_REQUEST,
147 format!("`seconds` must be at most {MAX_CPU_PROFILE_TIME_SECS}"),
148 ));
149 }
150 if sample_freq == 0 {
151 return Err((
152 StatusCode::BAD_REQUEST,
153 "`hz` must be greater than zero".to_owned(),
154 ));
155 }
156 if sample_freq > MAX_CPU_PROFILE_HZ {
157 return Err((
158 StatusCode::BAD_REQUEST,
159 format!("`hz` must be at most {MAX_CPU_PROFILE_HZ}"),
160 ));
161 }
162 Ok(())
163}
164
165fn cpu_pprof_response(mut stacks: StackProfile) -> impl IntoResponse {
166 if let Some(mappings) = mappings::MAPPINGS.as_ref() {
171 stacks.mappings = mappings.clone();
172 }
173 let pprof = stacks.to_pprof(("samples", "count"), ("cpu", "nanoseconds"), None);
174 (
175 HeaderMap::from_iter([
176 (
177 CONTENT_DISPOSITION,
178 HeaderValue::from_static("attachment; filename=\"cpu.pb.gz\""),
179 ),
180 (
181 CONTENT_TYPE,
182 HeaderValue::from_static("application/octet-stream"),
183 ),
184 ]),
185 pprof,
186 )
187}
188
189async fn handle_post_cpu(
190 Json(request): Json<CpuProfileRequest>,
191) -> Result<impl IntoResponse, (StatusCode, String)> {
192 let stacks = capture_cpu_profile(request.merge_threads, request.seconds, request.hz).await?;
193 Ok(cpu_pprof_response(stacks))
194}
195
196#[allow(dead_code)]
197enum MemProfilingStatus {
198 Disabled,
199 Enabled(Option<ProfStartTime>),
200}
201
202#[derive(Template)]
203#[template(path = "prof.html")]
204struct ProfTemplate<'a> {
205 version: &'a str,
206 executable: &'a str,
207 mem_prof: MemProfilingStatus,
208 ever_symbolized: bool,
209}
210
211#[derive(Template)]
212#[template(path = "flamegraph.html")]
213pub struct FlamegraphTemplate<'a> {
214 pub version: &'a str,
215 pub title: &'a str,
216 pub mzfg: &'a str,
217}
218
219#[cfg(all(feature = "jemalloc", not(miri)))]
229struct MemProfilingSuspendGuard {
230 ctl: tokio::sync::MutexGuard<'static, jemalloc_pprof::JemallocProfCtl>,
231 memory_was_active: bool,
232}
233
234#[cfg(all(feature = "jemalloc", not(miri)))]
235impl Drop for MemProfilingSuspendGuard {
236 fn drop(&mut self) {
237 if self.memory_was_active {
238 if let Err(e) = self.ctl.resume() {
241 tracing::error!("failed to resume memory profiling after CPU profiling: {e}");
242 }
243 }
244 }
245}
246
247#[allow(dropping_copy_types)]
248async fn capture_cpu_profile(
249 merge_threads: bool,
250 time_secs: u64,
252 sample_freq: u32,
254) -> Result<StackProfile, (StatusCode, String)> {
255 validate_cpu_profile_params(time_secs, sample_freq)?;
256 let _cpu_profiling_guard = CpuProfilingGuard::acquire()?;
257 let ctl_lock;
263 cfg_if! {
264 if #[cfg(any(not(feature = "jemalloc"), miri))] {
265 ctl_lock = ();
266 } else {
267 ctl_lock = match jemalloc_pprof::PROF_CTL.as_ref() {
268 Some(ctl) => {
269 let borrow = ctl.lock().await;
272 let memory_was_active = borrow.activated();
274 if memory_was_active {
278 borrow.pause().map_err(|e| {
279 (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
280 })?;
281 }
282 Some(MemProfilingSuspendGuard {
285 ctl: borrow,
286 memory_was_active,
287 })
288 }
289 None => None,
290 };
291 }
292 }
293 let stacks = unsafe {
296 mz_prof::time::prof_time(Duration::from_secs(time_secs), sample_freq, merge_threads)
297 }
298 .await
299 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
300 drop(ctl_lock);
304 Ok(stacks)
305}
306
307async fn time_prof(
308 merge_threads: bool,
309 build_info: &'static BuildInfo,
310 time_secs: u64,
312 sample_freq: u32,
314) -> impl IntoResponse + use<> {
315 let stacks = capture_cpu_profile(merge_threads, time_secs, sample_freq).await?;
316 let (secs_s, freq_s) = (format!("{time_secs}"), format!("{sample_freq}"));
317 Ok::<_, (StatusCode, String)>(flamegraph(
318 stacks,
319 "CPU Time Flamegraph",
320 false,
321 &[
322 ("Sampling time (s)", &secs_s),
323 ("Sampling frequency (Hz)", &freq_s),
324 ],
325 build_info,
326 ))
327}
328
329fn flamegraph<'a, 'b>(
330 stacks: StackProfile,
331 title: &'a str,
332 display_bytes: bool,
333 extras: &'b [(&'b str, &'b str)],
334 build_info: &'static BuildInfo,
335) -> impl IntoResponse + use<'a> {
336 let mut header_extra = vec![];
337 if display_bytes {
338 header_extra.push(("display_bytes", "1"));
339 }
340 for (k, v) in extras {
341 header_extra.push((k, v));
342 }
343 let mzfg = stacks.to_mzfg(true, &header_extra);
344 mz_http_util::template_response(FlamegraphTemplate {
345 version: build_info.version,
346 title,
347 mzfg: &mzfg,
348 })
349}
350
351#[cfg(any(not(feature = "jemalloc"), miri))]
352mod disabled {
353 use axum::Json;
354 use axum::extract::{Form, Query};
355 use axum::response::IntoResponse;
356 use http::StatusCode;
357 use http::header::HeaderMap;
358 use mz_build_info::BuildInfo;
359 use serde::Deserialize;
360
361 use mz_prof::ever_symbolized;
362
363 use super::{
364 MemProfilingStatus, ModeResponse, ModeUpdateRequest, ProfTemplate, cpu_profiling_active,
365 time_prof,
366 };
367
368 #[derive(Deserialize)]
369 pub struct ProfQuery {
370 _action: Option<String>,
371 }
372
373 #[allow(clippy::unused_async)]
374 pub async fn handle_get(
375 _: Query<ProfQuery>,
376 _: HeaderMap,
377 build_info: &'static BuildInfo,
378 ) -> impl IntoResponse {
379 mz_http_util::template_response(ProfTemplate {
380 version: build_info.version,
381 executable: &super::EXECUTABLE,
382 mem_prof: MemProfilingStatus::Disabled,
383 ever_symbolized: ever_symbolized(),
384 })
385 }
386
387 #[derive(Deserialize)]
388 pub struct ProfForm {
389 action: String,
390 threads: Option<String>,
391 time_secs: Option<u64>,
392 hz: Option<u32>,
393 }
394
395 pub async fn handle_post(
396 Form(ProfForm {
397 action,
398 threads,
399 time_secs,
400 hz,
401 }): Form<ProfForm>,
402 build_info: &'static BuildInfo,
403 ) -> impl IntoResponse {
404 let merge_threads = threads.as_deref() == Some("merge");
405 match action.as_ref() {
406 "time_fg" => {
407 let time_secs = time_secs.ok_or_else(|| {
408 (
409 StatusCode::BAD_REQUEST,
410 "Expected value for `time_secs`".to_owned(),
411 )
412 })?;
413 let hz = hz.ok_or_else(|| {
414 (
415 StatusCode::BAD_REQUEST,
416 "Expected value for `hz`".to_owned(),
417 )
418 })?;
419
420 Ok(time_prof(merge_threads, build_info, time_secs, hz).await)
421 }
422 _ => Err((
423 StatusCode::BAD_REQUEST,
424 format!("unrecognized `action` parameter: {}", action),
425 )),
426 }
427 }
428
429 #[allow(clippy::unused_async)]
430 pub async fn handle_get_mode() -> Json<ModeResponse> {
431 Json(ModeResponse {
432 cpu_active: cpu_profiling_active(),
433 memory_available: false,
434 memory_active: false,
435 })
436 }
437
438 pub async fn handle_post_mode(
439 Json(request): Json<ModeUpdateRequest>,
440 ) -> Result<Json<ModeResponse>, (StatusCode, String)> {
441 if request.memory_active == Some(true) {
442 return Err((
443 StatusCode::FORBIDDEN,
444 "memory profiling is unavailable in this build".to_owned(),
445 ));
446 }
447 Ok(handle_get_mode().await)
448 }
449
450 #[allow(clippy::unused_async)]
451 pub async fn handle_get_heap() -> Result<(), (StatusCode, String)> {
452 Err((
453 StatusCode::BAD_REQUEST,
454 "This software was compiled without heap profiling support.".to_string(),
455 ))
456 }
457}
458
459#[cfg(all(feature = "jemalloc", not(miri)))]
460mod enabled {
461 use std::io::{BufReader, Read};
462 use std::sync::Arc;
463
464 use axum::Json;
465 use axum::extract::{Form, Query};
466 use axum::response::IntoResponse;
467 use axum_extra::TypedHeader;
468 use bytesize::ByteSize;
469 use headers::ContentType;
470 use http::header::{CONTENT_DISPOSITION, HeaderMap};
471 use http::{HeaderValue, StatusCode};
472 use jemalloc_pprof::{JemallocProfCtl, PROF_CTL};
473 use mappings::MAPPINGS;
474 use mz_build_info::BuildInfo;
475 use mz_ore::cast::CastFrom;
476 use mz_prof::jemalloc::{JemallocProfCtlExt, JemallocStats};
477 use mz_prof::{StackProfileExt, ever_symbolized};
478 use pprof_util::parse_jeheap;
479 use serde::Deserialize;
480 use tokio::sync::Mutex;
481
482 use super::{
483 MemProfilingStatus, ModeResponse, ModeUpdateRequest, ProfTemplate, cpu_profiling_active,
484 flamegraph, time_prof,
485 };
486
487 #[derive(Deserialize)]
488 pub struct ProfForm {
489 action: String,
490 threads: Option<String>,
491 time_secs: Option<u64>,
492 hz: Option<u32>,
493 }
494
495 pub async fn handle_post(
496 Form(ProfForm {
497 action,
498 threads,
499 time_secs,
500 hz,
501 }): Form<ProfForm>,
502 build_info: &'static BuildInfo,
503 ) -> impl IntoResponse {
504 let prof_ctl = PROF_CTL.as_ref().unwrap();
505 let merge_threads = threads.as_deref() == Some("merge");
506
507 fn render_jemalloc_stats(stats: &JemallocStats) -> Vec<(&str, String)> {
508 let stats = [
509 ("Allocated", stats.allocated),
510 ("In active pages", stats.active),
511 ("Allocated for allocator metadata", stats.metadata),
512 (
513 "Maximum number of bytes in physically resident data pages mapped by the allocator",
514 stats.resident,
515 ),
516 ("Bytes unused, but retained by allocator", stats.retained),
517 ];
518 stats
519 .into_iter()
520 .map(|(k, v)| (k, ByteSize(u64::cast_from(v)).display().si().to_string()))
521 .collect()
522 }
523
524 match action.as_str() {
525 "activate" => {
526 {
527 let mut borrow = prof_ctl.lock().await;
528 borrow
529 .activate()
530 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
531 };
532 Ok(render_template(prof_ctl, build_info).await.into_response())
533 }
534 "deactivate" => {
535 {
536 let mut borrow = prof_ctl.lock().await;
537 borrow
538 .deactivate()
539 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
540 };
541 Ok(render_template(prof_ctl, build_info).await.into_response())
542 }
543 "dump_jeheap" => {
544 let mut borrow = prof_ctl.lock().await;
545 require_profiling_activated(&borrow)?;
546 let mut f = borrow
547 .dump()
548 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
549 let mut s = String::new();
550 f.read_to_string(&mut s)
551 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
552 Ok((
553 HeaderMap::from_iter([(
554 CONTENT_DISPOSITION,
555 HeaderValue::from_static("attachment; filename=\"jeprof.heap\""),
556 )]),
557 s,
558 )
559 .into_response())
560 }
561 "dump_sym_mzfg" => {
562 let mut borrow = prof_ctl.lock().await;
563 require_profiling_activated(&borrow)?;
564 let f = borrow
565 .dump()
566 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
567 let r = BufReader::new(f);
568 let stacks = parse_jeheap(r, MAPPINGS.as_deref())
569 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
570 let stats = borrow
571 .stats()
572 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
573 let stats_rendered = render_jemalloc_stats(&stats);
574 let mut header = stats_rendered
575 .iter()
576 .map(|(k, v)| (*k, v.as_str()))
577 .collect::<Vec<_>>();
578 header.push(("display_bytes", "1"));
579 let mzfg = stacks.to_mzfg(true, &header);
580 Ok((
581 HeaderMap::from_iter([(
582 CONTENT_DISPOSITION,
583 HeaderValue::from_static("attachment; filename=\"trace.mzfg\""),
584 )]),
585 mzfg,
586 )
587 .into_response())
588 }
589 "mem_fg" => {
590 let mut borrow = prof_ctl.lock().await;
591 require_profiling_activated(&borrow)?;
592 let f = borrow
593 .dump()
594 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
595 let r = BufReader::new(f);
596 let stacks = parse_jeheap(r, MAPPINGS.as_deref())
597 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
598 let stats = borrow
599 .stats()
600 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
601 let stats_rendered = render_jemalloc_stats(&stats);
602 let stats_rendered = stats_rendered
603 .iter()
604 .map(|(k, v)| (*k, v.as_str()))
605 .collect::<Vec<_>>();
606 Ok(
607 flamegraph(stacks, "Heap Flamegraph", true, &stats_rendered, build_info)
608 .into_response(),
609 )
610 }
611 "time_fg" => {
612 let time_secs = time_secs.ok_or_else(|| {
613 (
614 StatusCode::BAD_REQUEST,
615 "Expected value for `time_secs`".to_owned(),
616 )
617 })?;
618 let hz = hz.ok_or_else(|| {
619 (
620 StatusCode::BAD_REQUEST,
621 "Expected value for `hz`".to_owned(),
622 )
623 })?;
624 Ok(time_prof(merge_threads, build_info, time_secs, hz)
625 .await
626 .into_response())
627 }
628 x => Err((
629 StatusCode::BAD_REQUEST,
630 format!("unrecognized `action` parameter: {}", x),
631 )),
632 }
633 }
634
635 #[derive(Deserialize)]
636 pub struct ProfQuery {
637 action: Option<String>,
638 }
639
640 pub async fn handle_get(
641 Query(query): Query<ProfQuery>,
642 headers: HeaderMap,
643 build_info: &'static BuildInfo,
644 ) -> impl IntoResponse {
645 let prof_ctl = PROF_CTL.as_ref().unwrap();
646 match query.action.as_deref() {
647 Some("dump_stats") => {
648 let json = headers
649 .get("accept")
650 .map_or(false, |accept| accept.as_bytes() == b"application/json");
651 let mut borrow = prof_ctl.lock().await;
652 let s = borrow
653 .dump_stats(json)
654 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
655 let content_type = match json {
656 false => ContentType::text(),
657 true => ContentType::json(),
658 };
659 Ok((TypedHeader(content_type), s).into_response())
660 }
661 Some(x) => Err((
662 StatusCode::BAD_REQUEST,
663 format!("unrecognized query: {}", x),
664 )),
665 None => Ok(render_template(prof_ctl, build_info).await.into_response()),
666 }
667 }
668
669 pub async fn handle_get_mode() -> Json<ModeResponse> {
670 let memory_active = current_memory_mode().await;
671 Json(mode_response(memory_active, prof_ctl().is_some()))
672 }
673
674 pub async fn handle_post_mode(
675 Json(request): Json<ModeUpdateRequest>,
676 ) -> Result<Json<ModeResponse>, (StatusCode, String)> {
677 if let Some(desired_memory_active) = request.memory_active {
678 if desired_memory_active && cpu_profiling_active() {
683 return Err((
684 StatusCode::CONFLICT,
685 "memory profiling cannot be activated while CPU profiling is running"
686 .to_owned(),
687 ));
688 }
689 let prof_ctl = prof_ctl().ok_or_else(|| {
690 (
691 StatusCode::FORBIDDEN,
692 "memory profiling is unavailable in this build".to_owned(),
693 )
694 })?;
695 let mut borrow = prof_ctl.lock().await;
696 if desired_memory_active && !borrow.activated() {
697 borrow
698 .activate()
699 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
700 } else if !desired_memory_active && borrow.activated() {
701 borrow
702 .deactivate()
703 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
704 }
705 return Ok(Json(mode_response(borrow.activated(), true)));
706 }
707 Ok(Json(mode_response(
708 current_memory_mode().await,
709 prof_ctl().is_some(),
710 )))
711 }
712
713 pub async fn handle_get_heap() -> Result<impl IntoResponse, (StatusCode, String)> {
714 let mut prof_ctl = PROF_CTL.as_ref().unwrap().lock().await;
715 require_profiling_activated(&prof_ctl)?;
716 let dump_file = prof_ctl
717 .dump()
718 .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
719 let dump_reader = BufReader::new(dump_file);
720 let profile = parse_jeheap(dump_reader, MAPPINGS.as_deref())
721 .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
722 let pprof = profile.to_pprof(("inuse_space", "bytes"), ("space", "bytes"), None);
723 Ok(pprof)
724 }
725
726 async fn render_template(
727 prof_ctl: &Arc<Mutex<JemallocProfCtl>>,
728 build_info: &'static BuildInfo,
729 ) -> impl IntoResponse {
730 let prof_md = prof_ctl.lock().await.get_md();
731 mz_http_util::template_response(ProfTemplate {
732 version: build_info.version,
733 executable: &super::EXECUTABLE,
734 mem_prof: MemProfilingStatus::Enabled(prof_md.start_time),
735 ever_symbolized: ever_symbolized(),
736 })
737 }
738
739 fn require_profiling_activated(prof_ctl: &JemallocProfCtl) -> Result<(), (StatusCode, String)> {
741 if prof_ctl.activated() {
742 Ok(())
743 } else {
744 Err((StatusCode::FORBIDDEN, "heap profiling not activated".into()))
745 }
746 }
747
748 async fn current_memory_mode() -> bool {
749 match prof_ctl() {
750 Some(prof_ctl) => prof_ctl.lock().await.activated(),
751 None => false,
752 }
753 }
754
755 fn prof_ctl() -> Option<Arc<Mutex<JemallocProfCtl>>> {
756 mz_ore::panic::catch_unwind(|| PROF_CTL.as_ref().cloned())
763 .ok()
764 .flatten()
765 }
766
767 fn mode_response(memory_active: bool, memory_available: bool) -> ModeResponse {
768 ModeResponse {
769 cpu_active: cpu_profiling_active(),
770 memory_available,
771 memory_active,
772 }
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use axum::Json;
779 use http::StatusCode;
780
781 use super::{
782 CpuProfileRequest, CpuProfilingGuard, MAX_CPU_PROFILE_TIME_SECS, ModeResponse,
783 ModeUpdateRequest, handle_get_mode, handle_post_cpu, handle_post_mode,
784 };
785
786 static CPU_GUARD_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
789
790 fn expect_error(
791 result: Result<impl axum::response::IntoResponse, (StatusCode, String)>,
792 msg: &str,
793 ) -> StatusCode {
794 match result {
795 Ok(_) => panic!("{}", msg),
796 Err((status, _)) => status,
797 }
798 }
799
800 #[mz_ore::test(tokio::test)]
801 async fn post_cpu_rejects_zero_seconds() {
802 let result = handle_post_cpu(Json(CpuProfileRequest {
803 seconds: 0,
804 hz: 99,
805 merge_threads: false,
806 }))
807 .await;
808 let status = expect_error(result, "zero-second capture must fail");
809 assert_eq!(status, StatusCode::BAD_REQUEST);
810 }
811
812 #[mz_ore::test(tokio::test)]
813 async fn post_cpu_rejects_zero_hz() {
814 let result = handle_post_cpu(Json(CpuProfileRequest {
815 seconds: 1,
816 hz: 0,
817 merge_threads: false,
818 }))
819 .await;
820 let status = expect_error(result, "zero-hz capture must fail");
821 assert_eq!(status, StatusCode::BAD_REQUEST);
822 }
823
824 #[mz_ore::test(tokio::test)]
825 async fn post_cpu_rejects_excessive_seconds() {
826 let result = handle_post_cpu(Json(CpuProfileRequest {
827 seconds: MAX_CPU_PROFILE_TIME_SECS + 1,
828 hz: 99,
829 merge_threads: false,
830 }))
831 .await;
832 let status = expect_error(result, "over-long capture must fail");
833 assert_eq!(status, StatusCode::BAD_REQUEST);
834 }
835
836 #[mz_ore::test(tokio::test)]
837 async fn post_cpu_conflicts_with_running_capture() {
838 let _lock = CPU_GUARD_LOCK.lock().await;
839 let _guard = CpuProfilingGuard::acquire().expect("no capture running");
840 let result = handle_post_cpu(Json(CpuProfileRequest {
841 seconds: 1,
842 hz: 99,
843 merge_threads: false,
844 }))
845 .await;
846 let status = expect_error(result, "concurrent capture must fail");
847 assert_eq!(status, StatusCode::CONFLICT);
848 }
849
850 #[mz_ore::test(tokio::test)]
851 async fn get_mode_reports_cpu_active_state() {
852 let _lock = CPU_GUARD_LOCK.lock().await;
853 let guard = CpuProfilingGuard::acquire().expect("no capture running");
854 let Json(ModeResponse { cpu_active, .. }) = handle_get_mode().await;
855 assert!(cpu_active);
856 drop(guard);
857 let Json(ModeResponse { cpu_active, .. }) = handle_get_mode().await;
858 assert!(!cpu_active);
859 }
860
861 #[mz_ore::test(tokio::test)]
862 async fn post_mode_without_memory_change_is_noop() {
863 let _lock = CPU_GUARD_LOCK.lock().await;
864 let _guard = CpuProfilingGuard::acquire().expect("no capture running");
865 let result = handle_post_mode(Json(ModeUpdateRequest {
866 memory_active: None,
867 }))
868 .await;
869 let _ = result.expect("mode update without a memory change must succeed");
870 }
871
872 #[cfg(any(not(feature = "jemalloc"), miri))]
873 #[mz_ore::test(tokio::test)]
874 async fn post_mode_rejects_memory_activation_when_unavailable() {
875 let result = handle_post_mode(Json(ModeUpdateRequest {
876 memory_active: Some(true),
877 }))
878 .await;
879 let (status, _) = result.expect_err("memory activation must fail without jemalloc");
880 assert_eq!(status, StatusCode::FORBIDDEN);
881 }
882
883 #[cfg(all(feature = "jemalloc", not(miri)))]
884 #[mz_ore::test(tokio::test)]
885 async fn get_mode_handles_optional_memory_support() {
886 let Json(ModeResponse {
887 memory_available,
888 memory_active,
889 ..
890 }) = handle_get_mode().await;
891 assert!(!memory_active || memory_available);
892 }
893
894 #[cfg(all(feature = "jemalloc", not(miri)))]
899 #[mz_ore::test(tokio::test)]
900 async fn cpu_capture_suspends_then_restores_memory_profiling() {
901 use super::{capture_cpu_profile, cpu_profiling_active};
902
903 let _lock = CPU_GUARD_LOCK.lock().await;
904
905 let Json(ModeResponse {
909 memory_available, ..
910 }) = handle_get_mode().await;
911 if !memory_available {
912 eprintln!(
913 "skipping cpu_capture_suspends_then_restores_memory_profiling: \
914 jemalloc memory profiling is unavailable in this build"
915 );
916 return;
917 }
918
919 let Json(before) = handle_post_mode(Json(ModeUpdateRequest {
921 memory_active: Some(true),
922 }))
923 .await
924 .expect("memory activation must succeed");
925 assert!(before.memory_active, "memory profiling should start active");
926 assert!(!before.cpu_active, "cpu profiling should start inactive");
927
928 let capture =
933 mz_ore::task::spawn(|| "cpu-capture-test", capture_cpu_profile(false, 1, 100));
934 while !cpu_profiling_active() && !capture.is_finished() {
935 tokio::task::yield_now().await;
936 }
937 assert!(
938 cpu_profiling_active(),
939 "the capture should have activated cpu profiling"
940 );
941 let conflict = handle_post_mode(Json(ModeUpdateRequest {
944 memory_active: Some(true),
945 }))
946 .await;
947 assert_eq!(
948 expect_error(conflict, "memory activation must be rejected mid-capture"),
949 StatusCode::CONFLICT,
950 );
951
952 let _ = capture.await.expect("cpu capture must succeed");
955 let Json(after) = handle_get_mode().await;
956 assert!(
957 !after.cpu_active,
958 "cpu profiling should be inactive after the capture"
959 );
960 assert!(
961 after.memory_active,
962 "memory profiling should be restored after the capture"
963 );
964 }
965}