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

com.xceptance.xlt.clientperformance.ClientPerformanceMetrics 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.4.1
Show newest version
/*
 * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.xceptance.xlt.clientperformance;

import java.util.List;
import java.util.Map.Entry;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.xceptance.xlt.api.engine.Data;
import com.xceptance.xlt.api.engine.PageLoadTimingData;
import com.xceptance.xlt.api.engine.RequestData;
import com.xceptance.xlt.api.engine.WebVitalData;
import com.xceptance.xlt.engine.SessionImpl;
import com.xceptance.xlt.engine.metrics.Metrics;
import com.xceptance.xlt.engine.resultbrowser.ActionInfo;
import com.xceptance.xlt.engine.resultbrowser.RequestInfo;
import com.xceptance.xlt.engine.util.UrlUtils;

/**
 * Helper class to report any client performance metrics to the XLT metrics system. Client performance data is gathered
 * externally by our timer recorder plug-ins for Firefox and Chrome. This helper class processes the generated data
 * either just in time or at the end of a user session.
 *
 * @see Metrics
 */
public class ClientPerformanceMetrics
{
    /**
     * The log facility.
     */
    private static final Logger LOG = LoggerFactory.getLogger(ClientPerformanceMetrics.class);

    /**
     * Write a list of {@link Data} entries to a timer data file for the given session.
     *
     * @param session
     *            - for which to write the data to the timer file
     * @param dataList
     *            - the list of {@link Data} which should be written to the timer file
     */
    public static void updatePerformanceData(final SessionImpl session, final List dataList)
    {
        LOG.debug("Writing timer data file and reporting client performance metrics");

        for (final ClientPerformanceData eachPerformanceData : dataList)
        {
            updatePerformanceData(session, eachPerformanceData);
        }
    }

    private static void updatePerformanceData(final SessionImpl session, final ClientPerformanceData data)
    {
        for (final ClientPerformanceRequest eachRequest : data.getRequestList())
        {
            updateAndLogRequestData(session, eachRequest);
        }

        for (final PageLoadTimingData eachData : data.getCustomDataList())
        {
            updateAndLogPageLoadTimingData(session, eachData);
        }

        postProcessAndLogWebVitalsData(data.getWebVitalsList(), session);
    }

    private static void updateAndLogRequestData(final SessionImpl session, final ClientPerformanceRequest request)
    {
        final ActionInfo actionInfo = getActionInfo(session, request.getRequestData().getTime());
        if (actionInfo != null)
        {
            actionInfo.requests.add(getRequestInfo(request));
        }

        // write request data to timer file
        logTimerData(session, actionInfo, request.getRequestData());
    }

    private static void updateAndLogPageLoadTimingData(final SessionImpl session, final PageLoadTimingData data)
    {
        final ActionInfo actionInfo = getActionInfo(session, data.getTime());
        if (actionInfo != null)
        {
            actionInfo.events.add(new ActionInfo.PageLoadEventInfo(data.getName(), data.getTime(), data.getRunTime()));
        }

        // write page-load timing data to timer file
        logTimerData(session, actionInfo, data);
    }

    private static RequestInfo getRequestInfo(final ClientPerformanceRequest request)
    {
        final RequestInfo requestInfo = new RequestInfo();
        final RequestData requestData = request.getRequestData();
        final int statusCode = requestData.getResponseCode();

        final String statusMessage = StringUtils.defaultIfBlank(request.getStatusMessage(), "n/a");
        requestInfo.loadTime = requestData.getRunTime();

        if (statusCode > 0)
        {
            requestInfo.status = String.valueOf(statusCode) + " - ";
        }
        requestInfo.status += statusMessage;

        requestInfo.mimeType = requestData.getContentType().toString();
        requestInfo.name = getRequestName(requestData);
        requestInfo.requestMethod = request.getHttpMethod();
        requestInfo.responseCode = statusCode;
        requestInfo.startTime = requestData.getTime();
        requestInfo.url = requestData.getUrl().toString();
        requestInfo.requestHeaders.addAll(request.getRequestHeaders());
        requestInfo.responseHeaders.addAll(request.getResponseHeaders());
        requestInfo.requestParameters.addAll(request.getFormDataParameters());
        requestInfo.requestBodyRaw = request.getRawBody();
        requestInfo.formDataEncoding = request.getFormDataEncoding();

        requestInfo.setTimings(requestData);

        return requestInfo;
    }

    private static String getRequestName(final RequestData requestData)
    {
        final String urlPath = UrlUtils.parseUrlString(requestData.getUrl().toString()).getPath();
        final String pathWithoutEndSeparator = StringUtils.removeEnd(urlPath, "/");

        String name = FilenameUtils.getName(pathWithoutEndSeparator);
        if (!StringUtils.equals(urlPath, pathWithoutEndSeparator))
        {
            name = name + "/";
        }

        if (StringUtils.isBlank(name))
        {
            name = "-";
        }
        return name;
    }

    /**
     * Post-processes the passed Web vitals and logs the resulting data.
     * 

* Some Web vitals are generated only once after a page load (FCP, FID, TTFB), others may be reported multiple times * (CLS, INP, LCP). The latter happens if the metric has worsened over the lifetime of the page. In these cases, the * metric typically represents an accumulated value. For the statistics, only the last (i.e. highest) reported value * is relevant, hence we will usually log only the last observation. *

* But remember that we have actions that trigger a page load and actions that don't (a.k.a. "non-page-view" * actions). This has these two implications: *

    *
  • We receive the reportings for the last page-view action and all following non-page-view actions in a single * chunk.
  • *
  • All Web vitals are typically be reported for the page-view action, however, non-page-view actions may cause * additional reportings of CLS/INP/LCP values.
  • *
*

* If we would log only the highest reported CLS/INP/LCP value, then we might attribute that value to a later * action, even though the biggest part of the accumulated metric value was contributed by a previous action. To not * blame the wrong action, we "reset" the value when action boundaries have been crossed. This means we introduce * artificial measurements which report the delta to the value from the previous action. This approach is * experimental. * * @param webVitalList * the web vitals to process * @param session * the current session object */ private static void postProcessAndLogWebVitalsData(final List webVitalList, final SessionImpl session) { // LCP, FID, TTFB occur only once, so report them for the action they lie within logWebVitals("FCP", webVitalList, session); logWebVitals("FID", webVitalList, session); logWebVitals("TTFB", webVitalList, session); // CLS, INP, and LCP may occur multiple times with ever increasing values // * report only the latest/greatest value per action // * "reset" values at action boundaries postProcessAndLogWebVitals("CLS", webVitalList, session); postProcessAndLogWebVitals("INP", webVitalList, session); postProcessAndLogWebVitals("LCP", webVitalList, session); } /** * Logs only those web vitals with the given name, such as "FCP". */ private static void logWebVitals(final String webVitalName, final List webVitalDataList, final SessionImpl session) { for (final WebVitalData webVitalData : webVitalDataList) { if (webVitalData.getName().equals(webVitalName)) { final ActionInfo actionInfo = getActionInfo(session, webVitalData.getTime()); logTimerData(session, actionInfo, webVitalData); } } } /** * Post-processes and logs only those web vitals with the given name, such as "CLS". */ private static void postProcessAndLogWebVitals(final String webVitalName, final List webVitalDataList, final SessionImpl session) { ActionInfo lastActionInfo = null; WebVitalData lastWebVitalData = null; double lastMaxValue = 0.0; for (final WebVitalData webVitalData : webVitalDataList) { if (webVitalData.getName().equals(webVitalName)) { final ActionInfo actionInfo = getActionInfo(session, webVitalData.getTime()); if (lastWebVitalData != null && lastActionInfo != actionInfo) { // action boundary crossed -> report the last known web vital now // adjust the value and log final double value = lastWebVitalData.getValue(); lastWebVitalData.setValue(value - lastMaxValue); logTimerData(session, lastActionInfo, lastWebVitalData); // remember the value lastMaxValue = value; } lastActionInfo = actionInfo; lastWebVitalData = webVitalData; } } if (lastWebVitalData != null) { // adjust the value and log final double value = lastWebVitalData.getValue(); lastWebVitalData.setValue(value - lastMaxValue); logTimerData(session, lastActionInfo, lastWebVitalData); } } private static void logTimerData(final SessionImpl session, final ActionInfo actionInfo, final Data data) { final String actionName = actionInfo != null ? actionInfo.name : "UnknownAction"; final String dName = data.getName(); final StringBuilder sb = new StringBuilder(actionName); if (StringUtils.isNotBlank(dName)) { if (data instanceof PageLoadTimingData || data instanceof WebVitalData) { sb.append(" [").append(dName).append("]"); } else { final int idx = dName.lastIndexOf('.'); if (idx < dName.length()) { sb.append('.').append(dName.substring(idx + 1)); } } } data.setName(sb.toString()); session.getDataManager().logDataRecord(data); } /** * Returns the {@link ActionInfo} object of the action that was running at the given time. * * @param session * the session that knows when which action was run * @param time * the time in question * @return the action info object or null if no action was active at that time */ private static ActionInfo getActionInfo(final SessionImpl session, final long time) { final Entry entry = session.getWebDriverActionStartTimes().floorEntry(time); final ActionInfo actionInfo = entry != null ? entry.getValue() : null; return actionInfo; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy