/* * Sun Public License * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is available at http://www.sun.com/ * * The Original Code is the SLAMD Distributed Load Generation Engine. * The Initial Developer of the Original Code is Neil A. Wilson. * Portions created by Neil A. Wilson are Copyright (C) 2004-2010. * Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): Neil A. Wilson */ package com.slamd.resourcemonitor; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.StringTokenizer; import com.slamd.common.Constants; import com.slamd.common.SLAMDException; import com.slamd.job.JobClass; import com.slamd.stat.IntegerValueTracker; import com.slamd.stat.RealTimeStatReporter; import com.slamd.stat.StatTracker; /** * This class defines a SLAMD resource monitor that uses command-line utilities * to monitor the size of a specified process. The process may be identified * by name or by PID, and the virtual size will be used for the process. This * resource monitor will operate properly on Solaris (both SPARC and x86). It * will also work for single-threaded processes on Linux with any threading * library, or for multi-threaded processes on Linux systems with NPTL support. * * * @author Neil A. Wilson */ public class ProcessSizeResourceMonitor extends ResourceMonitor { /** * The display name of the stat tracker used to keep track of the process size * in kilobytes. */ public static final String STAT_TRACKER_PROCESS_SIZE = "Process Size (KB)"; /** * The name of the configuration property that specifies the process ID of the * process to monitor. */ public static final String PROPERTY_PROCESS_ID = "process_id"; /** * The default process ID that will be used if none is given. */ public static final int DEFAULT_PROCESS_ID = -1; /** * The name of the configuration property that specifies a file containing * the process ID of the process to monitor. */ public static final String PROPERTY_PROCESS_ID_FILE = "process_id_file"; /** * The default process ID file that will be used if none is given. */ public static final String DEFAULT_PROCESS_ID_FILE = null; /** * The name of the configuration property that specifies the name of the * process to monitor. */ public static final String PROPERTY_PROCESS_NAME = "process_name"; /** * The default process name that will be used if none is given. */ public static final String DEFAULT_PROCESS_NAME = null; // The array list used to hold the data before we put it in the stat tracker. private ArrayList<Integer> sizeList; // A flag that indicates whether real-time statistics reporting should be // enabled. private boolean enableRealTimeStats; // Indicates whether the requested process has existed at any time during the // execution of this monitor. private boolean processExists; // The frequency that we should use when collecting statistics. private int collectionInterval; // The process ID of the process to monitor. private int processID; // The stat tracker used to keep track of the process size. private IntegerValueTracker processSize; // The path to the process ID file to use to determine the process to monitor. private String processIDFile; // The name of the process to monitor. private String processName; /** * Performs any initialization specific to this resource monitor. * * @throws SLAMDException If a problem occurs while performing the * initialization. */ @Override() public void initializeMonitor() throws SLAMDException { sizeList = new ArrayList<Integer>(); processExists = false; processID = getProperty(PROPERTY_PROCESS_ID, DEFAULT_PROCESS_ID); processIDFile = getProperty(PROPERTY_PROCESS_ID_FILE, DEFAULT_PROCESS_ID_FILE); processName = getProperty(PROPERTY_PROCESS_NAME, DEFAULT_PROCESS_NAME); if ((processIDFile == null) || (processIDFile.length() == 0)) { processIDFile = null; } if ((processName == null) || (processName.length() == 0)) { processName = null; } int configuredCount = 0; if (processID != DEFAULT_PROCESS_ID) { configuredCount++; } if ((processIDFile != null) && (processIDFile.length() > 0)) { configuredCount++; } if ((processName != null) && (processName.length() > 0)) { configuredCount++; } if (configuredCount == 0) { throw new SLAMDException("No process ID, PID file, or process name " + "specified to monitor"); } else if (configuredCount > 1) { throw new SLAMDException("Only one of the process ID, PID file, or " + "process name may be specified"); } } /** * Indicates whether the current client system is supported for this resource * monitor. * * @return <CODE>true</CODE> if the current client system is supported for * this resource monitor, or <CODE>false</CODE> if not. */ @Override() public boolean clientSupported() { int osType = getClientOS(); switch (osType) { case OS_TYPE_SOLARIS: return true; case OS_TYPE_LINUX: // FIXME: Should we do some kind of check here to see if NPTL is // available? Probably not, since it wouldn't matter for // multithreaded applications. Just trust the user to make the // right choice. return true; default: return false; } } /** * Creates a new instance of this resource monitor thread. Note that the * <CODE>initialize()</CODE> method should have been called on the new * instance before it is returned. * * @return A new instance of this resource monitor thread. * * @throws SLAMDException If a problem occurs while creating or initializing * the resource monitor. */ @Override() public ResourceMonitor newInstance() throws SLAMDException { ProcessSizeResourceMonitor monitor = new ProcessSizeResourceMonitor(); monitor.initialize(getMonitorClient(), getMonitorProperties()); return monitor; } /** * Initializes the stat trackers maintained by this resource monitor. * * @param clientID The client ID to use for the stubs. * @param threadID The thread ID to use for the stubs. * @param collectionInterval The collection interval to use for the stubs. */ @Override() public void initializeStatistics(String clientID, String threadID, int collectionInterval) { this.collectionInterval = collectionInterval; String procName; if (processName != null) { procName = processName; } else if (processIDFile != null) { procName = "PID File " + processIDFile; } else { procName = "PID " + processID; } processSize = new IntegerValueTracker(clientID, threadID, clientID + ' ' + procName + ' ' + STAT_TRACKER_PROCESS_SIZE, collectionInterval); } /** * Retrieves the name to use for this resource monitor. * * @return The name to use for this resource monitor. */ @Override() public String getMonitorName() { return "Process Size"; } /** * Retrieves the statistical data collected by this resource monitor. * * @return The statistical data collected by this resource monitor. */ @Override() public StatTracker[] getResourceStatistics() { if (processExists) { return new StatTracker[] { processSize }; } else { if (processName != null) { logMessage("Process Size Resource Monitor: The " + processName + " process was not detected during the time the monitor " + "was active for this job."); } else { logMessage("Process Size Resource Monitor: The target process was " + "not detected during the time the monitor was active for " + "this job."); } return new StatTracker[0]; } } /** * Performs the work of actually collecting resource statistics. This method * should periodically call the <CODE>shouldStop()</CODE> method to determine * whether to stop collecting statistics. * * @return A value that indicates the status of the monitor when it * completed. */ @Override() public int runMonitor() { // Determine whether to enable real-time statistics collection. If so, then // we'll actually capture the data twice -- once for real-time reporting and // then again for what we actually report back to the server. ResourceMonitorJob monitorJob = getMonitorJob(); if ((monitorJob != null) && (monitorJob.enableRealTimeStats())) { String jobID = monitorJob.getJobID(); RealTimeStatReporter statReporter = monitorJob.getStatReporter(); enableRealTimeStats = true; processSize.startTracker(); processSize.enableRealTimeStats(statReporter, jobID); } int osType = getClientOS(); boolean haveProcessID = ((processName == null) && (processIDFile == null)); while (! shouldStop()) { long stopSleepTime = System.currentTimeMillis() + (1000 * collectionInterval); if (! haveProcessID) { try { if ((processName != null) && (processName.length() > 0)) { processID = getPIDForName(); if (processID < 0) { sizeList.add(0); } else { haveProcessID = true; } } else if ((processIDFile != null) && (processIDFile.length() > 0)) { processID = getPIDFromFile(); if (processID < 0) { sizeList.add(0); } else { haveProcessID = true; } } } catch (IOException ioe) { logMessage("Unable to determine process ID for process \"" + processName + "\" -- " + ioe); return Constants.JOB_STATE_STOPPED_DUE_TO_ERROR; } } if (haveProcessID) { try { switch (osType) { case OS_TYPE_SOLARIS: boolean pidFound = runSolaris(); if (pidFound) { processExists = true; } else if (processName != null) { // This could mean that the process is no longer running. If // so, then set a flag that we can use to try to detect when the // process is running again. logMessage("Last known process ID " + processID + " could not be found. Will start checking for a " + "new process ID"); haveProcessID = false; } break; case OS_TYPE_LINUX: pidFound = runLinux(); if (pidFound) { processExists = true; } else if (processName != null) { // This could mean that the process is no longer running. If // so, then set a flag that we can use to try to detect when the // process is running again. logMessage("Last known process ID " + processID + " could not be found. Will start checking for a " + "new process ID"); haveProcessID = false; } break; default: logMessage("Unsupported client OS (" + osType + ')'); return Constants.JOB_STATE_STOPPED_DUE_TO_ERROR; } } catch (Exception e) { writeVerbose("Caught an exception: " + e); writeVerbose(JobClass.stackTraceToString(e)); } } long sleepTime = stopSleepTime - System.currentTimeMillis(); if (sleepTime > 0) { try { Thread.sleep(sleepTime); } catch (Exception e) {} } } if (enableRealTimeStats) { processSize.stopTracker(); } int[] sizeArray = new int[sizeList.size()]; int[] countArray = new int[sizeArray.length]; for (int i=0; i < sizeArray.length; i++) { sizeArray[i] = sizeList.get(i); countArray[i] = 1; } processSize.setIntervalData(sizeArray, countArray); return Constants.JOB_STATE_COMPLETED_SUCCESSFULLY; } /** * Retrieves the process ID for the process with the specified name. * * @return The process ID for the process with the specified name, or -1 if * no such process is available. * * @throws IOException If a problem occurs while trying to make the * determination. */ private int getPIDForName() throws IOException { switch (getClientOS()) { // Solaris and Linux are exactly alike in this respect. case OS_TYPE_SOLARIS: case OS_TYPE_LINUX: Process p = Runtime.getRuntime().exec("ps -e -o comm,pid"); BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); String line; while ((line = reader.readLine()) != null) { StringTokenizer tokenizer = new StringTokenizer(line, " \t"); String command = tokenizer.nextToken(); if (command.contains(processName)) { try { reader.close(); return Integer.parseInt(tokenizer.nextToken()); } catch (Exception e) { reader.close(); logMessage("Unable to determine PID for process \"" + processName + "\" from line \"" + line + '"'); return -1; } } } reader.close(); return -1; default: logMessage("Unable to determine PID for process \"" + processName + "\" -- unsupported client OS"); return -1; } } /** * Retrieves the process ID to monitor from the specified file. * * @return The process ID to monitor from the specified file, or -1 if no * process ID could be read. * * @throws IOException If a problem occurs while trying to make the * determination. */ private int getPIDFromFile() throws IOException { BufferedReader reader = new BufferedReader(new FileReader(processIDFile)); String line = reader.readLine(); reader.close(); try { return Integer.parseInt(line.trim()); } catch (NumberFormatException nfe) { writeVerbose("Number Format Exception reading PID from file \"" + processIDFile + "\" -- " + JobClass.stackTraceToString(nfe)); return -1; } } /** * Performs all necessary processing to determine the process size for the * process on a Solaris system. * * @return <CODE>true</CODE> if the current size of the process was found, or * <CODE>false</CODE> if not for some reason (e.g., the process is no * longer running). * * @throws IOException If a problem occurs executing the appropriate command * or reading its output. */ private boolean runSolaris() throws IOException { Process p = Runtime.getRuntime().exec("ps -p " + processID + " -o vsz"); BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); String line; boolean pidFound = false; boolean sizeAdded = false; while ((line = reader.readLine()) != null) { if (line.trim().equalsIgnoreCase("VSZ")) { continue; } try { int processSize = Integer.parseInt(line.trim()); sizeList.add(processSize); if (enableRealTimeStats) { this.processSize.addValue(processSize); } pidFound = true; sizeAdded = true; break; } catch (Exception e) { writeVerbose("Unable to determine process size: " + e); sizeList.add(0); if (enableRealTimeStats) { processSize.addValue(0); } sizeAdded = true; break; } } if (! sizeAdded) { writeVerbose("Unable to determine process size"); sizeList.add(0); if (enableRealTimeStats) { processSize.addValue(0); } } reader.close(); return pidFound; } /** * Performs all necessary processing to determine the process size for the * process on a Linux system. Note that this will only be accurate for * multithreaded applications on systems with NPTL support. * * @return <CODE>true</CODE> if the current size of the process was found, or * <CODE>false</CODE> if not for some reason (e.g., the process is no * longer running). * * @throws IOException If a problem occurs executing the appropriate command * or reading its output. */ private boolean runLinux() throws IOException { Process p = Runtime.getRuntime().exec("ps -p " + processID + " -o vsz"); BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); String line; boolean pidFound = false; boolean sizeAdded = false; while ((line = reader.readLine()) != null) { if (line.trim().equalsIgnoreCase("VSZ")) { continue; } try { int processSize = Integer.parseInt(line.trim()); sizeList.add(processSize); if (enableRealTimeStats) { this.processSize.addValue(processSize); } pidFound = true; sizeAdded = true; break; } catch (Exception e) { writeVerbose("Unable to determine process size: " + e); sizeList.add(0); if (enableRealTimeStats) { processSize.addValue(0); } sizeAdded = true; break; } } if (! sizeAdded) { writeVerbose("Unable to determine process size"); sizeList.add(0); if (enableRealTimeStats) { processSize.addValue(0); } } reader.close(); return pidFound; } }