com.xceptance.xlt.api.webdriver.XltChromeDriver Maven / Gradle / Ivy
Show all versions of xlt Show documentation
/*
* Copyright (c) 2005-2022 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.api.webdriver;
import java.io.File;
import java.net.URL;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableMap;
import com.xceptance.common.lang.ReflectionUtils;
import com.xceptance.xlt.api.util.XltProperties;
import com.xceptance.xlt.clientperformance.ClientPerformanceExtensionConnector.CommunicationException;
import com.xceptance.xlt.clientperformance.ClientPerformanceUtils;
import com.xceptance.xlt.clientperformance.WebExtConnectionHandler;
import com.xceptance.xlt.engine.SessionImpl;
import com.xceptance.xlt.engine.WebDriverActionDirector;
/**
* An extended {@link ChromeDriver} which allows to record data about requests and browser events or to run Chrome with
* a virtual display.
*
* Collected Data
*
* To collect data about requests and browser events, a special extension will be installed into the browser. This
* extension provides access to the following information:
*
* - for requests:
*
* - start and total processing time
* - URL, status code, response content type
* - sent and received bytes
* - network timings (DNS time, connect time, send time, server busy time, receive time, time to first byte, time to
* last byte)
*
*
* - for browser events:
*
* - the time after which the event occurred when loading a new page
*
*
*
* The following browser events will be reported:
*
* - DomLoading
* - DomInteractive
* - DomComplete
* - DomContentLoadedEventStart
* - DomContentLoadedEventEnd
* - LoadEventStart
* - LoadEventEnd
* - FirstPaint
* - FirstContentfulPaint
*
* All this data will be available in the XLT load test report. Request data is shown in the Requests section and
* browser events can be found in the Page Load Timings section.
*
* Headless Mode
*
* On Unix machines, it is possible to run the browser in "headless" mode, i.e. with a virtual display. To put the
* browser in headless mode, simply set the following property in the configuration of your test project:
*
*
* xlt.webDriver.chrome_clientperformance.screenless = true
*
*
* Note that for the headless mode to work, the xvfb
binary must be installed on the target machine and
* must be findable via the PATH variable. If this is not the case, the browser will be run with the default display.
*/
public class XltChromeDriver extends ChromeDriver
{
private static final String PROPERTY_DOMAIN = "xlt.webDriver.chrome_clientperformance.";
/**
* The name of the field in {@link ChromeDriverService} that holds the environment variable map.
*/
private static final String FIELD_NAME_ENVIRONMENT = "environment";
/**
* The class logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(XltChromeDriver.class);
/**
* The XLT property to enable headless mode if it is available at all.
*/
private static final String PROPERTY_HEADLESS = PROPERTY_DOMAIN + "screenless";
/**
* Whether headless mode is enabled.
*/
private static final boolean HEADLESS_ENABLED;
/**
* The XLT property to enable recording of incomplete/aborted requests.
*/
private static final String PROPERTY_RECORD_INCOMPLETE = PROPERTY_DOMAIN + "recordIncomplete";
/**
* Whether recording of incomplete/aborted requests is enabled.
*/
private static final boolean RECORD_INCOMPLETE_ENABLED;
/**
* The base name of the extension file.
*/
private static final String EXTENSION_FILE_NAME = "xlt-timerrecorder-chrome";
/**
* The file name extension of the extension file.
*/
private static final String EXTENSION_FILE_ENDING = ".crx";
/**
* The file to which the extension is extracted.
*/
private static File extensionFile;
/**
* The number of connect retries for the initial connect. See {@link XltChromeDriver#initConnect(int)}.
*/
private static final int CONNECT_RETRY_COUNT = 5;
/**
* The timeout to start with during the initial connect. See {@link XltChromeDriver#initConnect(int)}.
*/
private static final long CONNECT_RETRY_BASE_TIMEOUT = 500;
/**
* The factor used to increase the timeout during the initial connect for each retry. See
* {@link XltChromeDriver#initConnect(int)}.
*/
private static final float CONNECT_RETRY_TIMEOUT_FACTOR = 1.5f;
/**
* If set to true then the test run will succeed also if we were not able to get the performance data due to a
* connection issue. In that case a session event is logged. Otherwise an exception is thrown which will break the
* test. Default is true. {@link XltChromeDriver#preQuit()}.
*/
private static final String PROPERTY_IGNORE_MISSING_DATA = PROPERTY_DOMAIN + "ignoreMissingData";
/**
* Hold the value read from xlt properties. See {@link XltChromeDriver#PROPERTY_CONNECT_TIMEOUT_IGNORE}
*/
private static final boolean IGNORE_MISSING_DATA;
/**
* Handle extension communication, send and receive messages and handle connection state changes
*/
private final WebExtConnectionHandler connectionHandler = WebExtConnectionHandler.newInstance(PROPERTY_DOMAIN);
static
{
// read in and remember settings
final XltProperties props = XltProperties.getInstance();
HEADLESS_ENABLED = props.getProperty(PROPERTY_HEADLESS, false);
RECORD_INCOMPLETE_ENABLED = props.getProperty(PROPERTY_RECORD_INCOMPLETE, false);
IGNORE_MISSING_DATA = props.getProperty(PROPERTY_IGNORE_MISSING_DATA, true);
// unpack the extension file to the temp directory
try
{
final File tmpFile = File.createTempFile(EXTENSION_FILE_NAME, EXTENSION_FILE_ENDING);
tmpFile.deleteOnExit();
final URL extensionUrl = ClientPerformanceUtils.class.getResource(EXTENSION_FILE_NAME + EXTENSION_FILE_ENDING);
if (extensionUrl == null)
{
LOG.error("Failed to locate Chrome extension file in class path");
}
else
{
FileUtils.copyURLToFile(extensionUrl, tmpFile);
extensionFile = tmpFile;
}
}
catch (final Exception e)
{
LOG.error("Failed to unpack Chrome extension to temp folder", e);
}
}
/**
* Creates a new {@link XltChromeDriver} instance with default settings.
*/
public XltChromeDriver()
{
this(null, null, HEADLESS_ENABLED);
}
/**
* Creates a new {@link XltChromeDriver} instance with the given parameters and otherwise default settings.
*
* @param options
* the options to use (may be null
)
*/
public XltChromeDriver(final ChromeOptions options)
{
this(null, options, HEADLESS_ENABLED);
}
/**
* Creates a new {@link XltChromeDriver} instance with the given parameters and otherwise default settings.
*
* @param options
* the options to use (may be null
)
* @param screenless
* whether to run in headless mode (using Xvfb)
*/
public XltChromeDriver(final ChromeOptions options, final boolean screenless)
{
this(null, options, screenless);
}
/**
* Creates a new {@link XltChromeDriver} instance with the given parameters and otherwise default settings.
*
* @param service
* the driver service (may be null
)
*/
public XltChromeDriver(final ChromeDriverService service)
{
this(service, null, HEADLESS_ENABLED);
}
/**
* Creates a new {@link XltChromeDriver} instance with the given parameters and otherwise default settings.
*
* @param service
* the driver service (may be null
)
* @param options
* the options to use (may be null
)
*/
public XltChromeDriver(final ChromeDriverService service, final ChromeOptions options)
{
this(service, options, HEADLESS_ENABLED);
}
/**
* Creates a new {@link XltChromeDriver} instance with the given parameters.
*
* @param service
* the driver service (may be null
)
* @param options
* the options to use (may be null
)
* @param screenless
* whether to run in headless mode (using Xvfb)
*/
public XltChromeDriver(final ChromeDriverService service, final ChromeOptions options, final boolean screenless)
{
super(modifyService(service, screenless), modifyOptions(options));
init();
}
private void init()
{
try
{
LOG.debug("Starting extension communication server");
connectionHandler.start();
initConnect(CONNECT_RETRY_COUNT);
}
catch (final CommunicationException e)
{
throw new WebDriverException("Starting extension communication failed", e);
}
}
/**
* Send the connect properties to the extension and wait for the connect. Retry a few times if no connection was
* made within a certain time as defined by {@link XltChromeDriver#CONNECT_RETRY_BASE_TIMEOUT}. For each retry
* increase the timeout as defined by {@link XltChromeDriver#CONNECT_RETRY_TIMEOUT_FACTOR}. If no connection was
* made then this will just continue so that we can retry later.
*
* @param retryCount
* - how often should we try to get a working connection
*/
private void initConnect(final int retryCount)
{
final String url = "data:,xltParameters?xltPort=" + connectionHandler.getPort() + "&clientID=" + connectionHandler.getID() +
"&recordIncompleted=" + RECORD_INCOMPLETE_ENABLED;
long timeout = CONNECT_RETRY_BASE_TIMEOUT;
int tries = 0;
do
{
tries++;
if (LOG.isDebugEnabled())
{
LOG.debug("Try: #" + tries + ". Sending connect parameters: " + url);
}
get(url);
try
{
if (LOG.isDebugEnabled())
{
LOG.debug("Waiting for " + timeout + " ms.");
}
connectionHandler.waitForConnect(timeout);
}
catch (CommunicationException | InterruptedException e)
{
LOG.warn("Error while waiting for connection.", e);
}
catch (java.util.concurrent.TimeoutException e)
{
LOG.debug("Timeout while waiting for connect.");
}
timeout = (long) (timeout * CONNECT_RETRY_TIMEOUT_FACTOR);
}
while (!connectionHandler.isConnected() && tries < retryCount);
}
/**
* Modifies the passed driver service for headless operation.
*
* @param service
* the driver service (may be null
)
* @param headless
* whether to run the browser in headless mode
* @return the modified service
*/
private static ChromeDriverService modifyService(ChromeDriverService service, final boolean headless)
{
// get/create the driver service
service = ObjectUtils.defaultIfNull(service, ChromeDriverService.createDefaultService());
// modify the service's environment for headless mode
if (headless)
{
final String display = ClientPerformanceUtils.getDisplay();
if (display != null)
{
// HACK: we can access the service's environment settings via reflection only
// read the environment settings
final Map environment = ReflectionUtils.readField(ChromeDriverService.class, service,
FIELD_NAME_ENVIRONMENT);
// create the new environment settings including the DISPLAY variable
final ImmutableMap.Builder mapBuilder = new ImmutableMap.Builder<>();
if (environment != null)
{
mapBuilder.putAll(environment);
}
mapBuilder.put("DISPLAY", display);
mapBuilder.put("DBUS_SESSION_BUS_ADDRESS", "/dev/null");
final ImmutableMap newEnvironment = mapBuilder.build();
// finally write the new environment settings
ReflectionUtils.writeField(ChromeDriverService.class, service, FIELD_NAME_ENVIRONMENT, newEnvironment);
}
}
return service;
}
/**
* Modifies the given options for client-performance measurements and headless operation.
*
* @param options
* the options
* @return the modified options
*/
private static ChromeOptions modifyOptions(ChromeOptions options)
{
// check if the extension file is (still) available
if (extensionFile == null || !extensionFile.isFile())
{
throw new WebDriverException("Chrome client performance extension not available (path: " + extensionFile + ")");
}
// get/create the options
options = ObjectUtils.defaultIfNull(options, new ChromeOptions());
// modify the options as needed
options.addExtensions(extensionFile);
options.addArguments("--unlimited-storage");
return options;
}
/**
* {@inheritDoc}
*/
@Override
public void close()
{
if (isConnected() && getWindowHandles().size() == 1)
{
quit();
}
else
{
super.close();
}
}
/**
* {@inheritDoc}
*/
@Override
public void quit()
{
if (!isConnected())
{
LOG.debug("Driver already closed");
return;
}
try
{
preQuit();
LOG.debug("Closing extension communication");
connectionHandler.stop();
}
catch (final WebDriverException t)
{
LOG.warn("Error during driver shutdown", t);
throw t;
}
catch (final Throwable t)
{
LOG.warn("Error during driver shutdown", t);
}
finally
{
LOG.debug("Closing driver");
super.quit();
LOG.debug("Chrome client performance driver closed");
}
}
private boolean isConnected()
{
return getSessionId() != null;
}
/**
* Performs any house-keeping actions needed before a driver can really be quit. Currently, this includes retrieving
* (final) request and performance data from the browser and reporting it to XLT.
*
* Note that this method will also be called directly from {@link WebDriverActionDirector}. However, to avoid making
* this method public API, it will be private and hence needs to be called reflectively. For this reason, don't
* rename it without adjusting {@link WebDriverActionDirector} accordingly.
*/
private void preQuit()
{
if (!isConnected())
{
return;
}
if (!hasWindow())
{
LOG.error("Failed to get client-performance metrics. All browser windows already closed.");
}
else
{
LOG.debug("Try to fetch and dump remaining client-performance metrics");
if (!connectionHandler.isConnected())
{
LOG.debug("Not connected. Try reconnect...");
initConnect(CONNECT_RETRY_COUNT);
}
if (connectionHandler.isConnected())
{
connectionHandler.reportRemainingPerformanceData();
}
else
{
final String logMessage = "No connection to fetch remaining data. Maybe not all performance data is available.";
if (IGNORE_MISSING_DATA)
{
LOG.error(logMessage);
SessionImpl.logEvent(getClass().getSimpleName(), logMessage);
}
else
{
throw new WebDriverException(logMessage);
}
}
}
}
private boolean hasWindow()
{
try
{
return getWindowHandles().size() > 0;
}
catch (final Throwable e)
{
return false;
}
}
/**
* Returns a {@link Builder} object to create a new {@link XltChromeDriver} instance.
*
* @return the builder
*/
public static Builder xltBuilder()
{
return new Builder();
}
/**
* Builder class to create {@link XltChromeDriver} instances. First set the desired properties and then call
* {@link #build()} to get the configured driver instance.
*/
public static final class Builder
{
private ChromeDriverService service;
private ChromeOptions options;
private boolean headless = HEADLESS_ENABLED;
/**
* Sets the desired driver service.
*
* @param service
* the service
* @return this builder instance
*/
public Builder setService(final ChromeDriverService service)
{
this.service = service;
return this;
}
/**
* Sets the desired options. Cannot be used together with {@link #setCapabilities(Capabilities)}.
*
* @param options
* the options
* @return this builder instance
*/
public Builder setOptions(final ChromeOptions options)
{
this.options = options;
return this;
}
/**
* Whether to run the browser in headless mode.
*
* @param headless
* whether headless mode is enabled
* @return this builder instance
*/
public Builder setHeadless(final boolean headless)
{
this.headless = headless;
return this;
}
/**
* Creates a new {@link XltChromeDriver} instance configured with all the previously set properties.
*
* @return the driver instance
*/
public XltChromeDriver build()
{
return new XltChromeDriver(service, options, headless);
}
}
}