diff --git a/CHANGELOG.md b/CHANGELOG.md index 43bf64f43..2bfa1c627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] - 2025-02-16 ### New Features +- Add performance sampler to record CPU and memory usage during execution. - Add format check for command-line arguments. - Pointer analysis - Add special handling for zero-length arrays to enhance PTA precision. diff --git a/docs/en/command-line-options.adoc b/docs/en/command-line-options.adoc index 1d2b58aa9..2a6caec7e 100644 --- a/docs/en/command-line-options.adoc +++ b/docs/en/command-line-options.adoc @@ -132,6 +132,11 @@ By default, Tai-e keeps results of all executed analyses in memory. If you run m * Specify output directory (--output-dir): `--output-dir ` ** By default, Tai-e stores all outputs, such as logs, IR, and various analysis results, in the `output` folder within the current working directory. If you prefer to save outputs to a different directory, simply use this option. +* Performance sampling (--performance-sampling) +** Enable periodic sampling of CPU and memory usage during execution. +** Results are saved as `tai-e-performance.json` in the output directory, containing system info, execution metadata, and time-series performance data. Useful for analyzing and comparing resource usage across runs. + + == A Usage Example of Command-Line Options We give an example of how to analyze a program by Tai-e. Suppose we want to analyze a program _P_ as described below: diff --git a/src/main/java/pascal/taie/Main.java b/src/main/java/pascal/taie/Main.java index d84887fd2..01322b3a6 100644 --- a/src/main/java/pascal/taie/Main.java +++ b/src/main/java/pascal/taie/Main.java @@ -35,6 +35,7 @@ import pascal.taie.config.PlanConfig; import pascal.taie.config.Scope; import pascal.taie.frontend.cache.CachedWorldBuilder; +import pascal.taie.util.PerformanceSampler; import pascal.taie.util.RuntimeInfoLogger; import pascal.taie.util.Timer; import pascal.taie.util.collection.Lists; @@ -58,8 +59,16 @@ public static void main(String... args) { logger.info("No analyses are specified"); System.exit(0); } + PerformanceSampler sampler = null; + if (options.isPerformanceSampling()) { + sampler = new PerformanceSampler(options.getOutputDir()); + sampler.start(); + } buildWorld(options, plan.analyses()); executePlan(plan); + if (sampler != null) { + sampler.stop(); + } }, "Tai-e"); LoggerConfigs.reconfigure(); } diff --git a/src/main/java/pascal/taie/config/Options.java b/src/main/java/pascal/taie/config/Options.java index add8a63b7..31eabe1ce 100644 --- a/src/main/java/pascal/taie/config/Options.java +++ b/src/main/java/pascal/taie/config/Options.java @@ -200,6 +200,17 @@ public File getOutputDir() { return outputDir; } + @JsonProperty + @Option(names = "--performance-sampling", + description = "Record performance metrics (e.g., CPU and memory usage)" + + " during execution (default: ${DEFAULT-VALUE})", + defaultValue = "false") + private boolean performanceSampling; + + public boolean isPerformanceSampling() { + return performanceSampling; + } + @JsonProperty @Option(names = "--pre-build-ir", description = "Build IR for all available methods before" + diff --git a/src/main/java/pascal/taie/util/PerformanceSampler.java b/src/main/java/pascal/taie/util/PerformanceSampler.java new file mode 100644 index 000000000..f4438a4a4 --- /dev/null +++ b/src/main/java/pascal/taie/util/PerformanceSampler.java @@ -0,0 +1,231 @@ +/* + * Tai-e: A Static Analysis Framework for Java + * + * Copyright (C) 2022 Tian Tan + * Copyright (C) 2022 Yue Li + * + * This file is part of Tai-e. + * + * Tai-e is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Tai-e is distributed in the hope that it will be useful,but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General + * Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Tai-e. If not, see . + */ + +package pascal.taie.util; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.sun.management.OperatingSystemMXBean; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Performance sampler for collecting system and JVM performance metrics during execution. + * Supports automatic sampling at configurable intervals and outputs data in JSON format. + */ +public class PerformanceSampler { + + private static final Logger logger = LogManager.getLogger(PerformanceSampler.class); + + public static final String OUTPUT_FILE = "tai-e-performance.json"; + + /** + * Sampling interval in seconds + */ + private static final int INTERVAL = 1; + + private final File outputFile; + + private final ScheduledExecutorService scheduler; + + private final OperatingSystemMXBean osBean; + + private final MemoryMXBean memoryBean; + + private final List samples; + + /** + * Start time of the performance sampling. + * -1 indicates that sampling has not started yet. + */ + private long startTime = -1; + + /** + * Finish time of the performance sampling. + * -1 indicates that sampling has not finished yet. + */ + private long finishTime = -1; + + /** + * Creates a new PerformanceSampler instance. + */ + public PerformanceSampler(File outputDir) { + this.outputFile = new File(outputDir, OUTPUT_FILE); + this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, this.getClass().getName()); + t.setDaemon(true); + return t; + }); + this.osBean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); + this.memoryBean = ManagementFactory.getMemoryMXBean(); + this.samples = new ArrayList<>(); + } + + /** + * Starts performance sampling. Records start time and begins periodic sampling. + */ + public void start() { + if (startTime != -1) { + throw new IllegalStateException("Performance sampling has already started"); + } + this.startTime = System.currentTimeMillis(); + scheduler.scheduleAtFixedRate(this::collectSample, + 0, INTERVAL, TimeUnit.SECONDS); + } + + /** + * Stops performance sampling, records finish time, and saves results to JSON file. + */ + public void stop() { + if (startTime == -1) { + throw new IllegalStateException("Performance sampling has not started yet"); + } + if (finishTime != -1) { + throw new IllegalStateException("Performance sampling has already finished"); + } + this.finishTime = System.currentTimeMillis(); + scheduler.shutdown(); + try { + if (!scheduler.awaitTermination(2, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + } + } catch (InterruptedException e) { + scheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + saveToFile(); + } + + /** + * Collects a single performance sample including CPU and memory usage. + */ + private void collectSample() { + try { + long timestamp = System.currentTimeMillis(); + + // Get CPU usage and handle negative values indicating unavailable data + double processCpuUsage = osBean.getProcessCpuLoad(); + if (processCpuUsage < 0) { + processCpuUsage = 0.0; + } + double systemCpuUsage = osBean.getCpuLoad(); + if (systemCpuUsage < 0) { + systemCpuUsage = 0.0; + } + + // Get JVM process memory usage (heap + non-heap) + long heapMemoryUsed = memoryBean.getHeapMemoryUsage().getUsed(); + long nonHeapMemoryUsed = memoryBean.getNonHeapMemoryUsage().getUsed(); + long processMemoryUsedMB = (heapMemoryUsed + nonHeapMemoryUsed) / (1024 * 1024); + + // Get total system memory usage + long totalMemory = osBean.getTotalMemorySize(); + long freeMemory = osBean.getFreeMemorySize(); + long systemMemoryUsedMB = (totalMemory - freeMemory) / (1024 * 1024); + + Sample sample = new Sample(timestamp, processCpuUsage, + systemCpuUsage, processMemoryUsedMB, systemMemoryUsedMB); + + synchronized (samples) { + samples.add(sample); + } + } catch (Exception e) { + // Log error but continue sampling + logger.error("Error collecting performance sample: {}", e.getMessage()); + } + } + + /** + * Saves performance data to JSON file. + */ + private void saveToFile() { + logger.info("Saving performance report to: {}", outputFile); + try { + String version = RuntimeInfoLogger.getVersion(); + String commit = RuntimeInfoLogger.getCommit(); + String operatingSystem = System.getProperty("os.name") + + " (" + System.getProperty("os.arch") + ")"; + String javaRuntime = System.getProperty("java.vendor") + + " " + System.getProperty("java.runtime.name") + + " " + System.getProperty("java.runtime.version"); + String username = System.getProperty("user.name"); + int cpuCores = Runtime.getRuntime().availableProcessors(); + long memoryMB = osBean.getTotalMemorySize() / (1024 * 1024); + long startTime = this.startTime; + long finishTime = this.finishTime; + List samples; + synchronized (this.samples) { + samples = new ArrayList<>(this.samples); + } + + PerformanceReport report = new PerformanceReport( + version, commit, operatingSystem, javaRuntime, + username, cpuCores, memoryMB, startTime, + finishTime, samples); + + ObjectMapper mapper = new ObjectMapper(); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + mapper.writeValue(outputFile, report); + } catch (IOException e) { + logger.error("Failed to write performance report: {}", e.getMessage()); + } + } + + /** + * Main performance report structure for JSON serialization. + */ + private record PerformanceReport( + @JsonProperty("version") String version, + @JsonProperty("commit") String commit, + @JsonProperty("operatingSystem") String operatingSystem, + @JsonProperty("javaRuntime") String javaRuntime, + @JsonProperty("username") String username, + @JsonProperty("cpuCores") int cpuCores, + @JsonProperty("memoryMB") long memoryMB, + @JsonProperty("startTime") long startTime, + @JsonProperty("finishTime") long finishTime, + @JsonProperty("samples") List samples) { + } + + /** + * Individual performance sample data point. + */ + private record Sample( + @JsonProperty("timestamp") long timestamp, + @JsonProperty("processCpuUsage") double processCpuUsage, + @JsonProperty("systemCpuUsage") double systemCpuUsage, + @JsonProperty("processMemoryUsedMB") long processMemoryUsedMB, + @JsonProperty("systemMemoryUsedMB") long systemMemoryUsedMB) { + } + +} diff --git a/src/main/java/pascal/taie/util/RuntimeInfoLogger.java b/src/main/java/pascal/taie/util/RuntimeInfoLogger.java index 829cf8a4d..fb929c6be 100644 --- a/src/main/java/pascal/taie/util/RuntimeInfoLogger.java +++ b/src/main/java/pascal/taie/util/RuntimeInfoLogger.java @@ -83,16 +83,34 @@ private static void logEnvInfo() { */ private static void logTaieInfo() { Properties properties = getBuildProperties(); - String version = properties != null - ? properties.getProperty(VERSION_KEY) - : UNKNOWN; + String version = getVersion(properties); logger.info("Tai-e Version: {}", version); - String commit = properties != null - ? properties.getProperty(COMMIT_KEY) - : UNKNOWN; + String commit = getCommit(properties); logger.info("Tai-e Commit: {}", commit); } + public static String getVersion() { + return getVersion(getBuildProperties()); + } + + private static String getVersion(@Nullable Properties manifest) { + if (manifest != null) { + return manifest.getProperty(VERSION_KEY, UNKNOWN); + } + return UNKNOWN; + } + + public static String getCommit() { + return getCommit(getBuildProperties()); + } + + private static String getCommit(@Nullable Properties manifest) { + if (manifest != null) { + return manifest.getProperty(COMMIT_KEY, UNKNOWN); + } + return UNKNOWN; + } + /** * Retrieves the build properties of the current JAR file, if available. * diff --git a/src/test/java/pascal/taie/analysis/pta/PointerAnalysisResultTest.java b/src/test/java/pascal/taie/analysis/pta/PointerAnalysisResultTest.java index 9a24ef696..ecd69c488 100644 --- a/src/test/java/pascal/taie/analysis/pta/PointerAnalysisResultTest.java +++ b/src/test/java/pascal/taie/analysis/pta/PointerAnalysisResultTest.java @@ -1,3 +1,25 @@ +/* + * Tai-e: A Static Analysis Framework for Java + * + * Copyright (C) 2022 Tian Tan + * Copyright (C) 2022 Yue Li + * + * This file is part of Tai-e. + * + * Tai-e is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Tai-e is distributed in the hope that it will be useful,but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General + * Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Tai-e. If not, see . + */ + package pascal.taie.analysis.pta; import org.junit.jupiter.api.Test; diff --git a/src/test/java/pascal/taie/util/PerformanceSamplerTest.java b/src/test/java/pascal/taie/util/PerformanceSamplerTest.java new file mode 100644 index 000000000..0c7ef28d3 --- /dev/null +++ b/src/test/java/pascal/taie/util/PerformanceSamplerTest.java @@ -0,0 +1,68 @@ +/* + * Tai-e: A Static Analysis Framework for Java + * + * Copyright (C) 2022 Tian Tan + * Copyright (C) 2022 Yue Li + * + * This file is part of Tai-e. + * + * Tai-e is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Tai-e is distributed in the hope that it will be useful,but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General + * Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Tai-e. If not, see . + */ + +package pascal.taie.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PerformanceSamplerTest { + + @Test + void unitTest() throws Exception { + File outputDir = new File("output"); + File outputFile = new File(outputDir, PerformanceSampler.OUTPUT_FILE); + outputFile.delete(); + PerformanceSampler sampler = new PerformanceSampler(outputDir); + sampler.start(); + Thread.sleep(1000); + sampler.stop(); + // check the output file + assertTrue(outputFile.exists()); + ObjectMapper mapper = new ObjectMapper(); + JsonNode json = mapper.readTree(outputFile); + assertNotNull(json.get("startTime")); + assertNotNull(json.get("finishTime")); + assertFalse(json.get("version").asText().isBlank()); + assertFalse(json.get("commit").asText().isBlank()); + assertFalse(json.get("samples").isEmpty()); + } + + @Test + void integrationTest() { + pascal.taie.Main.main( + "--performance-sampling", + "-pp", + "-cp", "src/test/resources/pta/basic", + "-m", "New", + "-a", "pta=implicit-entries:false;only-app:true;" + ); + } + +}