All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.xceptance.xlt.engine.resultbrowser.RequestHistory Maven / Gradle / Ivy

Go to download

XLT (Xceptance LoadTest) is an extensive load and performance test tool developed and maintained by Xceptance.

There is a newer version: 8.1.0
Show newest version
package com.xceptance.xlt.engine.resultbrowser;

import java.text.ParseException;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.codec.digest.DigestUtils;

import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.WebResponse;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.xceptance.common.collection.ConcurrentLRUCache;
import com.xceptance.common.collection.LRUList;
import com.xceptance.common.lang.ThrowableUtils;
import com.xceptance.common.util.ParameterCheckUtils;
import com.xceptance.common.util.ParseUtils;
import com.xceptance.common.util.RegExUtils;
import com.xceptance.xlt.api.engine.RequestData;
import com.xceptance.xlt.api.engine.Session;
import com.xceptance.xlt.api.htmlunit.LightWeightPage;
import com.xceptance.xlt.api.util.XltLogger;
import com.xceptance.xlt.api.util.XltProperties;
import com.xceptance.xlt.common.XltConstants;
import com.xceptance.xlt.engine.SessionImpl;
import com.xceptance.xlt.engine.util.TimedCounter;

/**
 * The RequestHistory stores all the requests that have been processed so far in a session. This includes page requests
 * as well as static content requests. On request, the requests may be dumped to the file system.
 *
 * @author Jörg Werner (Xceptance Software Technologies GmbH)
 */
public class RequestHistory
{
    /**
     * The property for output to disk
     */
    public static final String OUTPUT2DISK_PROPERTY = XltConstants.XLT_PACKAGE_PATH + ".output2disk";

    /**
     * The property for HAR export.
     */
    private static final String OUTPUT2DISK_WRITEHAR_PROPERTY = OUTPUT2DISK_PROPERTY + ".writeHarFile";

    /**
     * The property for the size of the output to disk
     */
    private static final String OUTPUT2DISK_SIZE_PROPERTY = OUTPUT2DISK_PROPERTY + ".size";

    /**
     * The property level for the output to disk error properties
     */
    public static final String OUTPUT2DISK_ERROR_PROPERTY = OUTPUT2DISK_PROPERTY + ".onError";

    /**
     * The property level for the dump limiter properties
     */
    public static final String LIMITER_PROPERTY = OUTPUT2DISK_ERROR_PROPERTY + ".limiter";

    /**
     * The property for the number of maximum dumps
     */
    private static final String MAX_DUMP_COUNT_PROPERTY = LIMITER_PROPERTY + ".maxDumps";

    /**
     * The property for the counter reset interval.
     */
    private static final String COUNTER_RESET_INTERVAL_PROPERTY = LIMITER_PROPERTY + ".resetInterval";

    /**
     * The property for the number of maximally handled different errors.
     */
    private static final String MAX_DIFFERENT_ERRORS_PROPERTY = LIMITER_PROPERTY + ".maxDifferentErrors";

    /**
     * Default size for LRU dump cache.
     */
    private static final int MAX_DIFFERENT_ERRORS_DEFAULT = 1000;

    /**
     * The counter reset interval.
     */
    private static final long COUNTER_RESET_INTERVAL = getConfiguredCounterResetInterval();

    /**
     * Testcase specific error keys and corresponding number of already dumped results.
     */
    private static final ConcurrentLRUCache DUMP_COUNT = getDumpCounter();

    /**
     * Get the configured counter reset interval in milliseconds
     *
     * @return the configured counter reset interval in milliseconds
     */
    private static long getConfiguredCounterResetInterval()
    {
        // get the value from configuration
        final String intervalString = XltProperties.getInstance().getProperty(COUNTER_RESET_INTERVAL_PROPERTY, "0");

        // parse seconds for the counter reset interval
        long tmpInterval = 0;
        try
        {
            tmpInterval = ParseUtils.parseTimePeriod(intervalString);
        }
        catch (final ParseException e)
        {
            throw new RuntimeException(String.format("The value '%s' of property '%s' cannot be resolved to a %s.", intervalString,
                                                     COUNTER_RESET_INTERVAL_PROPERTY, "time period"));
        }
        catch (final IllegalArgumentException e)
        {
            throw new RuntimeException(String.format("The value '%s' of property '%s' cannot be resolved to a %s.", intervalString,
                                                     COUNTER_RESET_INTERVAL_PROPERTY, "time period"));
        }

        // return the interval in milliseconds
        return tmpInterval * 1000;
    }

    private static ConcurrentLRUCache getDumpCounter()
    {
        int maxDiffErrors = XltProperties.getInstance().getProperty(MAX_DIFFERENT_ERRORS_PROPERTY, MAX_DIFFERENT_ERRORS_DEFAULT);

        // check minimum required size for LRU cache
        if (maxDiffErrors < 10)
        {
            maxDiffErrors = 10;
        }

        return new ConcurrentLRUCache(maxDiffErrors * 3);
    }

    /**
     * The possible dump mode values.
     */
    public enum DumpMode
    {
     NEVER,
     ON_ERROR,
     ALWAYS;

        /**
         * Returns the dumpMode according to the argument string. Will return {@link DumpMode#ALWAYS} if none of the
         * expected strings is matched. Note that this implementation differs from the default implementation of
         * {@link Enum#valueOf(Class, String)}!
         *
         * @return the DumpMode according to the argument string, won't return null
         */
        public static DumpMode valueFrom(final String propertyValue)
        {
            if ("never".equals(propertyValue))
            {
                return DumpMode.NEVER;
            }
            else if ("onError".equals(propertyValue) || "onErrors".equals(propertyValue))
            {
                return DumpMode.ON_ERROR;
            }
            else
            {
                return DumpMode.ALWAYS;
            }
        }
    }

    /**
     * The list of HTML pages, which have not been dumped so far.
     */
    private final LRUList pages;

    /**
     * The list of requests, for which a corresponding HTML page not yet exists. Once such a page is added to the
     * request history, any pending request is attached to this page.
     */
    private final List pendingRequests;

    /**
     * The configured dump mode.
     */
    private DumpMode dumpMode;

    /**
     * The dumping manager.
     */
    private DumpMgr dumpMgr;

    /**
     * Maximal number of dumps.
     */
    private final int maxDumpCount;

    /**
     * Creates a new RequestHistory object.
     */
    public RequestHistory()
    {
        int historySize = XltProperties.getInstance().getProperty(OUTPUT2DISK_SIZE_PROPERTY, 3);
        if (historySize < 1)
        {
            XltLogger.runTimeLogger.warn(OUTPUT2DISK_SIZE_PROPERTY + " must be larger than 1, setting 3 as default now.");
            historySize = 3;
        }

        pages = new LRUList(historySize);
        pendingRequests = new LinkedList();

        // get dump mode
        final String dumpModeValue = XltProperties.getInstance().getProperty(OUTPUT2DISK_PROPERTY, "onError");

        dumpMode = DumpMode.valueFrom(dumpModeValue);

        dumpMgr = new DumpMgr();
        dumpMgr.setHarExportEnabled(XltProperties.getInstance().getProperty(OUTPUT2DISK_WRITEHAR_PROPERTY, false));

        maxDumpCount = XltProperties.getInstance().getProperty(MAX_DUMP_COUNT_PROPERTY, -1);
    }

    /**
     * Adds the given request/response information to the internal in-memory request list if the current dump mode is
     * {@link DumpMode#ON_ERROR}. Otherwise the values are either ignored ({@link DumpMode#NEVER}) or dumped immediately
     * ({@link DumpMode#ALWAYS}).
     *
     * @param name
     *            the request name
     * @param webRequest
     *            the request settings
     * @param webResponse
     *            the response
     * @param requestData
     *            the request data
     */
    public synchronized void add(final String name, final WebRequest webRequest, final WebResponse webResponse,
                                 final RequestData requestData)
    {
        ParameterCheckUtils.isNotNull(name, "name");
        ParameterCheckUtils.isNotNull(webRequest, "webRequestSettings");

        if (dumpMode == DumpMode.NEVER)
        {
            // do nothing
        }
        else if (dumpMode == DumpMode.ON_ERROR)
        {
            // add a new pending request
            pendingRequests.add(new Request(name, webRequest, webResponse, requestData));
        }
        else if (dumpMode == DumpMode.ALWAYS)
        {
            // immediately dump it, no need to keep it in memory
            dumpMgr.dump(new Request(name, webRequest, webResponse, requestData));
        }
    }

    /**
     * Adds the given lightweight page to the internal in-memory page list if the current dump mode is
     * {@link DumpMode#ON_ERROR}. Otherwise the page is either ignored ({@link DumpMode#NEVER}) or dumped immediately (
     * {@link DumpMode#ALWAYS}).
     *
     * @param lwPage
     *            the lightweight page
     */
    public void add(final LightWeightPage lwPage)
    {
        final Page page = new Page(lwPage.getTimerName(), lwPage);
        add(page);
    }

    /**
     * Adds the given HTML page to the internal in-memory page list if the current dump mode is
     * {@link DumpMode#ON_ERROR}. Otherwise the page is either ignored ({@link DumpMode#NEVER}) or dumped immediately (
     * {@link DumpMode#ALWAYS}).
     *
     * @param name
     *            the page's name
     * @param htmlPage
     *            the page
     */
    public void add(final String name, final HtmlPage htmlPage)
    {
        ParameterCheckUtils.isNotNull(name, "name");

        final Page page = new Page(name, htmlPage);
        add(page);
    }

    /**
     * Adds the given screenshot page to the internal in-memory page list if the current dump mode is
     * {@link DumpMode#ON_ERROR}. Otherwise the page is either ignored ({@link DumpMode#NEVER}) or dumped immediately (
     * {@link DumpMode#ALWAYS}).
     *
     * @param name
     *            the page's name
     * @param image
     *            the screenshot page
     */
    public void add(final ActionInfo actionInfo, final byte[] image)
    {
        ParameterCheckUtils.isNotNull(actionInfo.name, "name");

        final Page page = new Page(actionInfo, image);
        add(page);
    }

    /**
     * Adds an empty page to the internal in-memory page list if the current dump mode is {@link DumpMode#ON_ERROR}.
     * Otherwise the page is either ignored ({@link DumpMode#NEVER}) or dumped immediately ( {@link DumpMode#ALWAYS}).
     *
     * @param name
     *            the page's name
     */
    public void add(final String name)
    {
        ParameterCheckUtils.isNotNull(name, "name");

        final Page page = new Page(name);
        add(page);
    }

    /**
     * Adds the given page to the internal in-memory page list if the current dump mode is {@link DumpMode#ON_ERROR}.
     * Otherwise the page is either ignored ({@link DumpMode#NEVER}) or dumped immediately ({@link DumpMode#ALWAYS}).
     *
     * @param name
     *            the page's name
     * @param image
     *            the screenshot page
     */
    private synchronized void add(final Page page)
    {
        if (dumpMode == DumpMode.NEVER)
        {
            // do nothing
        }
        else if (dumpMode == DumpMode.ON_ERROR)
        {
            // add a new page and attach all pending requests to it
            pages.add(page);

            page.getRequests().addAll(pendingRequests);
            pendingRequests.clear();
        }
        else if (dumpMode == DumpMode.ALWAYS)
        {
            // immediately dump it, no need to keep it in memory
            dumpMgr.dump(page);
        }
    }

    /**
     * Dumps the collected requests and pages to the file system.
     */
    public void dumpToDisk()
    {
        if (requestDumpPermission())
        {
            dump();
        }
    }

    /**
     * Dumping might be restricted to a certain amount per error.
     *
     * @return true if permission to dump is granted, false otherwise
     */
    private boolean requestDumpPermission()
    {
        // permission denied by default
        boolean permissionGranted = false;

        switch (dumpMode)
        {
            case ALWAYS:
            {
                permissionGranted = true;
                break;
            }
            case ON_ERROR:
            {
                if (Session.getCurrent().hasFailed())
                {
                    // no limit
                    if (maxDumpCount < 0)
                    {
                        permissionGranted = true;
                    }
                    // do not dump
                    else if (maxDumpCount == 0)
                    {
                        // contradiction: log in case of error but max 0 dumps?
                        XltLogger.runTimeLogger.warn("Dump mode is " + dumpMode + " but maximum dump count is 0.");
                    }
                    // limited dump
                    else
                    {
                        // get the error key
                        final String key = getErrorKey();

                        // get the key's dump counter
                        final TimedCounter count = getErrorCount(key);

                        // if dumping for this hash is OK increase dump counter and grant permission
                        if (count.get() < maxDumpCount)
                        {
                            count.increment();
                            permissionGranted = true;
                        }
                    }
                }
                break;
            }
            default:
            {
                // do not grant permission
            }
        }

        return permissionGranted;
    }

    /**
     * Create the key for the session's failure reason.
     *
     * @return the session failure's key
     */
    private static String getErrorKey()
    {
        final SessionImpl session = (SessionImpl) Session.getCurrent();

        // get the error stack trace
        final Throwable t = session.getFailReason();

        String key = t != null ? ThrowableUtils.getMinifiedStackTrace(t) : "";

        // remove the hint
        key = RegExUtils.removeAll(key, ThrowableUtils.DIRECTORY_HINT_REGEX);

        // take the testcase name into account
        key = session.getUserName() + "|" + key;

        // hash the current key to reduce memory usage
        key = DigestUtils.md5Hex(key);

        return key;
    }

    /**
     * Get the dump counter for the given key or create a new one (initialized with 0) if the key is
     * unknown.
     *
     * @param key
     *            the error key
     * @return the key's counter
     */
    private static TimedCounter getErrorCount(final String key)
    {
        TimedCounter count = DUMP_COUNT.get(key);
        if (count == null)
        {
            synchronized (DUMP_COUNT)
            {
                count = DUMP_COUNT.get(key);
                if (count == null)
                {
                    count = new TimedCounter(COUNTER_RESET_INTERVAL);
                    DUMP_COUNT.put(key, count);
                }
            }
        }
        return count;
    }

    /**
     * Dump the requests to the file system.
     */
    private void dump()
    {
        final List pagesCopy;
        final List requestsCopy;
        synchronized (this)
        {
            pagesCopy = new LinkedList(pages);
            requestsCopy = new LinkedList(pendingRequests);

            pages.clear();
            pendingRequests.clear();
        }

        dumpMgr.dumpToDisk(pagesCopy, requestsCopy);
    }

    /**
     * Clears the request list.
     */
    public synchronized void clear()
    {
        pages.clear();
        pendingRequests.clear();

        dumpMgr.clear();
    }

    /**
     * Returns the current dump mode.
     *
     * @return the dumpMode
     */
    public DumpMode getDumpMode()
    {
        return dumpMode;
    }

    /**
     * Sets the new dump mode.
     *
     * @param dumpMode
     *            the dumpMode to set
     */
    public void setDumpMode(final DumpMode dumpMode)
    {
        this.dumpMode = dumpMode;
    }

    /**
     * Sets the new dump manager.
     *
     * @param dumpManager
     *            new dump manager
     */
    public void setDumpManager(final DumpMgr dumpManager)
    {
        if (dumpManager != null)
        {
            dumpMgr = dumpManager;
        }
    }

    /**
     * Returns the dump manager.
     *
     * @return dump manager
     */
    public DumpMgr getDumpManager()
    {
        return dumpMgr;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy