Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 5 additions & 23 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,27 +69,10 @@ jobs:
with:
name: pollcatch-decoder
path: ./decoder/target/debug/pollcatch-decoder
build-async-profiler:
name: Build async-profiler
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install async-profiler Dependencies
run: sudo apt-get update && sudo apt-get install -y sudo libicu-dev patchelf curl make g++ openjdk-11-jdk-headless gcovr
- name: Build async-profiler
working-directory: tests
shell: bash
run: ./build-async-profiler.sh
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: libasyncProfiler
path: ./tests/async-profiler/build/lib/libasyncProfiler.so
test:
name: Integration Test
runs-on: ubuntu-latest
needs: [build-for-testing, build-decoder, build-async-profiler]
needs: [build-for-testing, build-decoder]
steps:
- uses: actions/checkout@v4
- name: Download pollcatch-decoder
Expand All @@ -102,11 +85,10 @@ jobs:
with:
name: example-simple
path: ./tests
- name: Download libasyncProfiler
uses: actions/download-artifact@v4
with:
name: libasyncProfiler
path: ./tests
- name: Download async-profiler
shell: bash
working-directory: tests
run: wget https://github.com/async-profiler/async-profiler/releases/download/v4.1/async-profiler-4.1-linux-x64.tar.gz -O async-profiler.tar.gz && tar xvf async-profiler.tar.gz && mv -vf async-profiler-*/lib/libasyncProfiler.so .
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need some kind of recurring task to keep this up to date? Or is there a way to get the URL for the latest release? Alternatively I guess we could vend this version somehow?

I just want to avoid the case where we're testing on a different version than our customers are using, especially since pollcatch is using those unstable APIs

- name: Run integration test
shell: bash
working-directory: tests
Expand Down
71 changes: 49 additions & 22 deletions decoder/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ enum Commands {
#[arg(long, default_value = "5")]
stack_depth: usize,
},
/// Print the total duration (in seconds) of the JFR recording. Used in the integration tests.
Duration {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if we made this show (and currently it only prints duration) but it could be a generally useful place to put metadata that people can use?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is intended for use by a very specific shell script

/// JFR file to read from
jfr_file: OsString,
/// If true, unzip first
#[arg(long)]
zip: bool,
},
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
Expand All @@ -78,6 +86,24 @@ fn extract_async_profiler_jfr_from_zip(file: File) -> anyhow::Result<Option<Vec<
Ok(None)
}

// doing this as a macro because functions are not higher-order enough
// extract events from $jfr_file, $zip decides if it's a zip, then run $f($FILE, input)
macro_rules! extract_events_from_path {
($jfr_file:expr, $zip:expr, $f:expr, $input:expr) => {{
let mut jfr_file = std::fs::File::open($jfr_file)?;
match $zip {
false => $f(&mut jfr_file, $input),
true => {
if let Some(data) = extract_async_profiler_jfr_from_zip(jfr_file)? {
$f(&mut Cursor::new(&data), $input)
} else {
anyhow::bail!("no async_profiler_dump_0.jfr file found");
}
}
}
}};
}

fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
tracing_subscriber::fmt::init();
Expand All @@ -89,21 +115,11 @@ fn main() -> anyhow::Result<()> {
zip,
include_non_pollcatch,
} => {
let mut jfr_file = std::fs::File::open(jfr_file)?;
let config = LongPollConfig {
min_length,
include_non_pollcatch,
};
let samples = match zip {
false => jfr_samples(&mut jfr_file, &config)?,
true => {
if let Some(data) = extract_async_profiler_jfr_from_zip(jfr_file)? {
jfr_samples(&mut Cursor::new(&data), &config)?
} else {
anyhow::bail!("no async_profiler_dump_0.jfr file found");
}
}
};
let samples = extract_events_from_path!(jfr_file, zip, jfr_samples, &config)?;
print_samples(&mut io::stdout(), samples, stack_depth).ok();
Ok(())
}
Expand All @@ -113,20 +129,15 @@ fn main() -> anyhow::Result<()> {
zip,
stack_depth,
} => {
let mut jfr_file = std::fs::File::open(jfr_file)?;
let events = match zip {
false => jfr_native_mem_events(&mut jfr_file, &type_)?,
true => {
if let Some(data) = extract_async_profiler_jfr_from_zip(jfr_file)? {
jfr_native_mem_events(&mut Cursor::new(&data), &type_)?
} else {
anyhow::bail!("no async_profiler_dump_0.jfr file found");
}
}
};
let events = extract_events_from_path!(jfr_file, zip, jfr_native_mem_events, &type_)?;
print_native_mem_events(&mut io::stdout(), events, &type_, stack_depth).ok();
Ok(())
}
Commands::Duration { jfr_file, zip } => {
let duration = extract_events_from_path!(jfr_file, zip, jfr_duration, ())?;
println!("{}", duration.as_secs_f64());
Ok(())
}
}
}

Expand Down Expand Up @@ -659,6 +670,19 @@ where
Ok(samples)
}

fn jfr_duration<T>(reader: &mut T, _config: ()) -> anyhow::Result<Duration>
where
T: Read + Seek,
{
let mut jfr_reader = JfrReader::new(reader);

let total_nanos = jfr_reader
.chunks()
.map(|i| i.map(|i| i.1.header.duration_nanos))
.sum::<Result<i64, _>>()?;
Ok(Duration::from_nanos(total_nanos as u64))
}

#[derive(Debug)]
struct NativeMemEvent {
start_time: Duration,
Expand Down Expand Up @@ -934,6 +958,9 @@ mod test {
}
print_samples(&mut to, samples, 4).unwrap();
assert_eq!(String::from_utf8(to).unwrap(), result);

let duration = crate::jfr_duration(&mut io::Cursor::new(jfr), ()).unwrap();
assert_eq!(duration.as_nanos(), 15003104000);
}

#[test]
Expand Down
4 changes: 3 additions & 1 deletion examples/simple/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ async fn main_internal(args: Args) -> Result<(), anyhow::Error> {
.build();

tracing::info!("starting profiler");
profiler.spawn()?;
let handle = profiler.spawn_controllable()?;
tracing::info!("profiler started");

if let Some(timeout) = args.duration {
Expand All @@ -145,5 +145,7 @@ async fn main_internal(args: Args) -> Result<(), anyhow::Error> {
slow::run().await;
}

handle.stop().await;

Ok(())
}
3 changes: 2 additions & 1 deletion examples/simple/slow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
#[allow(deprecated)]
fn accidentally_slow() {
std::thread::sleep_ms(10);
std::hint::black_box(0);
// do a phony allocation for the allocation profiler
std::hint::black_box(Vec::<u8>::with_capacity(1 << 20));
}

#[inline(never)]
Expand Down
16 changes: 16 additions & 0 deletions tests/integration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,18 @@ rm -f profiles/*.jfr
# Pass --worker-threads 16 to make the test much less flaky since there is always some worker thread running
./simple --local profiles --duration 30s --reporting-interval 10s --worker-threads 16 --native-mem 4k

found_good=0

for profile in profiles/*.jfr; do
duration=$(./pollcatch-decoder duration "$profile")
# Ignore "partial" profiles of less than 8s
if [[ $duration > 8 ]]; then
found_good=1
else
echo "Profile $profile is too short"
continue
fi

# Basic event presence check
native_malloc_count=$(./pollcatch-decoder nativemem --type malloc "$profile" | wc -l)
if [ "$native_malloc_count" -lt 1 ]; then
Expand Down Expand Up @@ -48,3 +59,8 @@ for profile in profiles/*.jfr; do
exit 1
fi
done

if [ "$found_good" -eq 0 ]; then
echo Found no good profiles
exit 1
fi
Loading