/* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at
* http://www.sun.com/cddl/cddl.html or
* install_dir/legal/LICENSE
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at install_dir/legal/LICENSE.
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* $Id$
*
* Copyright 2005-2010 Sun Microsystems Inc. All Rights Reserved
*/
package com.sun.faban.driver.engine;
import com.sun.faban.common.FabanNamespaceContext;
import com.sun.faban.common.Utilities;
import com.sun.faban.driver.CustomMetrics;
import com.sun.faban.driver.CustomTableMetrics;
import com.sun.faban.driver.Timing;
import static com.sun.faban.driver.engine.AgentThread.TIME_NOT_SET;
import com.sun.faban.driver.util.Random;
import com.sun.faban.driver.util.Timer;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.File;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.logging.Logger;
/**
* DriverContext is the point of communication between the
* developer-provided driver and the Faban driver framework.
* Each thread has it's own context.<p>
* This class provides the actual implementation and is
* located in the engine package to get direct access to all
* the restricted/needed classes and members.
*
* @author Akara Sucharitakul
*/
public class DriverContext extends com.sun.faban.driver.DriverContext {
/** Thread local used for obtaining the context. */
private static ThreadLocal<DriverContext> localContext = new InheritableThreadLocal<DriverContext>();
/** The thread associated with this context. */
AgentThread agentThread;
/** The timing structure for this thread/context. */
TimingInfo timingInfo = new TimingInfo();
/** The central timer. */
Timer timer;
/**
* Flag whether pause is supported with the current protocol.
* Default is true. Protocols that may not be request/response but
* may have concurrent inbound and outbound traffic, AND wishes to
* support auto timing should set this flag to false. The default is true.
*/
boolean pauseSupported = true;
/** Context-specific logger. */
Logger logger;
/** The properties hashmap. It is lazy initialized. */
static HashMap<String, String[]> properties;
/** Class name of this class. */
private String className;
/** The XPath instance used to evaluate the XPaths. */
private XPath xPathInstance;
/** Desired upload speed of this context */
private int kbpsUpload = -1;
/** Desired download speed of this context */
private int kbpsDownload = -1;
/**
* Obtains the DriverContext associated with this thread.
* @return the associated DriverContext
*/
public static DriverContext getContext() {
return localContext.get();
}
/**
* Constructs a DriverContext. Called only from AgentThread.
* @param thread The AgentThread used by this context
* @param timer The timer used by this thread
*/
DriverContext(AgentThread thread, Timer timer) {
className = getClass().getName();
agentThread = thread;
this.timer = timer;
localContext.set(this);
}
/**
* Obtains the scale or scaling rate of the current run.
*
* @return the current run's scaling rate
*/
public int getScale() {
return agentThread.runInfo.scale;
}
/**
* Obtains the number of client threads in this agent.
*
* @return the number of client threads
*/
public int getClientsInAgent() {
return agentThread.runInfo.agentInfo.threads;
}
/**
* Obtains the total number of clients threads for this driver.
*
* @return the number of client threads for this driver
*/
public int getClientsInDriver() {
return agentThread.runInfo.driverConfig.numThreads;
}
/**
* Obtains the global thread id for this context's thread. The thread id
* is unique for each driver type.
* @return the global agentImpl thread id
*/
public int getThreadId() {
return agentThread.id;
}
/**
* Obtains the agent id for this agentImpl.
* @return the current agentImpl's id
*/
public int getAgentId() {
return agentThread.runInfo.agentInfo.agentNumber;
}
/**
* Obtains the driver's name as annotated in the driver class.
* @return the driver name
*/
public String getDriverName() {
return agentThread.driverConfig.name;
}
/**
* Obtains the logger to be used by the calling driver.
* @return the appropriate logger
*/
public Logger getLogger() {
if (logger == null) {
logger = Logger.getLogger(agentThread.driverConfig.
className + '.' + agentThread.id);
if (agentThread.runInfo.logHandler != null) {
logger.addHandler(agentThread.runInfo.logHandler);
}
}
return logger;
}
/**
* Obtains the name of the operation currently executing.
* @return the current operation's name,
* or null if called from outside an operation.
*/
public String getCurrentOperation() {
if (agentThread.currentOperation == -1)
return null;
return agentThread.driverConfig.operations[
agentThread.currentOperation].name;
}
/**
* Obtains the unique id assigned to the current operation type.
* This id is commonly used to index into array structures containing
* operation-specific information such as stats. The id ranges from 0 to
* n where n is the number of operations in the driver less one.
*
* @return The unique id assigned to this operation type,
* or -1 if called from outside an operation.
*/
public int getOperationId() {
return agentThread.currentOperation;
}
/**
* Obtains the number of operations active in this driver.
*
* @return The number of active operations
*/
public int getOperationCount() {
return agentThread.driverConfig.operations.length;
}
/**
* Obtains the per-thread random value generator. Drivers
* should use this random value generator and not instantiate
* their own.
* @return The random value generator
*/
public Random getRandom() {
return agentThread.random;
}
/**
* Resets the state of the current mix to start off at the beginning
* of the mix. For stateless mixes such as FlatMix, this operation
* does nothing.
*/
public void resetMix() {
agentThread.selector[agentThread.mixId].reset();
}
/**
* Attaches a custom metrics object to the primary metrics.
* This should be done by the driver at initialization time.
* Only one custom metrics can be attached. Subsequent calls
* to this method replaces the previously attached metrics.
* @param metrics The custom metrics to be replaced
*/
public void attachMetrics(CustomMetrics metrics) {
attachMetrics("Miscellaneous Statistics", metrics);
}
/**
* Attaches a custom metrics object to the primary metrics,
* given a name or description. The name/description must be unique.
* This should be done by the driver at initialization time.
* Only one custom metrics can be attached. Subsequent calls
* to this method replaces the previously attached metrics.
* @param name The name or description of this metrics
* @param metrics The custom metrics to be replaced
*/
public void attachMetrics(String name, CustomMetrics metrics) {
if (agentThread.metrics.metricAttachments == null)
agentThread.metrics.metricAttachments =
new LinkedHashMap<String, CustomMetrics>();
agentThread.metrics.metricAttachments.put(name, metrics);
}
/**
* Attaches a custom table metrics object to the primary metrics,
* given a name or description. The name/description must be unique.
* This should be done by the driver at initialization time.
* Only one custom metrics can be attached. Subsequent calls
* to this method replaces the previously attached metrics.
* @param name The name or description of this metrics
* @param metrics The custom metrics to be replaced
*/
public void attachMetrics(String name, CustomTableMetrics metrics) {
if (agentThread.metrics.tableAttachments == null)
agentThread.metrics.tableAttachments =
new LinkedHashMap<String, CustomTableMetrics>();
agentThread.metrics.tableAttachments.put(name, metrics);
}
/**
* Parses the properties DOM tree in puts the output into a HashMap.
* Returns properties so that we do not have the effect of double-checks.
* @param propertiesElement The DOM element containing the properties
* @return The resulting map
*/
private static synchronized HashMap<String, String[]> parseProperties(
Element propertiesElement) {
if (properties == null) {
NodeList list = propertiesElement.getElementsByTagNameNS(
RunInfo.DRIVERURI, "property");
int length = list.getLength();
HashMap<String, String[]> props =
new HashMap<String, String[]>(length);
for (int i = 0; i < length; i++) {
Element propertyElement = (Element) list.item(i);
Attr attr = propertyElement.getAttributeNodeNS(null, "name");
if (attr != null) {
props.put(attr.getValue(), getValue(propertyElement));
}
NodeList nameList =
propertyElement.getElementsByTagNameNS(
RunInfo.DRIVERURI, "name");
if (nameList.getLength() != 1) {
continue;
}
Element nameElement = (Element) nameList.item(0);
String name = nameElement.getFirstChild().getNodeValue();
if (name != null) {
props.put(name, getValue(propertyElement));
}
}
properties = props;
}
return properties;
}
/**
* Gets the value of a property DOM element.
* @param propertyElement The DOM element
* @return The list of associated values
*/
private static String[] getValue(Element propertyElement) {
NodeList valueList = propertyElement.getElementsByTagNameNS(
RunInfo.DRIVERURI, "value");
String[] values;
int length = valueList.getLength();
if (length >= 1) {
values = new String[length];
for (int i = 0; i < length; i++) {
Node valueNode = valueList.item(i).getFirstChild();
values[i] = valueNode == null ? "" : valueNode.getNodeValue();
}
} else {
values = new String[1];
Node valueNode = propertyElement.getFirstChild();
values[0] = valueNode == null ? "" : valueNode.getNodeValue();
}
return values;
}
/**
* Obtains a single-value property from the configuration. If the name
* of a multi-value property is given, only one value is returned.
* It is undefined as to which value in the list is returned.
*
* @param name The property name
* @return The property value, or null if there is no such property
*/
public String getProperty(String name) {
if (properties == null) {
properties = parseProperties(getPropertiesNode());
}
String[] value = properties.get(name);
if (value == null) {
return null;
}
return value[0];
}
/**
* Obtains a multiple-value property from the configuration. A
* single-value property will be returned as an array of dimension 1.
*
* @param name The property name
* @return The property values
*/
public String[] getPropertyValues(String name) {
if (properties == null) {
properties = parseProperties(getPropertiesNode());
}
return properties.get(name);
}
/**
* Obtains the reference to the whole properties element as configured
* in the driverConfig element of this driver in the config file. This
* method allows custom free-form structures but the driver will need
* to spend the effort walking the DOM tree.
*
* @return The DOM tree representing the properties node
*/
public Element getPropertiesNode() {
return agentThread.driverConfig.properties;
}
/**
* Checks whether the driver is currently in steady state or not.
* This method needs to be called after the critical section of the
* operation. The transaction times must have been recorded in order
* to establish whether or not the transaction is in steady state.
* @return True if in steady state, false if not.
*/
public boolean isTxSteadyState() {
return agentThread.isSteadyState();
}
/**
* Reads the element or attribute by it's XPath. The XPath is evaluated
* from the root of the configuration file.
*
* @param xPath The XPath to evaluate.
* @return The element or attribute value defined by the XPath
* @exception XPathExpressionException If the given XPath has an error
*/
public String getXPathValue(String xPath) throws XPathExpressionException {
if (xPathInstance == null) {
XPathFactory xf = XPathFactory.newInstance();
FabanNamespaceContext nsCtx = new FabanNamespaceContext();
xPathInstance = xf.newXPath();
xPathInstance.setNamespaceContext(nsCtx);
}
return xPathInstance.evaluate(xPath,
agentThread.driverConfig.rootElement);
}
/**
* Records the start and end time of the critical section of an operation.
* This operation may block until the appropriate start time for the
* operation has arrived. There is no blocking for the end time.
* This method is for use in the driver code to demarcate critical
* sections.
* @throws IllegalStateException if the operation uses auto timing
*/
public void recordTime() {
if (agentThread.currentOperation == -1)
throw new IllegalStateException("DriverContext.recordTime called " +
"outside an operation");
if (agentThread.driverConfig.operations[agentThread.currentOperation].
timing != Timing.MANUAL) {
String msg = "Driver: " + getDriverName() + ", Operation: " +
getCurrentOperation() + ", timing: MANUAL illegal call " +
"to recordTime() in driver code.";
logger.severe(msg);
IllegalStateException e = new IllegalStateException(msg);
logger.throwing(className, "recordTime", e);
throw e;
}
if (timingInfo != null) {
if (timingInfo.invokeTime == TIME_NOT_SET) {
timer.wakeupAt(timingInfo.intendedInvokeTime);
// But since sleep may not be exact, we get the time again here.
timingInfo.invokeTime = System.nanoTime();
} else if (timingInfo.lastRespondTime != TIME_NOT_SET) {
// The critical section was paused.
timingInfo.pauseTime +=
System.nanoTime() - timingInfo.lastRespondTime;
timingInfo.lastRespondTime = TIME_NOT_SET;
} else {
timingInfo.respondTime = System.nanoTime();
}
}
}
/**
* Pauses the critical section so that operations made during the pause
* do not count into the response time. If Timing.AUTO is used, the pause
* ends automatically when the next request is sent to the server. For
* manual timing, the next call to recordTime ends the pause. Calls
* pauseTime when the critical section is already paused are simply ignored.
*/
public void pauseTime() {
if (agentThread.currentOperation == -1)
throw new IllegalStateException("DriverContext.pauseTime called " +
"outside an operation");
if (agentThread.driverConfig.operations[agentThread.currentOperation].
timing != Timing.MANUAL) {
String msg = "Driver: " + getDriverName() + ", Operation: " +
getCurrentOperation() + ", timing: MANUAL illegal call " +
"to pauseTime() in driver code.";
logger.severe(msg);
IllegalStateException e = new IllegalStateException(msg);
logger.throwing(className, "recordTime", e);
throw e;
}
if (timingInfo.lastRespondTime == TIME_NOT_SET) {
timingInfo.lastRespondTime = System.nanoTime();
}
}
/**
* Obtains a relative time, in milliseconds. This time is relative to
* a certain time at the beginning of the benchmark run and does not
* represent a wall clock time. All agents will have the same reference
* time. Use this time to check time durations during the benchmark run.
*
* @return The relative time of the benchmark run
*/
public int getTime() {
return timer.getTime();
}
/**
* Wakes up closest to a system nanosec time.
* @param time The time to wake up
*/
public void wakeupAt(long time) {
timer.wakeupAt(time);
}
/**
* Obtains the relative time - in milliseconds - that steady state starts,
* if set. The if the time is not yet set, it will return 0.
*
* @return The relative time steady state starts
*/
public int getSteadyStateStart() {
return (int) (timer.toRelTime(agentThread.endRampUp) / Utilities.TO_NANOS);
}
/**
* Obtains a relative time, in nanosecs. This time is relative to
* a certain time at the beginning of the benchmark run and does not
* represent a wall clock time. All agents will have the same reference
* time. Use this time to check time durations during the benchmark run.
*
* @return The relative time of the benchmark run
*/
public long getNanoTime() {
return timer.toRelTime(System.nanoTime());
}
/**
* Obtains the relative time - in nanosecs - that steady state starts,
* if set. The if the time is not yet set, it will return 0.
*
* @return The relative time steady state starts
*/
public long getSteadyStateStartNanos() {
return timer.toRelTime(agentThread.endRampUp);
}
/**
* Obtains the configured ramp up time.
*
* @return The configured ramp up time, in seconds
*/
public int getRampUp() {
return agentThread.runInfo.rampUp;
}
/**
* Obtains the configured steady state time.
*
* @return The configured steady state time, in seconds
*/
public int getSteadyState() {
return agentThread.runInfo.stdyState;
}
/**
* Obtains the configured ramp down time.
*
* @return The configured ramp down time, in seconds
*/
public int getRampDown() {
return agentThread.runInfo.rampDown;
}
/**
* Property whether pause is supported with the current protocol.
* Default is true. Protocols that may not be request/response but
* may have concurrent inbound and outbound traffic, AND wishes to
* support auto timing should set this flag to false. The default is true.
*
* @return The current setting of the pauseSupported property.
*/
public boolean isPauseSupported() {
return pauseSupported;
}
/**
* Property whether pause is supported with the current protocol.
* Default is true. Protocols that may not be request/response but
* may have concurrent inbound and outbound traffic, AND wishes to
* support auto timing should set this flag to false. The default is true.
*
* @param pause The new setting of the pauseSupported property.
*/
public void setPauseSupported(boolean pause) {
pauseSupported = pause;
}
/**
* Records the start time of an operation. This method is not
* exposed through the interface and is only used by the transport
* facilities.
* @return The recorded time - system nanotime, or TIME_NOT_SET if not set
*/
public long recordStartTime() {
// Not in an operation, don't record time.
if (agentThread.currentOperation == -1)
return TIME_NOT_SET;
if (timingInfo != null && agentThread.driverConfig.operations[
agentThread.currentOperation].timing == Timing.AUTO) {
if (timingInfo.invokeTime == TIME_NOT_SET) {
if (timingInfo.respondTime != TIME_NOT_SET)
logger.warning("Respond time already set before " +
"sleeping. Please report a bug.");
timer.wakeupAt(timingInfo.intendedInvokeTime);
// But since sleep may not be exact, we get the time again here.
timingInfo.invokeTime = System.nanoTime();
return timingInfo.invokeTime;
} else if (pauseSupported && timingInfo.respondTime != TIME_NOT_SET) {
if (timingInfo.respondTime < timingInfo.invokeTime)
logger.warning("Respond time (" + timingInfo.respondTime +
") less than invoke time (" +
timingInfo.invokeTime + "). Please report a bug.");
// Some response already read, then transmit again.
// In this case the time from last receive to this transmit
// is the pause time ...
timingInfo.lastRespondTime = timingInfo.respondTime;
// We set the pause time only on the first byte transmitted.
timingInfo.respondTime = TIME_NOT_SET;
long time = System.nanoTime();
timingInfo.pauseTime += time - timingInfo.lastRespondTime;
return time;
}
// Otherwise this can be a subsequent write.
// Invoke time already set and respond time not set.
}
return TIME_NOT_SET;
}
/**
* Records the end time of an operation. This method is not
* exposed through the interface and is only used by the transport
* facilities.
* @return The recorded time - system nanotime, or TIME_NOT_SET if not set
*/
public long recordEndTime() {
long tstamp = TIME_NOT_SET;
// Not in an operation, don't record time.
if (agentThread.currentOperation != -1) {
if (timingInfo != null && agentThread.driverConfig.operations[
agentThread.currentOperation].timing == Timing.AUTO ) {
// Some stacks clear the connection by doing a read before a
// write in a request, normally a read of 0 bytes. We need to
// make sure such reads are not part of the response time.
if (timingInfo.invokeTime == TIME_NOT_SET) {
int[] previousOps = agentThread.previousOperation;
String name = agentThread.driverConfig.mix[0].
operations[previousOps[0]].name;
if (previousOps.length > 1)
name += ',' + agentThread.driverConfig.mix[1].
operations[previousOps[1]].name;
logger.warning("Read before write! Some input may still " +
"be in the buffer from previous operation " +
name + ". Ignoring such input.");
} else {
timingInfo.respondTime = tstamp = System.nanoTime();
}
}
}
return tstamp;
}
/**
* Sets the intended invocation time for the next invocation
* on this thread. This is called from AgentThread only.
* @param time The time to invoke
*/
void setInvokeTime(long time) {
// Then set the intended start time.
timingInfo.intendedInvokeTime = time;
// And set the other times to invalid.
timingInfo.invokeTime = TIME_NOT_SET;
timingInfo.respondTime = TIME_NOT_SET;
timingInfo.lastRespondTime = TIME_NOT_SET;
timingInfo.pauseTime = 0l;
}
/**
* TimingInfo is a value object that contains individual
* timing records for each operation.
*/
public static class TimingInfo {
/** Intended Invoke Time. */
public long intendedInvokeTime = TIME_NOT_SET;
/** Actual Invoke Time. */
public long invokeTime = TIME_NOT_SET;
/** Respond Time. */
public long respondTime = TIME_NOT_SET;
/** Last respond time, if any. */
public long lastRespondTime = TIME_NOT_SET;
/** Pause Time. */
public long pauseTime = 0l;
}
/**
* Obtains the base directory where the benchmark currently being run
* is installed.
*
* @return The benchmark's base directory
*/
public String getBaseDir() {
return agentThread.agent.driverBase;
}
/**
* Obtains the resource directory used for this benchmark, if exists.
* @return The resource directory for this benchmark
*/
public String getResourceDir() {
return agentThread.agent.driverBase + File.separator + "resources";
}
/**
* Set the desired upload speed for the thread using this context.
* This method is intended for use only by transport classes; drivers
* should call an apporpriate method on the transport to set this
* value. Note that not all transports support bandwidth throttling
* (so drivers that do call this method will have no idea if the value
* is used or not).
*
* @param kbps desired speed in kilobytes per second. If kbps is < 0,
* speed will be unlimited.
*/
public void setUploadSpeed(int kbps) {
this.kbpsUpload = kbps;
}
/**
* Return the desired upload speed for the thread using this context.
*
* @return desired speed in kilobytes per second
*/
public int getUploadSpeed() {
return kbpsUpload;
}
/**
* Set the desired downoad speed for the thread using this context.
* This method is intended for use only by transport classes; drivers
* should call an apporpriate method on the transport to set this
* value. Note that not all transports support bandwidth throttling
* (so drivers that do call this method will have no idea if the value
* is used or not).
*
* @param kbps desired speed in kilobytes per second. If kbps is < 0,
* speed will be unlimited.
*/
public void setDownloadSpeed(int kbps) {
this.kbpsDownload = kbps;
}
/**
* Return the desired download speed for the thread using this context.
*
* @return desired speed in kilobytes per second
*/
public int getDownloadSpeed() {
return kbpsDownload;
}
}