From f5ef9c5f8cc3f213a4f5576874ea86e594429a85 Mon Sep 17 00:00:00 2001 From: Julia <51991513+js-xc@users.noreply.github.com> Date: Fri, 5 Apr 2024 13:23:44 +0200 Subject: [PATCH 01/13] #480: Comparison Report: Add Web Vitals page --- config/diffreportgenerator.properties | 7 +- config/xsl/diffreport/sections/navigation.xsl | 1 + config/xsl/diffreport/sections/web-vitals.xsl | 108 ++++++++++++++++++ config/xsl/diffreport/text/descriptions.xsl | 84 ++++++++++++++ config/xsl/diffreport/util/timer-cell.xsl | 12 +- config/xsl/diffreport/web-vitals.xsl | 88 ++++++++++++++ .../DiffReportGeneratorConfiguration.java | 1 + 7 files changed, 296 insertions(+), 5 deletions(-) create mode 100644 config/xsl/diffreport/sections/web-vitals.xsl create mode 100644 config/xsl/diffreport/web-vitals.xsl diff --git a/config/diffreportgenerator.properties b/config/diffreportgenerator.properties index bd0642a3b..896e32441 100644 --- a/config/diffreportgenerator.properties +++ b/config/diffreportgenerator.properties @@ -74,5 +74,8 @@ com.xceptance.xlt.diffreportgenerator.transformations.4.outputFileName = request com.xceptance.xlt.diffreportgenerator.transformations.5.styleSheetFileName = page-load-timings.xsl com.xceptance.xlt.diffreportgenerator.transformations.5.outputFileName = page-load-timings.html -com.xceptance.xlt.diffreportgenerator.transformations.6.styleSheetFileName = custom-timers.xsl -com.xceptance.xlt.diffreportgenerator.transformations.6.outputFileName = custom-timers.html \ No newline at end of file +com.xceptance.xlt.diffreportgenerator.transformations.6.styleSheetFileName = web-vitals.xsl +com.xceptance.xlt.diffreportgenerator.transformations.6.outputFileName = web-vitals.html + +com.xceptance.xlt.diffreportgenerator.transformations.7.styleSheetFileName = custom-timers.xsl +com.xceptance.xlt.diffreportgenerator.transformations.7.outputFileName = custom-timers.html \ No newline at end of file diff --git a/config/xsl/diffreport/sections/navigation.xsl b/config/xsl/diffreport/sections/navigation.xsl index 77c7a852b..f0828a02d 100644 --- a/config/xsl/diffreport/sections/navigation.xsl +++ b/config/xsl/diffreport/sections/navigation.xsl @@ -12,6 +12,7 @@
  • Actions
  • Requests
  • Page Load Timings
  • +
  • Web Vitals
  • Custom Timers
  • diff --git a/config/xsl/diffreport/sections/web-vitals.xsl b/config/xsl/diffreport/sections/web-vitals.xsl new file mode 100644 index 000000000..495d568ba --- /dev/null +++ b/config/xsl/diffreport/sections/web-vitals.xsl @@ -0,0 +1,108 @@ + + + + + +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Action Name +
    + + +
    First Contentful Paint
    (FCP)
    Largest Contentful Paint
    (LCP)
    Cumulative Layout Shift
    (CLS)
    First Input Delay
    (FID)
    Interaction to Next Paint
    (INP)
    Time to First Byte
    (TTFB)
    + +
    No data available
    +
    +
    + +
    + +
    \ No newline at end of file diff --git a/config/xsl/diffreport/text/descriptions.xsl b/config/xsl/diffreport/text/descriptions.xsl index 7517711bc..01d341f1b 100644 --- a/config/xsl/diffreport/text/descriptions.xsl +++ b/config/xsl/diffreport/text/descriptions.xsl @@ -168,6 +168,90 @@ + + +

    Web Vitals

    +
    + +
    + +

    + The Web Vitals section helps you evaluate how well your pages perform on aspects that are important for a great user experience. + + + +

    +
    +

    + Web Vitals is + a Google initiative to provide unified guidance for web page + quality signals that are essential to delivering a great + user experience on the web. It aims to simplify the wide + variety of available performance-measuring tools, and help + site owners focus on the metrics that matter most, the Core + Web Vitals.

    + +

    Core Web Vitals

    + +

    + Largest + Contentful Paint · Perceived Load + Speed
    LCP reports the render time of the + largest image or text block visible in the viewport, + relative to when the user first navigated to the page.

    + +

    + First Input + Delay · Interactivity
    FID measures the + time from when a user first interacts with a page (that is, + when they click a link, tap on a button, or use a custom, + JavaScript-powered control) to the time when the browser is + actually able to begin processing event handlers in response + to that interaction.

    + +

    + Cumulative + Layout Shift · Visual Stability
    CLS is a + measure of the largest burst of layout shift scores for + every unexpected layout shift that occurs during the + lifespan of a page.

    + +

    Other Web Vitals

    + +

    + First + Contentful Paint · Perceived Load + Speed
    The FCP metric measures the time from + when the user first navigated to the page to when any part + of the page's content is rendered on the screen.

    + +

    + Interaction to + Next Paint · Interactivity
    INP is a + metric that assesses a page's overall responsiveness to user + interactions by observing the latency of all click, tap, and + keyboard interactions that occur throughout the lifespan of + a user's visit to a page. The final INP value is the longest + interaction observed, ignoring outliers.

    + +

    + Time to First + Byte · Server Responsiveness
    TTFB is a + metric that measures the time between the request for a + resource and when the first byte of a response begins to + arrive.

    + +

    Scores

    + +

    + The displayed score value for a Web Vital is the 75th + percentile (estimated) of all measurements in a given + action. +

    +
    +
    +
    +

    Custom Timers

    diff --git a/config/xsl/diffreport/util/timer-cell.xsl b/config/xsl/diffreport/util/timer-cell.xsl index 0d5c9be6e..e4fa8c921 100644 --- a/config/xsl/diffreport/util/timer-cell.xsl +++ b/config/xsl/diffreport/util/timer-cell.xsl @@ -5,9 +5,13 @@ + + + + @@ -67,15 +71,17 @@ + - + -> - + ( + - + ) + value number diff --git a/config/xsl/diffreport/web-vitals.xsl b/config/xsl/diffreport/web-vitals.xsl new file mode 100644 index 000000000..a58747eac --- /dev/null +++ b/config/xsl/diffreport/web-vitals.xsl @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +<!DOCTYPE html> + + + + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + +
    +
    + + + + + + +
    + +
    diff --git a/src/main/java/com/xceptance/xlt/report/diffreport/DiffReportGeneratorConfiguration.java b/src/main/java/com/xceptance/xlt/report/diffreport/DiffReportGeneratorConfiguration.java index 522ef4229..2431256cc 100644 --- a/src/main/java/com/xceptance/xlt/report/diffreport/DiffReportGeneratorConfiguration.java +++ b/src/main/java/com/xceptance/xlt/report/diffreport/DiffReportGeneratorConfiguration.java @@ -95,6 +95,7 @@ public List getDiffElementSpecifications() specs.add(new ElementSpecification("/testreport/requests/request", "name")); specs.add(new ElementSpecification("/testreport/pageLoadTimings/pageLoadTiming", "name")); specs.add(new ElementSpecification("/testreport/customTimers/customTimer", "name")); + specs.add(new ElementSpecification("/testreport/webVitalsList/webVitals", "name")); specs.add(new ElementSpecification("/testreport/summary/*", "name")); return specs; From 7fbf09504f8a965792fc6228cacff4efc3fd78a4 Mon Sep 17 00:00:00 2001 From: Julia <51991513+js-xc@users.noreply.github.com> Date: Thu, 18 Apr 2024 10:44:37 +0200 Subject: [PATCH 02/13] #489: Comparison Report: Add custom values page --- config/diffreportgenerator.properties | 5 +- config/testreport/css/default.css | 27 +++- config/xsl/diffreport/custom-values.xsl | 88 ++++++++++ .../xsl/diffreport/sections/custom-values.xsl | 150 ++++++++++++++++++ config/xsl/diffreport/sections/navigation.xsl | 1 + config/xsl/diffreport/text/descriptions.xsl | 13 ++ config/xsl/diffreport/util/timer-cell.xsl | 12 +- .../DiffReportGeneratorConfiguration.java | 1 + 8 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 config/xsl/diffreport/custom-values.xsl create mode 100644 config/xsl/diffreport/sections/custom-values.xsl diff --git a/config/diffreportgenerator.properties b/config/diffreportgenerator.properties index 896e32441..e0c66a61d 100644 --- a/config/diffreportgenerator.properties +++ b/config/diffreportgenerator.properties @@ -78,4 +78,7 @@ com.xceptance.xlt.diffreportgenerator.transformations.6.styleSheetFileName = web com.xceptance.xlt.diffreportgenerator.transformations.6.outputFileName = web-vitals.html com.xceptance.xlt.diffreportgenerator.transformations.7.styleSheetFileName = custom-timers.xsl -com.xceptance.xlt.diffreportgenerator.transformations.7.outputFileName = custom-timers.html \ No newline at end of file +com.xceptance.xlt.diffreportgenerator.transformations.7.outputFileName = custom-timers.html + +com.xceptance.xlt.diffreportgenerator.transformations.8.styleSheetFileName = custom-values.xsl +com.xceptance.xlt.diffreportgenerator.transformations.8.outputFileName = custom-values.html \ No newline at end of file diff --git a/config/testreport/css/default.css b/config/testreport/css/default.css index 37a5ea800..1049c4aa1 100644 --- a/config/testreport/css/default.css +++ b/config/testreport/css/default.css @@ -1575,6 +1575,19 @@ table * { .n90, .n91, .n92, .n93, .n94, .n95, .n96, .n97, .n98, .n99 { background-color: rgb(206,134,134) !important; } .n100 { background-color: rgb(200,120,120) !important; } +.a0 { background-color: rgba(238,242,246,0.7) !important; } +.a1, .a2, .a3, .a4, .a5, .a6, .a7, .a8, .a9 { background-color: rgba(222,230,237,0.7) !important; } +.a10, .a11, .a12, .a13, .a14, .a15, .a16, .a17, .a18, .a19 { background-color: rgba(206,217,228,0.7) !important; } +.a20, .a21, .a22, .a23, .a24, .a25, .a26, .a27, .a28, .a29 { background-color: rgba(189,204,219,0.7) !important; } +.a30, .a31, .a32, .a33, .a34, .a35, .a36, .a37, .a38, .a39 { background-color: rgba(173,191,210,0.7) !important; } +.a40, .a41, .a42, .a43, .a44, .a45, .a46, .a47, .a48, .a49 { background-color: rgba(156,178,201,0.7) !important; } +.a50, .a51, .a52, .a53, .a54, .a55, .a56, .a57, .a58, .a59 { background-color: rgba(140,165,192,0.7) !important; } +.a60, .a61, .a62, .a63, .a64, .a65, .a66, .a67, .a68, .a69 { background-color: rgba(123,152,183,0.7) !important; } +.a70, .a71, .a72, .a73, .a74, .a75, .a76, .a77, .a78, .a79 { background-color: rgba(107,139,174,0.7) !important; } +.a80, .a81, .a82, .a83, .a84, .a85, .a86, .a87, .a88, .a89 { background-color: rgba(91,126,164,0.7) !important; } +.a90, .a91, .a92, .a93, .a94, .a95, .a96, .a97, .a98, .a99 { background-color: rgba(81,114,148,0.7) !important; } +.a100, .infinity.neutral { background-color: rgba(72,102,132,0.7) !important; } + tr:hover td.p0, tr:hover td.n0 { background: linear-gradient(rgb(188, 203, 220), rgba(0,0,0,0), rgb(188, 203, 220)) !important; } tr:hover td.p1, tr:hover td.p2, tr:hover td.p3, tr:hover td.p4, tr:hover td.p5, tr:hover td.p6, tr:hover td.p7, tr:hover td.p8, tr:hover td.p9 { background: linear-gradient(rgb(235,250,235), rgba(0,0,0,0), rgb(235,250,235)) !important; } @@ -1599,7 +1612,19 @@ tr:hover td.n60 , tr:hover td.n61 , tr:hover td.n62 , tr:hover td.n63 , tr:hover tr:hover td.n70 , tr:hover td.n71 , tr:hover td.n72 , tr:hover td.n73 , tr:hover td.n74 , tr:hover td.n75 , tr:hover td.n76 , tr:hover td.n77 , tr:hover td.n78 , tr:hover td.n79 { background: linear-gradient(rgb(217,161,161), rgba(0,0,0,0), rgb(217,161,161)) !important; } tr:hover td.n80 , tr:hover td.n81 , tr:hover td.n82 , tr:hover td.n83 , tr:hover td.n84 , tr:hover td.n85 , tr:hover td.n86 , tr:hover td.n87 , tr:hover td.n88 , tr:hover td.n89 { background: linear-gradient(rgb(211,147,147), rgba(0,0,0,0), rgb(211,147,147)) !important; } tr:hover td.n90 , tr:hover td.n91 , tr:hover td.n92 , tr:hover td.n93 , tr:hover td.n94 , tr:hover td.n95 , tr:hover td.n96 , tr:hover td.n97 , tr:hover td.n98 , tr:hover td.n99 { background: linear-gradient(rgb(206,134,134), rgba(0,0,0,0), rgb(206,134,134)) !important; } -tr:hover td.n100 { background: linear-gradient(rgb(200,120,120), rgba(0,0,0,0), rgb(200,120,120)) !important; } +tr:hover td.n100 { background: linear-gradient(rgb(200,120,120), rgba(0,0,0,0), rgb(200,120,120)) !important; } + +tr:hover td.a0, tr:hover td.a1, tr:hover td.a2, tr:hover td.a3, tr:hover td.a4, tr:hover td.a5, tr:hover td.a6, tr:hover td.a7, tr:hover td.a8, tr:hover td.a9 { background: linear-gradient(rgba(238,242,246,0.7), rgba(0,0,0,0), rgba(238,242,246,0.7)) !important; } +tr:hover td.a10, tr:hover td.a11, tr:hover td.a12, tr:hover td.a13, tr:hover td.a14, tr:hover td.a15, tr:hover td.a16, tr:hover td.a17, tr:hover td.a18, tr:hover td.a19 { background: linear-gradient(rgba(222,230,237,0.7), rgba(0,0,0,0), rgba(222,230,237,0.7)) !important; } +tr:hover td.a20, tr:hover td.a21, tr:hover td.a22, tr:hover td.a23, tr:hover td.a24, tr:hover td.a25, tr:hover td.a26, tr:hover td.a27, tr:hover td.a28, tr:hover td.a29 { background: linear-gradient(rgba(206,217,228,0.7), rgba(0,0,0,0), rgba(206,217,228,0.7)) !important; } +tr:hover td.a30, tr:hover td.a31, tr:hover td.a32, tr:hover td.a33, tr:hover td.a34, tr:hover td.a35, tr:hover td.a36, tr:hover td.a37, tr:hover td.a38, tr:hover td.a39 { background: linear-gradient(rgba(189,204,219,0.7), rgba(0,0,0,0), rgba(189,204,219,0.7)) !important; } +tr:hover td.a40, tr:hover td.a41, tr:hover td.a42, tr:hover td.a43, tr:hover td.a44, tr:hover td.a45, tr:hover td.a46, tr:hover td.a47, tr:hover td.a48, tr:hover td.a49 { background: linear-gradient(rgba(173,191,210,0.7), rgba(0,0,0,0), rgba(173,191,210,0.7)) !important; } +tr:hover td.a50, tr:hover td.a51, tr:hover td.a52, tr:hover td.a53, tr:hover td.a54, tr:hover td.a55, tr:hover td.a56, tr:hover td.a57, tr:hover td.a58, tr:hover td.a59 { background: linear-gradient(rgba(156,178,201,0.7), rgba(0,0,0,0), rgba(156,178,201,0.7)) !important; } +tr:hover td.a60, tr:hover td.a61, tr:hover td.a62, tr:hover td.a63, tr:hover td.a64, tr:hover td.a65, tr:hover td.a66, tr:hover td.a67, tr:hover td.a68, tr:hover td.a69 { background: linear-gradient(rgba(140,165,192,0.7), rgba(0,0,0,0), rgba(140,165,192,0.7)) !important; } +tr:hover td.a70, tr:hover td.a71, tr:hover td.a72, tr:hover td.a73, tr:hover td.a74, tr:hover td.a75, tr:hover td.a76, tr:hover td.a77, tr:hover td.a78, tr:hover td.a79 { background: linear-gradient(rgba(123,152,183,0.7), rgba(0,0,0,0), rgba(123,152,183,0.7)) !important; } +tr:hover td.a80, tr:hover td.a81, tr:hover td.a82, tr:hover td.a83, tr:hover td.a84, tr:hover td.a85, tr:hover td.a86, tr:hover td.a87, tr:hover td.a88, tr:hover td.a89 { background: linear-gradient(rgba(107,139,174,0.7), rgba(0,0,0,0), rgba(107,139,174,0.7)) !important; } +tr:hover td.a90, tr:hover td.a91, tr:hover td.a92, tr:hover td.a93, tr:hover td.a94, tr:hover td.a95, tr:hover td.a96, tr:hover td.a97, tr:hover td.a98, tr:hover td.a99 { background: linear-gradient(rgba(91,126,164,0.7), rgba(0,0,0,0), rgba(91,126,164,0.7)) !important; } +tr:hover td.a100, tr:hover .infinity.neutral { background: linear-gradient(rgba(81,114,148,0.7), rgba(0,0,0,0), rgba(81,114,148,0.7)) !important; } .added { color: #888 !important; } diff --git a/config/xsl/diffreport/custom-values.xsl b/config/xsl/diffreport/custom-values.xsl new file mode 100644 index 000000000..9ba05b453 --- /dev/null +++ b/config/xsl/diffreport/custom-values.xsl @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +<!DOCTYPE html> + + + + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + +
    +
    + + + + + + +
    + +
    diff --git a/config/xsl/diffreport/sections/custom-values.xsl b/config/xsl/diffreport/sections/custom-values.xsl new file mode 100644 index 000000000..3803b63d8 --- /dev/null +++ b/config/xsl/diffreport/sections/custom-values.xsl @@ -0,0 +1,150 @@ + + + + + +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Value Name +
    + + +
    CountStatsPercentiles
    Total1/s1/h*1/d*MeanMin.Max.Dev. + +
    + + + tableEntry- + + +
    No data available
    +
    +
    + +
    + +
    \ No newline at end of file diff --git a/config/xsl/diffreport/sections/navigation.xsl b/config/xsl/diffreport/sections/navigation.xsl index f0828a02d..fc7bd59d1 100644 --- a/config/xsl/diffreport/sections/navigation.xsl +++ b/config/xsl/diffreport/sections/navigation.xsl @@ -14,6 +14,7 @@
  • Page Load Timings
  • Web Vitals
  • Custom Timers
  • +
  • Custom Values
  • diff --git a/config/xsl/diffreport/text/descriptions.xsl b/config/xsl/diffreport/text/descriptions.xsl index 01d341f1b..e541922d2 100644 --- a/config/xsl/diffreport/text/descriptions.xsl +++ b/config/xsl/diffreport/text/descriptions.xsl @@ -265,6 +265,19 @@
    + + +

    Custom Values

    +
    + +
    +

    + The custom values include all values that have been recorded by your custom samplers. +

    +

    *) numbers might be projected

    +
    +
    + diff --git a/config/xsl/diffreport/util/timer-cell.xsl b/config/xsl/diffreport/util/timer-cell.xsl index e4fa8c921..55c5e038b 100644 --- a/config/xsl/diffreport/util/timer-cell.xsl +++ b/config/xsl/diffreport/util/timer-cell.xsl @@ -3,9 +3,10 @@ - + - + + @@ -41,7 +42,7 @@ - + @@ -57,6 +58,10 @@ + + a + + n @@ -86,6 +91,7 @@ value number colorized + neutral diff --git a/src/main/java/com/xceptance/xlt/report/diffreport/DiffReportGeneratorConfiguration.java b/src/main/java/com/xceptance/xlt/report/diffreport/DiffReportGeneratorConfiguration.java index 2431256cc..6d3de4858 100644 --- a/src/main/java/com/xceptance/xlt/report/diffreport/DiffReportGeneratorConfiguration.java +++ b/src/main/java/com/xceptance/xlt/report/diffreport/DiffReportGeneratorConfiguration.java @@ -95,6 +95,7 @@ public List getDiffElementSpecifications() specs.add(new ElementSpecification("/testreport/requests/request", "name")); specs.add(new ElementSpecification("/testreport/pageLoadTimings/pageLoadTiming", "name")); specs.add(new ElementSpecification("/testreport/customTimers/customTimer", "name")); + specs.add(new ElementSpecification("/testreport/customValues/customValue", "name")); specs.add(new ElementSpecification("/testreport/webVitalsList/webVitals", "name")); specs.add(new ElementSpecification("/testreport/summary/*", "name")); From 465a09cf1a32357f3c096e7deaaa51c779b57ca4 Mon Sep 17 00:00:00 2001 From: Joerg Werner <4639399+jowerner@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:27:55 +0200 Subject: [PATCH 03/13] * removed any color info from the `infinity` CSS class * added the correct color class to infinity cells instead * additionally fixed the cell background for the `a0` class to be the standard gray background --- config/testreport/css/default.css | 37 +++++++++++------------ config/xsl/diffreport/util/timer-cell.xsl | 29 +++++++++--------- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/config/testreport/css/default.css b/config/testreport/css/default.css index 1049c4aa1..0a6031c9a 100644 --- a/config/testreport/css/default.css +++ b/config/testreport/css/default.css @@ -1575,7 +1575,7 @@ table * { .n90, .n91, .n92, .n93, .n94, .n95, .n96, .n97, .n98, .n99 { background-color: rgb(206,134,134) !important; } .n100 { background-color: rgb(200,120,120) !important; } -.a0 { background-color: rgba(238,242,246,0.7) !important; } +.a0 { background-color: rgba(245,245,245) !important; } .a1, .a2, .a3, .a4, .a5, .a6, .a7, .a8, .a9 { background-color: rgba(222,230,237,0.7) !important; } .a10, .a11, .a12, .a13, .a14, .a15, .a16, .a17, .a18, .a19 { background-color: rgba(206,217,228,0.7) !important; } .a20, .a21, .a22, .a23, .a24, .a25, .a26, .a27, .a28, .a29 { background-color: rgba(189,204,219,0.7) !important; } @@ -1586,9 +1586,9 @@ table * { .a70, .a71, .a72, .a73, .a74, .a75, .a76, .a77, .a78, .a79 { background-color: rgba(107,139,174,0.7) !important; } .a80, .a81, .a82, .a83, .a84, .a85, .a86, .a87, .a88, .a89 { background-color: rgba(91,126,164,0.7) !important; } .a90, .a91, .a92, .a93, .a94, .a95, .a96, .a97, .a98, .a99 { background-color: rgba(81,114,148,0.7) !important; } -.a100, .infinity.neutral { background-color: rgba(72,102,132,0.7) !important; } +.a100 { background-color: rgba(72,102,132,0.7) !important; } -tr:hover td.p0, tr:hover td.n0 { background: linear-gradient(rgb(188, 203, 220), rgba(0,0,0,0), rgb(188, 203, 220)) !important; } +tr:hover td.p0, tr:hover td.n0, tr:hover td.a0 { background: linear-gradient(rgb(188, 203, 220), rgba(0,0,0,0), rgb(188, 203, 220)) !important; } tr:hover td.p1, tr:hover td.p2, tr:hover td.p3, tr:hover td.p4, tr:hover td.p5, tr:hover td.p6, tr:hover td.p7, tr:hover td.p8, tr:hover td.p9 { background: linear-gradient(rgb(235,250,235), rgba(0,0,0,0), rgb(235,250,235)) !important; } tr:hover td.p10, tr:hover td.p11, tr:hover td.p12, tr:hover td.p13, tr:hover td.p14, tr:hover td.p15, tr:hover td.p16, tr:hover td.p17, tr:hover td.p18, tr:hover td.p19 { background: linear-gradient(rgb(229,246,229), rgba(0,0,0,0), rgb(229,246,229)) !important; } @@ -1602,19 +1602,19 @@ tr:hover td.p80, tr:hover td.p81, tr:hover td.p82, tr:hover td.p83, tr:hover td. tr:hover td.p90, tr:hover td.p91, tr:hover td.p92, tr:hover td.p93, tr:hover td.p94, tr:hover td.p95, tr:hover td.p96, tr:hover td.p97, tr:hover td.p98, tr:hover td.p99 { background: linear-gradient(rgb(125,170,125), rgba(0,0,0,0), rgb(125,170,125)) !important; } tr:hover td.p100 { background: linear-gradient(rgb(110,160,110), rgba(0,0,0,0), rgb(110,160,110)) !important; } -tr:hover td.n1 , tr:hover td.n2 , tr:hover td.n3 , tr:hover td.n4 , tr:hover td.n5 , tr:hover td.n6 , tr:hover td.n7 , tr:hover td.n8 , tr:hover td.n9 { background: linear-gradient(rgb(253,248,248), rgba(0,0,0,0), rgb(253,248,248)) !important; } -tr:hover td.n10 , tr:hover td.n11 , tr:hover td.n12 , tr:hover td.n13 , tr:hover td.n14 , tr:hover td.n15 , tr:hover td.n16 , tr:hover td.n17 , tr:hover td.n18 , tr:hover td.n19 { background: linear-gradient(rgb(250,242,242), rgba(0,0,0,0), rgb(250,242,242)) !important; } -tr:hover td.n20 , tr:hover td.n21 , tr:hover td.n22 , tr:hover td.n23 , tr:hover td.n24 , tr:hover td.n25 , tr:hover td.n26 , tr:hover td.n27 , tr:hover td.n28 , tr:hover td.n29 { background: linear-gradient(rgb(244,228,228), rgba(0,0,0,0), rgb(244,228,228)) !important; } -tr:hover td.n30 , tr:hover td.n31 , tr:hover td.n32 , tr:hover td.n33 , tr:hover td.n34 , tr:hover td.n35 , tr:hover td.n36 , tr:hover td.n37 , tr:hover td.n38 , tr:hover td.n39 { background: linear-gradient(rgb(239,215,215), rgba(0,0,0,0), rgb(239,215,215)) !important; } -tr:hover td.n40 , tr:hover td.n41 , tr:hover td.n42 , tr:hover td.n43 , tr:hover td.n44 , tr:hover td.n45 , tr:hover td.n46 , tr:hover td.n47 , tr:hover td.n48 , tr:hover td.n49 { background: linear-gradient(rgb(233,201,201), rgba(0,0,0,0), rgb(233,201,201)) !important; } -tr:hover td.n50 , tr:hover td.n51 , tr:hover td.n52 , tr:hover td.n53 , tr:hover td.n54 , tr:hover td.n55 , tr:hover td.n56 , tr:hover td.n57 , tr:hover td.n58 , tr:hover td.n59 { background: linear-gradient(rgb(228,188,188), rgba(0,0,0,0), rgb(228,188,188)) !important; } -tr:hover td.n60 , tr:hover td.n61 , tr:hover td.n62 , tr:hover td.n63 , tr:hover td.n64 , tr:hover td.n65 , tr:hover td.n66 , tr:hover td.n67 , tr:hover td.n68 , tr:hover td.n69 { background: linear-gradient(rgb(222,174,174), rgba(0,0,0,0), rgb(222,174,174)) !important; } -tr:hover td.n70 , tr:hover td.n71 , tr:hover td.n72 , tr:hover td.n73 , tr:hover td.n74 , tr:hover td.n75 , tr:hover td.n76 , tr:hover td.n77 , tr:hover td.n78 , tr:hover td.n79 { background: linear-gradient(rgb(217,161,161), rgba(0,0,0,0), rgb(217,161,161)) !important; } -tr:hover td.n80 , tr:hover td.n81 , tr:hover td.n82 , tr:hover td.n83 , tr:hover td.n84 , tr:hover td.n85 , tr:hover td.n86 , tr:hover td.n87 , tr:hover td.n88 , tr:hover td.n89 { background: linear-gradient(rgb(211,147,147), rgba(0,0,0,0), rgb(211,147,147)) !important; } -tr:hover td.n90 , tr:hover td.n91 , tr:hover td.n92 , tr:hover td.n93 , tr:hover td.n94 , tr:hover td.n95 , tr:hover td.n96 , tr:hover td.n97 , tr:hover td.n98 , tr:hover td.n99 { background: linear-gradient(rgb(206,134,134), rgba(0,0,0,0), rgb(206,134,134)) !important; } -tr:hover td.n100 { background: linear-gradient(rgb(200,120,120), rgba(0,0,0,0), rgb(200,120,120)) !important; } - -tr:hover td.a0, tr:hover td.a1, tr:hover td.a2, tr:hover td.a3, tr:hover td.a4, tr:hover td.a5, tr:hover td.a6, tr:hover td.a7, tr:hover td.a8, tr:hover td.a9 { background: linear-gradient(rgba(238,242,246,0.7), rgba(0,0,0,0), rgba(238,242,246,0.7)) !important; } +tr:hover td.n1, tr:hover td.n2, tr:hover td.n3, tr:hover td.n4, tr:hover td.n5, tr:hover td.n6, tr:hover td.n7, tr:hover td.n8, tr:hover td.n9 { background: linear-gradient(rgb(253,248,248), rgba(0,0,0,0), rgb(253,248,248)) !important; } +tr:hover td.n10, tr:hover td.n11, tr:hover td.n12, tr:hover td.n13, tr:hover td.n14, tr:hover td.n15, tr:hover td.n16, tr:hover td.n17, tr:hover td.n18, tr:hover td.n19 { background: linear-gradient(rgb(250,242,242), rgba(0,0,0,0), rgb(250,242,242)) !important; } +tr:hover td.n20, tr:hover td.n21, tr:hover td.n22, tr:hover td.n23, tr:hover td.n24, tr:hover td.n25, tr:hover td.n26, tr:hover td.n27, tr:hover td.n28, tr:hover td.n29 { background: linear-gradient(rgb(244,228,228), rgba(0,0,0,0), rgb(244,228,228)) !important; } +tr:hover td.n30, tr:hover td.n31, tr:hover td.n32, tr:hover td.n33, tr:hover td.n34, tr:hover td.n35, tr:hover td.n36, tr:hover td.n37, tr:hover td.n38, tr:hover td.n39 { background: linear-gradient(rgb(239,215,215), rgba(0,0,0,0), rgb(239,215,215)) !important; } +tr:hover td.n40, tr:hover td.n41, tr:hover td.n42, tr:hover td.n43, tr:hover td.n44, tr:hover td.n45, tr:hover td.n46, tr:hover td.n47, tr:hover td.n48, tr:hover td.n49 { background: linear-gradient(rgb(233,201,201), rgba(0,0,0,0), rgb(233,201,201)) !important; } +tr:hover td.n50, tr:hover td.n51, tr:hover td.n52, tr:hover td.n53, tr:hover td.n54, tr:hover td.n55, tr:hover td.n56, tr:hover td.n57, tr:hover td.n58, tr:hover td.n59 { background: linear-gradient(rgb(228,188,188), rgba(0,0,0,0), rgb(228,188,188)) !important; } +tr:hover td.n60, tr:hover td.n61, tr:hover td.n62, tr:hover td.n63, tr:hover td.n64, tr:hover td.n65, tr:hover td.n66, tr:hover td.n67, tr:hover td.n68, tr:hover td.n69 { background: linear-gradient(rgb(222,174,174), rgba(0,0,0,0), rgb(222,174,174)) !important; } +tr:hover td.n70, tr:hover td.n71, tr:hover td.n72, tr:hover td.n73, tr:hover td.n74, tr:hover td.n75, tr:hover td.n76, tr:hover td.n77, tr:hover td.n78, tr:hover td.n79 { background: linear-gradient(rgb(217,161,161), rgba(0,0,0,0), rgb(217,161,161)) !important; } +tr:hover td.n80, tr:hover td.n81, tr:hover td.n82, tr:hover td.n83, tr:hover td.n84, tr:hover td.n85, tr:hover td.n86, tr:hover td.n87, tr:hover td.n88, tr:hover td.n89 { background: linear-gradient(rgb(211,147,147), rgba(0,0,0,0), rgb(211,147,147)) !important; } +tr:hover td.n90, tr:hover td.n91, tr:hover td.n92, tr:hover td.n93, tr:hover td.n94, tr:hover td.n95, tr:hover td.n96, tr:hover td.n97, tr:hover td.n98, tr:hover td.n99 { background: linear-gradient(rgb(206,134,134), rgba(0,0,0,0), rgb(206,134,134)) !important; } +tr:hover td.n100 { background: linear-gradient(rgb(200,120,120), rgba(0,0,0,0), rgb(200,120,120)) !important; } + +tr:hover td.a1, tr:hover td.a2, tr:hover td.a3, tr:hover td.a4, tr:hover td.a5, tr:hover td.a6, tr:hover td.a7, tr:hover td.a8, tr:hover td.a9 { background: linear-gradient(rgba(238,242,246,0.7), rgba(0,0,0,0), rgba(238,242,246,0.7)) !important; } tr:hover td.a10, tr:hover td.a11, tr:hover td.a12, tr:hover td.a13, tr:hover td.a14, tr:hover td.a15, tr:hover td.a16, tr:hover td.a17, tr:hover td.a18, tr:hover td.a19 { background: linear-gradient(rgba(222,230,237,0.7), rgba(0,0,0,0), rgba(222,230,237,0.7)) !important; } tr:hover td.a20, tr:hover td.a21, tr:hover td.a22, tr:hover td.a23, tr:hover td.a24, tr:hover td.a25, tr:hover td.a26, tr:hover td.a27, tr:hover td.a28, tr:hover td.a29 { background: linear-gradient(rgba(206,217,228,0.7), rgba(0,0,0,0), rgba(206,217,228,0.7)) !important; } tr:hover td.a30, tr:hover td.a31, tr:hover td.a32, tr:hover td.a33, tr:hover td.a34, tr:hover td.a35, tr:hover td.a36, tr:hover td.a37, tr:hover td.a38, tr:hover td.a39 { background: linear-gradient(rgba(189,204,219,0.7), rgba(0,0,0,0), rgba(189,204,219,0.7)) !important; } @@ -1624,19 +1624,16 @@ tr:hover td.a60, tr:hover td.a61, tr:hover td.a62, tr:hover td.a63, tr:hover td. tr:hover td.a70, tr:hover td.a71, tr:hover td.a72, tr:hover td.a73, tr:hover td.a74, tr:hover td.a75, tr:hover td.a76, tr:hover td.a77, tr:hover td.a78, tr:hover td.a79 { background: linear-gradient(rgba(123,152,183,0.7), rgba(0,0,0,0), rgba(123,152,183,0.7)) !important; } tr:hover td.a80, tr:hover td.a81, tr:hover td.a82, tr:hover td.a83, tr:hover td.a84, tr:hover td.a85, tr:hover td.a86, tr:hover td.a87, tr:hover td.a88, tr:hover td.a89 { background: linear-gradient(rgba(107,139,174,0.7), rgba(0,0,0,0), rgba(107,139,174,0.7)) !important; } tr:hover td.a90, tr:hover td.a91, tr:hover td.a92, tr:hover td.a93, tr:hover td.a94, tr:hover td.a95, tr:hover td.a96, tr:hover td.a97, tr:hover td.a98, tr:hover td.a99 { background: linear-gradient(rgba(91,126,164,0.7), rgba(0,0,0,0), rgba(91,126,164,0.7)) !important; } -tr:hover td.a100, tr:hover .infinity.neutral { background: linear-gradient(rgba(81,114,148,0.7), rgba(0,0,0,0), rgba(81,114,148,0.7)) !important; } - +tr:hover td.a100 { background: linear-gradient(rgba(81,114,148,0.7), rgba(0,0,0,0), rgba(81,114,148,0.7)) !important; } .added { color: #888 !important; } .removed { color: #888 !important; } .infinity { - background-color: rgb(200,120,120) !important; /* The infinity symbol is rather small, so we use a larger font size for it, however, without increasing the height of the table cell. */ font-size: 1.3rem; line-height: 0; } -tr:hover .infinity { background: linear-gradient(rgb(200,120,120), rgba(0,0,0,0), rgb(200,120,120)) !important; } /* diff --git a/config/xsl/diffreport/util/timer-cell.xsl b/config/xsl/diffreport/util/timer-cell.xsl index 55c5e038b..e7c7a7fee 100644 --- a/config/xsl/diffreport/util/timer-cell.xsl +++ b/config/xsl/diffreport/util/timer-cell.xsl @@ -8,6 +8,8 @@ + + @@ -19,7 +21,7 @@ - + @@ -40,13 +42,10 @@ - - - - + @@ -77,21 +76,21 @@ - - - -> - - ( - + - - ) - + + + -> + + ( + + + + ) + value number colorized - neutral + infinity From e331b72ac70a86211e02f9450d19d9ddbd119bcd Mon Sep 17 00:00:00 2001 From: xceptance-dan <122790040+xceptance-dan@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:46:57 +0200 Subject: [PATCH 04/13] 307 sorting and filtering via hash in online report feature updated (#365) * Implemented Sorting And Filtering via Hash * Moved registerSortAndFilterListeners() -> has to be called always. * Registering sorting and filter listeners now depends on load event * Fixed event handling (FF was always faster than Chrome regarding load event) * Added sorting support for webvitals (load test) * Implemented id and support for trend reports * Added ids for for diff report --- config/testreport/js/xlt.js | 266 +++++++++++++++++- .../xsl/diffreport/sections/custom-values.xsl | 22 +- config/xsl/diffreport/sections/web-vitals.xsl | 13 +- config/xsl/diffreport/util/timer-section.xsl | 32 +-- config/xsl/diffreport/util/timer-table.xsl | 29 +- config/xsl/loadreport/sections/agents.xsl | 28 +- .../xsl/loadreport/sections/content-types.xsl | 6 +- .../xsl/loadreport/sections/custom-values.xsl | 21 +- config/xsl/loadreport/sections/errors.xsl | 14 +- config/xsl/loadreport/sections/events.xsl | 8 +- config/xsl/loadreport/sections/hosts.xsl | 6 +- .../loadreport/sections/response-codes.xsl | 6 +- config/xsl/loadreport/sections/web-vitals.xsl | 14 +- config/xsl/loadreport/util/timer-section.xsl | 84 +++--- config/xsl/loadreport/util/timer-table.xsl | 43 +-- config/xsl/trendreport/util/timer-table.xsl | 4 + 16 files changed, 432 insertions(+), 164 deletions(-) diff --git a/config/testreport/js/xlt.js b/config/testreport/js/xlt.js index 15d0391a3..8eef58adf 100644 --- a/config/testreport/js/xlt.js +++ b/config/testreport/js/xlt.js @@ -12,17 +12,20 @@ var path = window.location.pathname.split( '/' ); var currentDocument = path[path.length - 1]; - if (targetDocument == currentDocument || targetDocument == "") { - // before we run it, check that this exists - if (targetHashText.length > 0) { - // quote any "." in the hash, otherwise JQuery interprets the following chars as class - targetHashText = targetHashText.replace(/\./g, "\\."); - // we scroll with offset to counter our sticky headers - $.scrollTo(targetHashText, 250, {easing:'swing', offset: {top: -120}}); - - return false; + var hashObj = splitHash(targetHashText); + targetHashText = hashObj.navigation; + + if(targetHashText != undefined){ + if (targetDocument == currentDocument || targetDocument == "") { + // before we run it, check that this exists + if (targetHashText.length > 0) { + // quote any "." in the hash, otherwise JQuery interprets the following chars as class + targetHashText = targetHashText.replace(/\./g, "\\."); + $.scrollTo(targetHashText, 250, {easing:'swing', offset: {top: -35}}); + return false; + } } - } + } } return true; @@ -433,6 +436,13 @@ (function setupBackToTopHandler() { $('nav').click( function(event) { $.scrollTo(0, 250, {easing:'swing'}); + // if there is an anchor remove it from the hash + if(window.location.hash != '') + { + var newHashObj = splitHash(window.location.hash); + newHashObj.navigation = ''; + updateHash(newHashObj); + } }); // stop stopPropagation $('nav li a').click( function(event) { @@ -455,6 +465,17 @@ if (hiddenContent) { hiddenContent.classList.remove("hidden"); } + + // We have to wait for the table.js to finish processing before registering events and + // may be calling switchToTargetTabIfRequired in sort + if (document.readyState === 'complete') { + // console.log("load already done"); + registerSortAndFilterListeners(); + } + else { + // console.log("attach event listener"); + window.addEventListener("load", registerSortAndFilterListeners); + } })(); // the table filters @@ -655,6 +676,231 @@ // see if we jumped and now have to scroll scrollTo(); }); + + // method that is called when the hash is updated. This happens if the user clicks on local anchors (table, charts), sorts tables by sortable table rows, updates the hash + // directly in the URL or if the hash is updated via code + function hashChanged(event){ + + // in some cases we have create a new hash out of a combination (old + new hash). For example, clicking on a request to get to the request charts totally wipes the hash. + // therefore we have to restore the sorting option and filter if there were any provided previously + var oldHashObj = splitHash(event.oldURL); + var newHashObj = splitHash(event.newURL); + + // hashes might contain a sorting option + if(oldHashObj.sort != undefined && newHashObj.sort == undefined){ + newHashObj.sort = oldHashObj.sort; + } + + // hashes might contain a filter + if(oldHashObj.filter != undefined && newHashObj.filter == undefined){ + newHashObj.filter = oldHashObj.filter; + } + + // sometimes sorting must be triggered from the update hash function. For example, when a user directly changes the sorting option (navbar) in the url + if(newHashObj.sort != undefined){ + var sortParam = newHashObj.sort.split('='); + var sortingElem = document.getElementById(sortParam[0]); + var sortingRule = sortParam[1]; + + if(sortingElem != null){ + // this checks if a sorting is required (URL manually edited and not by clicking on a table row) + if(sortingElem.classList.contains("table-sorted-" + sortingRule) == false){ + sort(sortingElem, sortingRule); + } + } + else{ + alert('No sorting element with given ID found: ' + sortParam[0]) + } + } + + updateHash(newHashObj); + } + + // splits the given hash - automatically tries to detect the current format. the returned hash object might contain a "navigation", "sort" and "filter" option + function splitHash(hash){ + var hashObj = {}; + + if(hash !== ""){ + // hash format: http://...#abc + var pos = hash.lastIndexOf('#'); + if (pos >= 0) { + hash = hash.slice(pos); + // hash format: #abc + if(hash.startsWith('#')){ + hash = hash.split('#')[1]; + } + + var split = hash.split('&'); + for(var i = 0; i < split.length; i++){ + var param = split[i]; + if(param.startsWith('sort')){ + hashObj.sort = param; + } + else if(param.startsWith('filter')){ + hashObj.filter = param; + } + else{ + hashObj.navigation = '#' + param; + } + } + } + } + + return hashObj; + } + + // updates the URL hash to the parameters passed in updateHashObj. This change is only applied if the hashObj is different from the current hash. + // if the update is applied a hashchanged event is fired which then calls "hashChanged" + function updateHash(updatedHashObj){ + var newHash = []; + + // check the possible parameters for the hash + if(updatedHashObj.navigation != undefined){ + newHash.push(updatedHashObj.navigation); + } + + if(updatedHashObj.sort != undefined){ + newHash.push(updatedHashObj.sort); + } + + if(updatedHashObj.filter != undefined){ + newHash.push(updatedHashObj.filter); + } + + // check if we have a hash to process + if(newHash.length > 0){ + // create the new hash string -> filter out empty elements (required for removal of anchors in the hash) + var newJoinedHash = newHash.filter(n => n).join('&'); + if(newJoinedHash.startsWith('#') == false){ + newJoinedHash = '#' + newJoinedHash; + } + + // check if the hash is different: only then update it + if(window.location.hash != newJoinedHash){ + // updated hash to params + window.location.hash = newJoinedHash; + } + } + } + + // eventlistener that fires if a sortable table row gets clicked + function updateHashAfterSort(sortingEvent){ + if(sortingEvent.target.classList.contains('table-sortable') && sortingEvent.target.id != undefined){ + var sortingRule = sortingEvent.target.classList.contains('table-sorted-asc') ? 'asc' : 'desc'; + // Get the current hash (if one exists) + var hashObj = splitHash(window.location.hash); + + // update the sorting option of the hash + hashObj.sort = sortingEvent.target.id + '=' + sortingRule; + + // trigger the hash change + updateHash(hashObj); + } + } + + // switches to the given tab if the current one is different + function switchToTargetTabIfRequired(targetTab){ + var requestPageActiveTab = document.querySelector('#tabletabbies > .c-tab[id].c-is-active'); + if(requestPageActiveTab != null){ + var currentTabId = requestPageActiveTab.getAttribute('id'); + + var targetTabId = targetTab.getAttribute('id'); + + // if the current tab is different from the target tab containing the sorting option switch tabs + if(currentTabId != targetTabId){ + document.querySelector('#tabletabbies .c-tabs-nav-link a[href=\'#' + targetTabId + '\']').click() + } + } + } + + // sorts the passed table row by the given rule -> either ascending (asc) or descending (desc). Invalid options or elements trigger an alert and are ignored + function sort(elem, rule) { + if(elem != null){ + if(rule == 'asc' || rule == 'desc'){ + // if users are on the request page we need to check in which tab the target sorting option is located + var targetTab = elem.closest('.c-tab[id]'); + switchToTargetTabIfRequired(targetTab); + + var classList = elem.classList; + // only sort if the sorting rule is not already applied on the element + if(classList.contains("table-sorted-" + rule) == false){ + while(elem.classList.contains("table-sorted-" + rule) == false){ + // Click sorting + elem.click(); + } + } + } + else{ + alert('Sorting only suppports \'asc\' or \'desc\' as parameter') + } + } + else{ + alert('Target element for sorting does not exist'); + } + } + + // method that gets triggered when the user enters some input to apply a filter + function updateHashAfterFilter(filterEvent){ + var filter = filterEvent.target.value; + var encodedFilter = encodeURIComponent(filterEvent.target.value); + // console.log('filter change:' + filter + ' to ' + encodedFilter); + + var newHashObj = splitHash(window.location.hash); + newHashObj.filter = 'filter=' + encodedFilter; + + updateHash(newHashObj); + } + + // $(window).on( "load", function(){ + function registerSortAndFilterListeners(){ + // Prepare Hash Monitoring + + // once everything is loaded check whether there is a sorting rule passed + var hashObj = splitHash(window.location.hash); + if(hashObj.sort != undefined){ + // Perform initial sorting + var sortParam = hashObj.sort.split('='); + var sortingElem = document.getElementById(sortParam[0]); + var sortingRule = sortParam[1]; + sort(sortingElem, sortingRule); + } + + // check for existing filter to apply + if(hashObj.filter != undefined){ + // Apply initial filter + var filterParam = hashObj.filter.split('='); + var encodedFilter = filterParam[1]; + + if(encodedFilter.length > 0){ + var decodedFilter = decodeURIComponent(encodedFilter); + // console.log('filter value: ' + decodedFilter); + var filterInputFields = document.querySelectorAll('input.filter'); + for(var i = 0; i < filterInputFields.length; i++){ + var filterField = filterInputFields[i]; + filterField.value = decodedFilter; + filter(filterField); + } + } + } + + // Register sorting listeners + var sortableTableRows = document.getElementsByClassName('table-sortable'); + for(var i = 0; i < sortableTableRows.length; i++){ + sortableTableRows[i].addEventListener('click', updateHashAfterSort); + } + + // Register filter listeners + var filterInputFields = document.querySelectorAll('input.filter'); + for(var i = 0; i < filterInputFields.length; i++){ + // filterInputFields[i].addEventListener('input', updateHashAfterFilter); + filterInputFields[i].addEventListener('focusout', updateHashAfterFilter); + } + + // Listeners applied, hook on the hashchange event + window.addEventListener('hashchange', hashChanged); + //}); + } + })(jQuery) /* diff --git a/config/xsl/diffreport/sections/custom-values.xsl b/config/xsl/diffreport/sections/custom-values.xsl index 3803b63d8..925e7fe97 100644 --- a/config/xsl/diffreport/sections/custom-values.xsl +++ b/config/xsl/diffreport/sections/custom-values.xsl @@ -15,6 +15,7 @@ + sortByName Value Name
    @@ -27,17 +28,20 @@ - Total - 1/s - 1/h* - 1/d* - Mean - Min. - Max. - Dev. + Total + 1/s + 1/h* + 1/d* + Mean + Min. + Max. + Dev. - + + + + diff --git a/config/xsl/diffreport/sections/web-vitals.xsl b/config/xsl/diffreport/sections/web-vitals.xsl index 495d568ba..f0b5d4bd5 100644 --- a/config/xsl/diffreport/sections/web-vitals.xsl +++ b/config/xsl/diffreport/sections/web-vitals.xsl @@ -15,17 +15,18 @@ + sortByName Action Name
    - First Contentful Paint
    (FCP) - Largest Contentful Paint
    (LCP) - Cumulative Layout Shift
    (CLS) - First Input Delay
    (FID) - Interaction to Next Paint
    (INP) - Time to First Byte
    (TTFB) + First Contentful Paint
    (FCP) + Largest Contentful Paint
    (LCP) + Cumulative Layout Shift
    (CLS) + First Input Delay
    (FID) + Interaction to Next Paint
    (INP) + Time to First Byte
    (TTFB) diff --git a/config/xsl/diffreport/util/timer-section.xsl b/config/xsl/diffreport/util/timer-section.xsl index 66b211f5d..406497341 100644 --- a/config/xsl/diffreport/util/timer-section.xsl +++ b/config/xsl/diffreport/util/timer-section.xsl @@ -45,23 +45,23 @@ Bytes Received - Total - 1/s - 1/min - 1/h* - 1/d* - Mean - Min. - Max. + Total + 1/s + 1/min + 1/h* + 1/d* + Mean + Min. + Max. - Total - 1/s - 1/min - 1/h* - 1/d* - Mean - Min. - Max. + Total + 1/s + 1/min + 1/h* + 1/d* + Mean + Min. + Max. diff --git a/config/xsl/diffreport/util/timer-table.xsl b/config/xsl/diffreport/util/timer-table.xsl index 21d6f7d0b..b6ed2993b 100644 --- a/config/xsl/diffreport/util/timer-table.xsl +++ b/config/xsl/diffreport/util/timer-table.xsl @@ -12,6 +12,7 @@ + sortByName
    @@ -36,23 +37,27 @@ - Distinct** + Distinct** - Total - 1/s - 1/min - 1/h* - Total + Total + 1/s + 1/min + 1/h* + Total - Total + Total - Mean - Min. - Max. - Dev. + Mean + Min. + Max. + Dev. - + + + + + diff --git a/config/xsl/loadreport/sections/agents.xsl b/config/xsl/loadreport/sections/agents.xsl index 04d2c2dde..f1ed59990 100644 --- a/config/xsl/loadreport/sections/agents.xsl +++ b/config/xsl/loadreport/sections/agents.xsl @@ -21,7 +21,7 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/config/xsl/loadreport/sections/content-types.xsl b/config/xsl/loadreport/sections/content-types.xsl index 400567374..9323272ff 100644 --- a/config/xsl/loadreport/sections/content-types.xsl +++ b/config/xsl/loadreport/sections/content-types.xsl @@ -15,9 +15,9 @@
    + Agent Name
    @@ -34,19 +34,19 @@
    Full GC
    TotalErrors%MeanMax.MeanMax.CountTime [ms]CPU [%]CountTime [ms]CPU [%]TotalErrors%MeanMax.MeanMax.CountTime [ms]CPU [%]CountTime [ms]CPU [%]
    - - - + + + diff --git a/config/xsl/loadreport/sections/custom-values.xsl b/config/xsl/loadreport/sections/custom-values.xsl index 4a8e1bfac..e0fac30c1 100644 --- a/config/xsl/loadreport/sections/custom-values.xsl +++ b/config/xsl/loadreport/sections/custom-values.xsl @@ -14,7 +14,7 @@
    Content TypeCountPercentageContent TypeCountPercentage
    - - - - - - - - - + + + + + + + + diff --git a/config/xsl/loadreport/sections/errors.xsl b/config/xsl/loadreport/sections/errors.xsl index b131d5b8b..d99c1fa05 100644 --- a/config/xsl/loadreport/sections/errors.xsl +++ b/config/xsl/loadreport/sections/errors.xsl @@ -74,9 +74,9 @@ class="table-autosort:1 table-autosort-order:desc"> - - - + + + @@ -266,11 +266,11 @@ class="table-autosort:0 table-autosort-order:desc error-table"> - - - + + + - + diff --git a/config/xsl/loadreport/sections/events.xsl b/config/xsl/loadreport/sections/events.xsl index 0c1350fd9..907188faf 100644 --- a/config/xsl/loadreport/sections/events.xsl +++ b/config/xsl/loadreport/sections/events.xsl @@ -25,10 +25,10 @@
    + Value Name
    @@ -27,16 +27,19 @@
    Total1/s1/h*1/d*MeanMin.Max.Dev.Total1/s1/h*1/d*MeanMin.Max.Dev. + + + P
    Error MessageCountPercentageError MessageCountPercentage
    CountTest CaseActionCountTest CaseAction DirectoryError InformationError Information
    - - - - + + + + diff --git a/config/xsl/loadreport/sections/hosts.xsl b/config/xsl/loadreport/sections/hosts.xsl index 3845fd44f..47e1c8687 100644 --- a/config/xsl/loadreport/sections/hosts.xsl +++ b/config/xsl/loadreport/sections/hosts.xsl @@ -15,9 +15,9 @@
    EventCountDroppedPercentageEventCountDroppedPercentage
    - - - + + + diff --git a/config/xsl/loadreport/sections/response-codes.xsl b/config/xsl/loadreport/sections/response-codes.xsl index 57c95f2af..250307581 100644 --- a/config/xsl/loadreport/sections/response-codes.xsl +++ b/config/xsl/loadreport/sections/response-codes.xsl @@ -15,9 +15,9 @@
    HostCountPercentageHostCountPercentage
    - - - + + + diff --git a/config/xsl/loadreport/sections/web-vitals.xsl b/config/xsl/loadreport/sections/web-vitals.xsl index a0b3f05ca..b04fcfc9b 100644 --- a/config/xsl/loadreport/sections/web-vitals.xsl +++ b/config/xsl/loadreport/sections/web-vitals.xsl @@ -13,18 +13,18 @@
    Response CodeCountPercentageResponse CodeCountPercentage
    - - - - - - - + + + + + + diff --git a/config/xsl/loadreport/util/timer-section.xsl b/config/xsl/loadreport/util/timer-section.xsl index e52345349..650b81ac3 100644 --- a/config/xsl/loadreport/util/timer-section.xsl +++ b/config/xsl/loadreport/util/timer-section.xsl @@ -8,7 +8,7 @@ -
    +
    @@ -49,7 +49,7 @@
    + Action Name
    First Contentful Paint
    (FCP)
    Largest Contentful Paint
    (LCP)
    Cumulative Layout Shift
    (CLS)
    First Input Delay
    (FID)
    Interaction to Next Paint
    (INP)
    Time to First Byte
    (TTFB)
    First Contentful Paint
    (FCP)
    Largest Contentful Paint
    (LCP)
    Cumulative Layout Shift
    (CLS)
    First Input Delay
    (FID)
    Interaction to Next Paint
    (INP)
    Time to First Byte
    (TTFB)
    - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -258,13 +258,13 @@
    +
    @@ -59,23 +59,23 @@
    Bytes Received
    Total1/s1/min1/h*1/d*MeanMin.Max.Total1/s1/min1/h*1/d*MeanMin.Max.Total1/s1/min1/h*1/d*MeanMin.Max.Total1/s1/min1/h*1/d*MeanMin.Max.
    - - + @@ -273,33 +273,33 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + diff --git a/config/xsl/loadreport/util/timer-table.xsl b/config/xsl/loadreport/util/timer-table.xsl index 4dece33f0..950312d8c 100644 --- a/config/xsl/loadreport/util/timer-table.xsl +++ b/config/xsl/loadreport/util/timer-table.xsl @@ -27,7 +27,7 @@
    +
    DNS Time [ms]DNS Time [ms] Connect Time [ms] Send Time [ms] Server Busy Time [ms]Time to Last [ms]
    MeanMin.Max.MeanMin.Max.MeanMin.Max.MeanMin.Max.MeanMin.Max.MeanMin.Max.MeanMin.Max.MeanMin.Max.MeanMin.Max.MeanMin.Max.MeanMin.Max.MeanMin.Max.MeanMin.Max.MeanMin.Max.
    - - @@ -53,40 +52,43 @@ - + - + - - - - + + + + - - - + + + - - + + - + - - - - + + + + @@ -97,6 +99,9 @@ - - - - - - @@ -66,25 +61,19 @@ - - - - - - diff --git a/config/xsl/loadreport/util/timer-row.xsl b/config/xsl/loadreport/util/timer-row.xsl index d939298a3..4fb6f3fc5 100644 --- a/config/xsl/loadreport/util/timer-row.xsl +++ b/config/xsl/loadreport/util/timer-row.xsl @@ -77,19 +77,11 @@ - - - - - - - - @@ -213,15 +205,11 @@ + - - - - - - - - @@ -113,13 +105,10 @@ + +
    +
    @@ -43,7 +43,6 @@
    Count Errors EventsRuntime Percentiles [ms] ApdexApdex Runtime Segmentation [ms]
    TotalTotal Distinct**1/s1/min1/h*Distinct**1/s1/min1/h* 1/s1/min1/h*1/s1/min1/h* Total%Total% TotalTotal MeanMin.Max.Dev.MeanMin.Max.Dev. + + + P + + + &le; @@ -143,7 +148,7 @@ - + diff --git a/config/xsl/trendreport/util/timer-table.xsl b/config/xsl/trendreport/util/timer-table.xsl index e6d46bc19..7857610a0 100644 --- a/config/xsl/trendreport/util/timer-table.xsl +++ b/config/xsl/trendreport/util/timer-table.xsl @@ -23,6 +23,7 @@
    + sortByName
    @@ -40,9 +41,11 @@ + sortByBaseline + sortByBaseline @@ -70,6 +73,7 @@
    #ReportInfo- + sortByRun table-sortable:numeric cluetip From b0ba8e5b81f9ab42a3bd519457ebfccdfcca814a Mon Sep 17 00:00:00 2001 From: Julia <51991513+js-xc@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:02:37 +0200 Subject: [PATCH 05/13] #439: Limit the number of error stack traces in memory during report generation --- config/reportgenerator.properties | 5 +++++ .../xceptance/xlt/common/XltPropertyNames.java | 2 ++ .../report/ReportGeneratorConfiguration.java | 18 ++++++++++++++++-- .../report/providers/ErrorsReportProvider.java | 16 +++++++++++++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/config/reportgenerator.properties b/config/reportgenerator.properties index 675cf556d..d642621db 100644 --- a/config/reportgenerator.properties +++ b/config/reportgenerator.properties @@ -65,6 +65,11 @@ com.xceptance.xlt.reportgenerator.requests.removeIndexes = true ## across both agents and load test duration in the report. #com.xceptance.xlt.reportgenerator.errors.directoryReplacementChance = 0.1 +## The maximum number of errors for which stack traces will be displayed (500 by +## default). Setting a lower limit can prevent the report generator from running +## out of memory if there are many different errors. +#com.xceptance.xlt.reportgenerator.errors.stackTracesLimit = 500 + ## Width and height of the generated charts (in pixels). com.xceptance.xlt.reportgenerator.charts.width = 900 com.xceptance.xlt.reportgenerator.charts.height = 300 diff --git a/src/main/java/com/xceptance/xlt/common/XltPropertyNames.java b/src/main/java/com/xceptance/xlt/common/XltPropertyNames.java index 65e841021..e4f79561f 100644 --- a/src/main/java/com/xceptance/xlt/common/XltPropertyNames.java +++ b/src/main/java/com/xceptance/xlt/common/XltPropertyNames.java @@ -36,6 +36,8 @@ public static class Errors public static final String DIRECTORY_LIMIT_PER_ERROR = BASE + "directoryLimitPerError"; public static final String DIRECTORY_REPLACEMENT_CHANCE = BASE + "directoryReplacementChance"; + + public static final String STACKTRACES_LIMIT = BASE + "stackTracesLimit"; } } } diff --git a/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java b/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java index 94db7eec7..9201f5994 100644 --- a/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java +++ b/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java @@ -280,6 +280,8 @@ public enum ChartCappingMode private final int directoryLimitPerError; private final double directoryReplacementChance; + + private final int stackTracesLimit; private final Map apdexThresholdsByActionNamePattern = new HashMap<>(); @@ -407,6 +409,8 @@ public ReportGeneratorConfiguration(Properties xltProperties, final File overrid directoryLimitPerError = getIntProperty(XltPropertyNames.ReportGenerator.Errors.DIRECTORY_LIMIT_PER_ERROR, 10); directoryReplacementChance = getDoubleProperty(XltPropertyNames.ReportGenerator.Errors.DIRECTORY_REPLACEMENT_CHANCE, 0.1); + stackTracesLimit = getIntProperty(XltPropertyNames.ReportGenerator.Errors.STACKTRACES_LIMIT, 500); + // event settings groupEventsByTestCase = getBooleanProperty(PROP_PREFIX + "events.groupByTestCase", true); eventLimit = getIntProperty(PROP_PREFIX + "events.eventLimit", 100); @@ -791,7 +795,7 @@ public int getErrorDetailsChartLimit() /** * The maximum number of directory hints remembered for a certain error (stack trace). * - * @return the maximum number of error traces to list + * @return the maximum number of directories to list */ public int getDirectoryLimitPerError() { @@ -801,12 +805,22 @@ public int getDirectoryLimitPerError() /** * The chance to replace directory hints remembered for a certain error (stack trace) when the maximum number is reached. * - * @return the chance to replace listed error traces + * @return the chance to replace listed directory hints */ public double getDirectoryReplacementChance() { return directoryReplacementChance; } + + /** + * The maximum number of errors that will be saved complete with their stack trace. + * + * @return the maximum number of error stack traces displayed in the report + */ + public int getStackTracesLimit() + { + return stackTracesLimit; + } /** * If true then the error details charts should be created. diff --git a/src/main/java/com/xceptance/xlt/report/providers/ErrorsReportProvider.java b/src/main/java/com/xceptance/xlt/report/providers/ErrorsReportProvider.java index 86107130d..2e508ff02 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/ErrorsReportProvider.java +++ b/src/main/java/com/xceptance/xlt/report/providers/ErrorsReportProvider.java @@ -71,6 +71,11 @@ public class ErrorsReportProvider extends AbstractReportProvider * which is given in percent. */ private double directoryReplacementChance; + + /** + * The maximum number of errors that will be saved complete with their stack trace. + */ + private int stackTracesLimit; /** * The root directory of the result set. @@ -137,6 +142,7 @@ public void setConfiguration(final ReportProviderConfiguration config) // cache some configuration values directoryLimitPerError = getConfiguration().getDirectoryLimitPerError(); directoryReplacementChance = getConfiguration().getDirectoryReplacementChance(); + stackTracesLimit = getConfiguration().getStackTracesLimit(); resultsDirectory = getConfiguration().getResultsDirectory(); } @@ -425,11 +431,19 @@ public void processDataRecord(final Data stat) if (errorValues == null) { final ErrorReport errorReport = new ErrorReport(); - errorReport.trace = trace; errorReport.message = txnStats.getFailureMessage(); errorReport.testCaseName = testCaseName; errorReport.actionName = failedActionName; errorReport.detailChartID = 0; + + if (errorReports.size() < stackTracesLimit) //only save stacktraces up to limit + { + errorReport.trace = trace; + } + else + { + errorReport.trace = "n/a"; + } errorValues = new ErrorValues(errorReport); errorReports.put(key, errorValues); From 045f9f2f037ba4888a545714600ae47c55f512e8 Mon Sep 17 00:00:00 2001 From: Julia <51991513+js-xc@users.noreply.github.com> Date: Wed, 24 Apr 2024 13:11:32 +0200 Subject: [PATCH 06/13] #474: Output error percentage values as part of the XML data output * error percentages in xml * put runtime segmentation percentages in xml * percentage for single agents in report * put error percentage for agent summary in xml * adjusted templates to changed xml structure --- config/xsl/loadreport/agents.xsl | 1 + config/xsl/loadreport/sections/agents.xsl | 31 +++++-------------- .../xsl/loadreport/util/summary-timer-row.xsl | 8 +---- config/xsl/loadreport/util/timer-row.xsl | 18 ++--------- .../xsl/loadreport/util/timer-summary-row.xsl | 19 +++--------- .../report/providers/AgentDataProcessor.java | 2 ++ .../xlt/report/providers/AgentReport.java | 2 ++ .../providers/BasicTimerDataProcessor.java | 3 +- .../providers/RequestDataProcessor.java | 9 ++++++ .../xlt/report/providers/RequestReport.java | 10 +++++- .../providers/SummaryReportProvider.java | 7 ++++- .../xlt/report/providers/TimerReport.java | 15 ++++++--- .../xlt/report/util/ReportUtils.java | 20 ++++++++++++ 13 files changed, 76 insertions(+), 69 deletions(-) diff --git a/config/xsl/loadreport/agents.xsl b/config/xsl/loadreport/agents.xsl index 557fe8ba6..0c5a371f1 100644 --- a/config/xsl/loadreport/agents.xsl +++ b/config/xsl/loadreport/agents.xsl @@ -61,6 +61,7 @@ --> + diff --git a/config/xsl/loadreport/sections/agents.xsl b/config/xsl/loadreport/sections/agents.xsl index f1ed59990..b5ac57f8e 100644 --- a/config/xsl/loadreport/sections/agents.xsl +++ b/config/xsl/loadreport/sections/agents.xsl @@ -3,6 +3,7 @@ +
    @@ -53,12 +54,6 @@
    - + - + value number error - + - + value number error - - - - - - - + % @@ -163,13 +152,7 @@ value number error - - - - - - - + % diff --git a/config/xsl/loadreport/util/summary-timer-row.xsl b/config/xsl/loadreport/util/summary-timer-row.xsl index 3fbd24996..2f6827240 100644 --- a/config/xsl/loadreport/util/summary-timer-row.xsl +++ b/config/xsl/loadreport/util/summary-timer-row.xsl @@ -46,17 +46,11 @@ value number error - + % value number colgroup1 error - + % - - - - - - diff --git a/config/xsl/loadreport/util/timer-summary-row.xsl b/config/xsl/loadreport/util/timer-summary-row.xsl index 3588b6c28..9abecf65a 100644 --- a/config/xsl/loadreport/util/timer-summary-row.xsl +++ b/config/xsl/loadreport/util/timer-summary-row.xsl @@ -45,19 +45,11 @@ value number colgroup1 error - + % - - - - - - diff --git a/src/main/java/com/xceptance/xlt/report/providers/AgentDataProcessor.java b/src/main/java/com/xceptance/xlt/report/providers/AgentDataProcessor.java index 988a0cbf3..445d04e27 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/AgentDataProcessor.java +++ b/src/main/java/com/xceptance/xlt/report/providers/AgentDataProcessor.java @@ -158,6 +158,8 @@ public AgentReport createAgentReport() agentReport.transactions = transactions; agentReport.transactionErrors = transactionErrors; + + agentReport.transactionErrorPercentage = ReportUtils.calculatePercentage(transactionErrors, transactions); return agentReport; } diff --git a/src/main/java/com/xceptance/xlt/report/providers/AgentReport.java b/src/main/java/com/xceptance/xlt/report/providers/AgentReport.java index 88b38d20b..c2dc71cc6 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/AgentReport.java +++ b/src/main/java/com/xceptance/xlt/report/providers/AgentReport.java @@ -45,4 +45,6 @@ public class AgentReport public int transactions; public int transactionErrors; + + public BigDecimal transactionErrorPercentage; } diff --git a/src/main/java/com/xceptance/xlt/report/providers/BasicTimerDataProcessor.java b/src/main/java/com/xceptance/xlt/report/providers/BasicTimerDataProcessor.java index 87514e8d1..2deff1787 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/BasicTimerDataProcessor.java +++ b/src/main/java/com/xceptance/xlt/report/providers/BasicTimerDataProcessor.java @@ -121,7 +121,8 @@ public TimerReport createTimerReport(final boolean generateHistogram) // set the counts final double count = runTimeStatistics.getCount(); final long duration = Math.max((getEndTime() - getStartTime()) / 1000, 1); - + + timerReport.errorPercentage = ReportUtils.calculatePercentage(totalErrors, (int) count); timerReport.count = (int) count; timerReport.countPerSecond = ReportUtils.convertToBigDecimal(count / duration); timerReport.countPerMinute = ReportUtils.convertToBigDecimal(count * 60 / duration); diff --git a/src/main/java/com/xceptance/xlt/report/providers/RequestDataProcessor.java b/src/main/java/com/xceptance/xlt/report/providers/RequestDataProcessor.java index 7205ba751..4d90a72bd 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/RequestDataProcessor.java +++ b/src/main/java/com/xceptance/xlt/report/providers/RequestDataProcessor.java @@ -17,6 +17,7 @@ import java.awt.Color; import java.io.File; +import java.math.BigDecimal; import java.math.BigInteger; import java.util.stream.Collectors; @@ -264,6 +265,14 @@ public void run() // just int is safe, more than 2 billion urls is unlikely timerReport.urls = getUrlList(distinctUrlSet, (int) distinctUrlsHLL.cardinality()); timerReport.countPerInterval = countPerSegment != null ? countPerSegment.getCountPerSegment() : ArrayUtils.EMPTY_INT_ARRAY; + timerReport.percentagePerInterval = countPerSegment != null ? new BigDecimal[countPerSegment.getCountPerSegment().length] : new BigDecimal[]{}; + if (countPerSegment != null) + { + for (int n = 0; n < countPerSegment.getCountPerSegment().length; n++) + { + timerReport.percentagePerInterval[n] = ReportUtils.calculatePercentage(countPerSegment.getCountPerSegment()[n], timerReport.count); + } + } final long duration = Math.max((getConfiguration().getChartEndTime() - getConfiguration().getChartStartTime()) / 1000, 1); diff --git a/src/main/java/com/xceptance/xlt/report/providers/RequestReport.java b/src/main/java/com/xceptance/xlt/report/providers/RequestReport.java index d71525509..ed382a089 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/RequestReport.java +++ b/src/main/java/com/xceptance/xlt/report/providers/RequestReport.java @@ -15,6 +15,8 @@ */ package com.xceptance.xlt.report.providers; +import java.math.BigDecimal; + import com.thoughtworks.xstream.annotations.XStreamAlias; /** @@ -72,9 +74,15 @@ public class RequestReport extends TimerReport * The number of timer values per configured runtime interval. */ public int[] countPerInterval; - + + /** + * The percentage of timer values per configured runtime interval. + */ + public BigDecimal[] percentagePerInterval; /** * A list of the top URLs. */ public UrlData urls; + + } diff --git a/src/main/java/com/xceptance/xlt/report/providers/SummaryReportProvider.java b/src/main/java/com/xceptance/xlt/report/providers/SummaryReportProvider.java index e640a0703..a6818c2de 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/SummaryReportProvider.java +++ b/src/main/java/com/xceptance/xlt/report/providers/SummaryReportProvider.java @@ -91,7 +91,12 @@ else if (data instanceof ActionData) { actionDataProcessor.processDataRecord(data); } - else if (data instanceof TransactionData || data instanceof EventData) + else if (data instanceof TransactionData) + { + transactionDataProcessor.processDataRecord(data); + agentDataProcessor.incrementTransactionCounters(((TransactionData) data).hasFailed()); + } + else if (data instanceof EventData) { transactionDataProcessor.processDataRecord(data); } diff --git a/src/main/java/com/xceptance/xlt/report/providers/TimerReport.java b/src/main/java/com/xceptance/xlt/report/providers/TimerReport.java index cea39ef5c..b084cffee 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/TimerReport.java +++ b/src/main/java/com/xceptance/xlt/report/providers/TimerReport.java @@ -40,29 +40,34 @@ public class TimerReport public int count; /** - * The number how often the timer has fired. + * The calculated number how often the timer has fired per second. */ public BigDecimal countPerSecond; /** - * The number how often the timer has fired. + * The calculated number how often the timer has fired per minute. */ public BigDecimal countPerMinute; /** - * The number how often the timer has fired. + * The calculated number how often the timer has fired per hour. */ public BigDecimal countPerHour; /** - * The number how often the timer has fired. + * The calculated number how often the timer has fired per day. */ public BigDecimal countPerDay; /** - * The number how often the request has failed. + * The number how often the timed item has failed. */ public int errors; + + /** + * The calculated number which percentage of the total timed items (see count) has failed. + */ + public BigDecimal errorPercentage; /** * The minimum timer runtime. diff --git a/src/main/java/com/xceptance/xlt/report/util/ReportUtils.java b/src/main/java/com/xceptance/xlt/report/util/ReportUtils.java index 2b3b72a4a..d6cc6e46b 100644 --- a/src/main/java/com/xceptance/xlt/report/util/ReportUtils.java +++ b/src/main/java/com/xceptance/xlt/report/util/ReportUtils.java @@ -338,4 +338,24 @@ public static String obtainProjectName(final Iterable documents) } return StringUtils.defaultString(projectName); } + + /** + * Calculates the percentage given by count/total, as a {@link BigDecimal} (rounding it to have at most + * {@value #DEFAULT_DECIMAL_PLACES} decimal places). Returns 0 if total is 0. + * + * @param count + * @param total + * @return + */ + public static BigDecimal calculatePercentage(int count, int total) + { + if (total == 0) + { + return new BigDecimal(0); + } + else + { + return convertToBigDecimal((double) count * 100 / total); + } + } } From 88018c857110149d8bedca40494e413ab8bf4248 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Apr 2024 13:23:57 +0200 Subject: [PATCH 07/13] Bump org.apache.commons:commons-configuration2 from 2.8.0 to 2.10.1 (#485) Bumps org.apache.commons:commons-configuration2 from 2.8.0 to 2.10.1. --- updated-dependencies: - dependency-name: org.apache.commons:commons-configuration2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b2f1e171b..0480c20c2 100644 --- a/pom.xml +++ b/pom.xml @@ -259,7 +259,7 @@ org.apache.commons commons-configuration2 - 2.8.0 + 2.10.1 org.apache.commons From fdca07697373530fca8ce977af11528262215a6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Apr 2024 13:31:24 +0200 Subject: [PATCH 08/13] Bump follow-redirects from 1.15.5 to 1.15.6 in /resultbrowser (#481) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- resultbrowser/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resultbrowser/package-lock.json b/resultbrowser/package-lock.json index 3e57f0a0a..4849c1ae3 100644 --- a/resultbrowser/package-lock.json +++ b/resultbrowser/package-lock.json @@ -2152,9 +2152,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { From 079160b362114d3e07fa977cdb729a9609675579 Mon Sep 17 00:00:00 2001 From: Rene Schwietzke Date: Thu, 25 Apr 2024 16:46:37 +0200 Subject: [PATCH 09/13] Update README.md Added XTC information --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b8813605f..f8642f1da 100644 --- a/README.md +++ b/README.md @@ -62,5 +62,10 @@ When you build XLT by yourself, the following limitations apply. * Depending on the distribution you use, you might need to adjust the path to your Chrome/Chromium executable in `build.properties` on Linux as well. +# XLT as a Service - XTC +If you are looking for a hosted version of XLT with all the bells and whistles, look no further. Xceptance offers XTC - The Xceptance Test Center, a fully hosted, multi-project, infinitely scalable version of XLT in the cloud. [Contact](https://www.xceptance.com/en/contact/) us for more details. + +![image](https://github.com/Xceptance/XLT/assets/1793856/7a8efb7b-caef-4aaf-9321-cdcaba673d8a) + # Jobs -Do you like the code and architecture of XLT? Do you have ideas for new features or just like to work with it? Why not considering to apply for a job at Xceptance? We are always hiring developers and testers. Just drop us a line. You can find more information on our career page [Jobs at Xceptance](https://www.xceptance.com/en/careers/). +Do you like XLT's code and architecture? Do you have ideas for new features or just enjoy working with it? Consider applying for a job at Xceptance. We are always hiring developers and testers. Just drop us a line. You can find more information on our career page [Jobs at Xceptance] (https://www.xceptance.com/en/careers/). From f67b8332b2422f5765bcb4675f37acdb08843706 Mon Sep 17 00:00:00 2001 From: Rene Schwietzke Date: Thu, 25 Apr 2024 16:47:59 +0200 Subject: [PATCH 10/13] Update README.md Docs link updated, forum removed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f8642f1da..7fd26d808 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # XLT -XLT is an extensive load and performance test tool developed and maintained by Xceptance. If you need more information, here is the current [website](https://www.xceptance.com/xlt/) and the current [documentation portal](https://xltdoc.xceptance.com/). There is also a [forum](https://ask.xceptance.de/) available to discuss load testing and test automation with XLT. +XLT is an extensive load and performance test tool developed and maintained by Xceptance. If you need more information, here is the current [website](https://www.xceptance.com/xlt/) and the current [documentation portal](https://docs.xceptance.com/). The documentation was fully rewritten in 2020 and it is continuously updated. We appreciate your feedback. You can also directly contribute on GitHub at https://github.com/Xceptance/xlt-documentation. If you need any information from the legacy documentation, you can still find it at https://lab.xceptance.de/releases/xlt/5.7.1/index.html From f4516769fe8df06c84e155818a531bbd6f05572c Mon Sep 17 00:00:00 2001 From: Julia <51991513+js-xc@users.noreply.github.com> Date: Tue, 28 May 2024 14:13:45 +0200 Subject: [PATCH 11/13] #465: Responsive Result Browser * bigger font in menu * column layout in mobile/portrait mode * filter menu made usable on mobile (full screen overlay) * navigation can be expanded/hidden on mobile in portrait mode * realbrowser action screenshots are sized to 100% screen width per default, zoomable --- resultbrowser/src/css/default.css | 252 +++++++++++++++++++++++++- resultbrowser/src/index.html | 20 +- resultbrowser/src/js/resultbrowser.js | 133 +++++++++++++- 3 files changed, 385 insertions(+), 20 deletions(-) diff --git a/resultbrowser/src/css/default.css b/resultbrowser/src/css/default.css index f13dcb257..a4a88ac35 100644 --- a/resultbrowser/src/css/default.css +++ b/resultbrowser/src/css/default.css @@ -23,7 +23,7 @@ body { * The header */ #header { - background-color: #A50000; + background-color: #A50000; } #header .logo { @@ -40,6 +40,7 @@ body { #header .logo h1 { font-size: 16px; color: white; + align-content: center; } /* @@ -64,8 +65,12 @@ body { #menu-icon { color: white; - margin-left: 8px; + margin: 5px; + margin-left: auto; + font-size: 20px; + font-weight: bold; cursor: pointer; + transform: rotate(90deg); } #menu { @@ -211,7 +216,7 @@ body { cursor: pointer; } -#navigation .expander::after { +#navigation .expander::before { font-style: normal; content: "◢"; margin-right: 0.5em; @@ -220,7 +225,7 @@ body { transform: translateY(-10%) rotate(-45deg); } -#navigation .expander.expanded::after { +#navigation .expander.expanded::before { transform: translateY(-10%) rotate(0deg); } @@ -627,3 +632,242 @@ table.key-value-table td.value .csep { transform: rotate(360deg); } } + +.actionimgScale { + position: absolute; + z-index: 10; + width: 50px; + height: 50px; + background: rgba(255, 255, 255, 0.5); + align-content: center; + text-align: center; + border-radius: 5px; + border: 1px solid #bbb; + cursor: pointer; +} + +.actionimgScale#actionimgSwitch{ + right: 20px; + bottom: 20px; +} + +.actionimgScale#actionimgSwitch.full { + font-size: 30px; +} + +.actionimgScale#actionimgSwitch.fit { + font-size: 20px; +} + +/******************************************************************************************** + * + * Responsive + * + ********************************************************************************************/ + +#wrapper, #mainContent { + flex-flow: row; + width: 100vw !important; +} + +#header .expander { + display: none; +} + +#menu-close { + display: none; +} + +/*responsive behavior for phones in portrait mode*/ +@media (max-width: 599px) { + + #header { + position: fixed; + top: 0; + width: 100%; + z-index: 2; + } + + #header .logo h1 { + font-size: 22px; + } + + #wrapper, #mainContent { + flex-flow:column; + } + + #header .expander { + cursor: pointer; + } + + #header .expander { + font-weight: bold; + font-size: 22px; + color: #fff; + display: inline-block; + text-align: center; + align-content: center; + width: 35px; + transform: rotate(0deg); + transition: transform .2s; + + } + + #header .expander.expanded { + transform: rotate(90deg); + } + + #menu-icon { + font-size: 24px; + } + + #leftSideMenu { + height: 90px; + overflow: hidden; + transition: height .2s; + } + + #leftSideMenu.expanded { + height: 80vh; + overflow: scroll; + } + + #navigation { + padding-top: 50px; + } + + #content { + height: 20vh; + transition: height .2s; + border-top: 1px solid #bbb; + } + + #content.expanded { + height: 100vh; + } + + #transaction { + overflow: visible; + font-size: 19px; + } + + #actionlist { + flex-basis: auto !important; + margin: 0px; + } + + #menu > ul { + flex-flow: column; + } +} + +/*responsive behavior for phones in portrait and landscape mode*/ +@media (max-width: 599px), (max-height: 400px) { + + #header .logo h1 { + align-content: center; + margin: 0px; + } + + #navigation { + font-size: 16px; + } + + #navigation li.action { + padding-top: 6px; + padding-bottom: 6px; + } + + #navigation ul li ul li { + font-size: 16px; + padding: 4px; + } + + #navigation .expander { + margin: 0px 5px; + } + + #responseContentActions { + position: fixed; + bottom: 10px; + right: 10px; + top: auto; + } + + #beautify, #highlightSyntax, #selectResponseContent { + background-color: #666; + border: none; + border-radius: 13px; + color: #fff; + padding: 8px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + cursor: pointer; + } + + #transactionContent { + border-top: 2px solid #666; + } + + #transactionContent h1 { + margin: 8px 2px; + font-size: 12pt; + } + + .actionimgScale#actionimgSwitch { + right: 10px; + bottom: 10px; + } + + .tabs .tabs-nav { + padding: 4px; + } + + .tabs .tabs-nav li { + border-radius: 4px; + background-color: #db0009; + border: 0px; + margin: 2px; + } + + .tabs .tabs-nav li.selected { + border: 0px; + } + + #menu { + width: 100%; + height: 100%; + overflow: hidden; + background-color: #fffd; + border: 0px; + } + + #menu > ul { + display: flex; + align-items: stretch; + } + + #menu li:nth-of-type(2n) .menu-group { + border-left: 0px; + border-right: 0px; + } + + #menu > ul > li { + flex-grow: 1; + } + + .menu-group { + width: -webkit-fill-available; + } + + .open #menu-close { + display: block; + position: absolute; + top: 5px; + right: 10px; + font-size: 20px; + } +} + \ No newline at end of file diff --git a/resultbrowser/src/index.html b/resultbrowser/src/index.html index 90e89bb5a..5bfd54388 100644 --- a/resultbrowser/src/index.html +++ b/resultbrowser/src/index.html @@ -1,8 +1,9 @@ - + + XLT Result Browser @@ -24,16 +25,17 @@
    -
    +
    -
    +
    + +
    diff --git a/resultbrowser/src/js/resultbrowser.js b/resultbrowser/src/js/resultbrowser.js index 58cfa60b5..1b2f571e5 100644 --- a/resultbrowser/src/js/resultbrowser.js +++ b/resultbrowser/src/js/resultbrowser.js @@ -5,6 +5,8 @@ requestContent = null, requestText = null, actionContent = null, + actionContentImage = null, + actionImage = null, errorContent = null, postRequestParam = null, requestBodySmall = null, @@ -19,8 +21,10 @@ }, menu = null, menuIcon = null, + menuClose = null, transactionContent = null, - valueLog = null; + valueLog = null, + zoomLevel = 100; // function aliases const getElementById = id => document.getElementById(id), @@ -127,6 +131,8 @@ requestContent = getElementById("requestcontent"); requestText = getElementById("requesttext"); actionContent = getElementById("actioncontent"); + actionContentImage = getElementById("actioncontent_image"); + actionImage = getElementById("actionimage"); errorContent = getElementById("errorcontent"); postRequestParam = getElementById("postrequestparameters"); requestBodySmall = getElementById("requestBodySmall"); @@ -139,6 +145,7 @@ menu = getElementById("menu"); menuIcon = getElementById("menu-icon"); + menuClose = getElementById("menu-close"); extras.highlight = !!window.hljs; extras.beautify.js = !!window.js_beautify; @@ -211,6 +218,11 @@ } setText(requestText, s); }); + + const actionimgSwitch = getElementById("actionimgSwitch"); + actionimgSwitch.addEventListener("click", function () { + zoomImage(actionimgSwitch); + }); } const selectResponseContent = getElementById("selectResponseContent"); @@ -224,6 +236,7 @@ // menu button menuIcon.addEventListener("click", showMenu); + menuClose.addEventListener("click", showMenu); document.addEventListener("click", function (e) { const x = e.target; if (filterElements(getParents(x), parent => parent.id == "menu").length === 0 && x.id != "menu-icon") { @@ -254,6 +267,28 @@ // transaction page transaction.addEventListener("click", showTransaction); } + + function zoomImage(element) { + + if (element.id == "actionimgSwitch") + { + if (element.classList.contains('fit')) + { + element.classList.remove('fit'); + element.classList.add('full'); + element.title = "Click to enlarge to 100%"; + actionImage.style = "width: 100%;" + } + else if (element.classList.contains('full')) + { + element.classList.remove('full'); + element.classList.add('fit'); + element.title = "Click to fit image to screen width"; + actionImage.style = "size: 100%;" + zoomLevel = 100; + } + } + } function toggleContent(element) { // show the given content pane and hide the others @@ -294,8 +329,18 @@ const data = dataStore.fetchData(element), actionFile = data.fileName; if (actionFile) { - actionContent.setAttribute("src", actionFile); - toggleContent(actionContent); + if (actionFile.indexOf(".png") > -1) + { + // put page screenshots in div + actionImage.setAttribute("src", actionFile); + toggleContent(actionContentImage); + } + else + { + // put html pages in iframe + actionContent.setAttribute("src", actionFile); + toggleContent(actionContent); + } } else { toggleContent(errorContent); @@ -307,6 +352,14 @@ element.classList.add("current"); } } + + function showHideMenu() { + // show/hide the menu + document.getElementById("leftSideMenu").classList.toggle("expanded"); + document.getElementById("content").classList.toggle("expanded"); + // switch the menu toggle + document.getElementById("mExpander").classList.toggle("expanded"); + } function expandCollapseAction(element) { // lazily create the requests @@ -894,10 +947,23 @@ hide(menu); } else { + menu.style.position = "absolute"; - menu.style.top = `${menuIcon.offsetTop}px`; - menu.style.left = `${menuIcon.offsetLeft + 17}px`; menu.style.zIndex = "200001"; + var mm = window.matchMedia("(max-width: 599px), (max-height: 400px) ") + if (mm.matches) + { + //mobile: use full screen for menu overlay + menu.style.top = `0px`; + menu.style.left = `0px`; + } + else + { + //desktop: pop up menu next to icon + menu.style.top = `${menuIcon.offsetTop}px`; + menu.style.left = `${menuIcon.offsetLeft + 17}px`; + } + show(menu); } @@ -1002,11 +1068,58 @@ activateTab(this); })); - Split(['#leftSideMenu', '#content'], { - sizes: [15, 85], - minSize: [300, 600], - gutterSize: 3 - }) + function splitContent(mm) { + if (mm.matches) { // If media query matches / is large screen + Split(['#leftSideMenu', '#content'], { + sizes: [15, 85], + minSize: [300, 600], + gutterSize: 3 + }); + } + else { //on small screens/mobile: revert changes made by Split + document.getElementById("leftSideMenu").removeAttribute("style"); + document.getElementById("content").removeAttribute("style"); + var gutter = document.getElementsByClassName('gutter-horizontal'); + while(gutter[0]) { + gutter[0].parentNode.removeChild(gutter[0]); + } + + if (document.getElementById("mExpander") === null) + { + const mExpander = document.createElement("span"); + mExpander.classList.add("expander"); + mExpander.classList.add("expanded"); + mExpander.id = "mExpander"; + mExpander.title = "Single-click to show/hide menu."; + mExpander.textContent = "☰"; + document.getElementById("header").insertBefore(mExpander, document.getElementById("header").firstChild); + + // setup click to show/hide requests + mExpander.addEventListener( + "click", + function () { + showHideMenu(); + } + ); + + // content click also shows/hides menu + /*content.addEventListener( + "click", + function () { + showHideMenu(); + } + );*/ + } + } + } + // Create a MediaQueryList object + var mm = window.matchMedia("(min-width: 600px)") + // Call listener function at run time + splitContent(mm); + // Attach listener function on state changes + mm.addEventListener("change", function() { + splitContent(mm); + }); // activate first request-tab activateTab(requestContent.querySelector(".tabs-nav li")); From 8c2b3ad0a1cb3b9b65c9e99b422ccaf436614a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Werner?= <4639399+jowerner@users.noreply.github.com> Date: Fri, 31 May 2024 17:38:42 +0200 Subject: [PATCH 12/13] #497: Make mTLS more convenient to use (#498) * initial * - make key/trust store configurable - load key/trust stores on first use only and cache them for subsequent uses * marked our changes in WebClientOptions.java * added needed Jetty server properties for testing mTLS (inactive by default) --- samples/app-server/start.ini | 19 +- .../config/default.properties | 17 ++ .../config/default.properties | 17 ++ .../config/default.properties | 17 ++ .../config/default.properties | 17 ++ .../testsuite-xlt/config/default.properties | 17 ++ .../xceptance/xlt/engine/XltWebClient.java | 202 ++++++++++++++---- .../java/org/htmlunit/WebClientOptions.java | 5 + 8 files changed, 261 insertions(+), 50 deletions(-) diff --git a/samples/app-server/start.ini b/samples/app-server/start.ini index eb27b65aa..598f2643d 100644 --- a/samples/app-server/start.ini +++ b/samples/app-server/start.ini @@ -33,10 +33,6 @@ etc/xc-loginservice.xml # Property Overrides #=========================================================== -# use XC keystore/truststore -jetty.sslContext.keyStorePath=etc/xc-keystore.jks -jetty.sslContext.trustStorePath=etc/xc-keystore.jks - ## the address to which the server will be bound #jetty.http.host=0.0.0.0 #jetty.ssl.host=${jetty.http.host} @@ -45,3 +41,18 @@ jetty.sslContext.trustStorePath=etc/xc-keystore.jks jetty.http.port=8080 jetty.ssl.port=8443 jetty.httpConfig.securePort=${jetty.ssl.port} + +## use default XC keystore/truststore +jetty.sslContext.keyStorePath=etc/xc-keystore.jks +jetty.sslContext.trustStorePath=etc/xc-keystore.jks + +## alternative keystore/truststore for testing mTLS +#jetty.sslContext.keyStorePath=_mtls/server-keystore.jks +#jetty.sslContext.keyStorePassword= +#jetty.sslContext.keyManagerPassword= + +#jetty.sslContext.trustStorePath=_mtls/server-truststore.jks +#jetty.sslContext.trustStorePassword= + +#jetty.sslContext.needClientAuth=true +#jetty.sslContext.wantClientAuth=true diff --git a/samples/testsuite-performance/config/default.properties b/samples/testsuite-performance/config/default.properties index 7e1c79c58..31ef83fe4 100644 --- a/samples/testsuite-performance/config/default.properties +++ b/samples/testsuite-performance/config/default.properties @@ -37,6 +37,23 @@ com.xceptance.xlt.ssl.easyMode = true ## need to be enabled there again before they can be used here. #com.xceptance.xlt.ssl.protocols = SSLv3, TLSv1, TLSv1.1, TLSv1.2 +## The path to a Java key store file with a client key/certificate. +## Specify the path relative to the root directory of the test suite. +## If the store is protected by a password, you can configure it here +## as well. Please ensure that the key in the store is always protected by the +## same password as the store itself. +#com.xceptance.xlt.tls.keyStore.file = config/keystore.p12 +#com.xceptance.xlt.tls.keyStore.password = + +## The path to a Java key store file with trusted server certificates. +## Specify the path relative to the root directory of the test suite. +## If the store is protected by a password, you can configure it here +## as well. +## If you don't configure any trust store here, the system-default trust store +## will be used. +#com.xceptance.xlt.tls.trustStore.file = config/truststore.p12 +#com.xceptance.xlt.tls.trustStore.password = + ## The connection timeout [ms] on sockets and establishing the connection. This ## value is used therefore twice. So a value of 30000 will set a timeout of ## 30 seconds to establish the connection and 30 seconds on waiting for data. diff --git a/samples/testsuite-posters/config/default.properties b/samples/testsuite-posters/config/default.properties index 0a7e7c6c9..ab7173015 100644 --- a/samples/testsuite-posters/config/default.properties +++ b/samples/testsuite-posters/config/default.properties @@ -37,6 +37,23 @@ com.xceptance.xlt.ssl.easyMode = true ## need to be enabled there again before they can be used here. #com.xceptance.xlt.ssl.protocols = SSLv3, TLSv1, TLSv1.1, TLSv1.2 +## The path to a Java key store file with a client key/certificate. +## Specify the path relative to the root directory of the test suite. +## If the store is protected by a password, you can configure it here +## as well. Please ensure that the key in the store is always protected by the +## same password as the store itself. +#com.xceptance.xlt.tls.keyStore.file = config/keystore.p12 +#com.xceptance.xlt.tls.keyStore.password = + +## The path to a Java key store file with trusted server certificates. +## Specify the path relative to the root directory of the test suite. +## If the store is protected by a password, you can configure it here +## as well. +## If you don't configure any trust store here, the system-default trust store +## will be used. +#com.xceptance.xlt.tls.trustStore.file = config/truststore.p12 +#com.xceptance.xlt.tls.trustStore.password = + ## The connection timeout [ms] on sockets and establishing the connection. This ## value is used therefore twice. So a value of 30000 will set a timeout of ## 30 seconds to establish the connection and 30 seconds on waiting for data. diff --git a/samples/testsuite-showcases/config/default.properties b/samples/testsuite-showcases/config/default.properties index 0ae674170..34a629475 100644 --- a/samples/testsuite-showcases/config/default.properties +++ b/samples/testsuite-showcases/config/default.properties @@ -37,6 +37,23 @@ com.xceptance.xlt.ssl.easyMode = true ## need to be enabled there again before they can be used here. #com.xceptance.xlt.ssl.protocols = SSLv3, TLSv1, TLSv1.1, TLSv1.2 +## The path to a Java key store file with a client key/certificate. +## Specify the path relative to the root directory of the test suite. +## If the store is protected by a password, you can configure it here +## as well. Please ensure that the key in the store is always protected by the +## same password as the store itself. +#com.xceptance.xlt.tls.keyStore.file = config/keystore.p12 +#com.xceptance.xlt.tls.keyStore.password = + +## The path to a Java key store file with trusted server certificates. +## Specify the path relative to the root directory of the test suite. +## If the store is protected by a password, you can configure it here +## as well. +## If you don't configure any trust store here, the system-default trust store +## will be used. +#com.xceptance.xlt.tls.trustStore.file = config/truststore.p12 +#com.xceptance.xlt.tls.trustStore.password = + ## The connection timeout [ms] on sockets and establishing the connection. This ## value is used therefore twice. So a value of 30000 will set a timeout of ## 30 seconds to establish the connection and 30 seconds on waiting for data. diff --git a/samples/testsuite-template/config/default.properties b/samples/testsuite-template/config/default.properties index 7e1c79c58..31ef83fe4 100644 --- a/samples/testsuite-template/config/default.properties +++ b/samples/testsuite-template/config/default.properties @@ -37,6 +37,23 @@ com.xceptance.xlt.ssl.easyMode = true ## need to be enabled there again before they can be used here. #com.xceptance.xlt.ssl.protocols = SSLv3, TLSv1, TLSv1.1, TLSv1.2 +## The path to a Java key store file with a client key/certificate. +## Specify the path relative to the root directory of the test suite. +## If the store is protected by a password, you can configure it here +## as well. Please ensure that the key in the store is always protected by the +## same password as the store itself. +#com.xceptance.xlt.tls.keyStore.file = config/keystore.p12 +#com.xceptance.xlt.tls.keyStore.password = + +## The path to a Java key store file with trusted server certificates. +## Specify the path relative to the root directory of the test suite. +## If the store is protected by a password, you can configure it here +## as well. +## If you don't configure any trust store here, the system-default trust store +## will be used. +#com.xceptance.xlt.tls.trustStore.file = config/truststore.p12 +#com.xceptance.xlt.tls.trustStore.password = + ## The connection timeout [ms] on sockets and establishing the connection. This ## value is used therefore twice. So a value of 30000 will set a timeout of ## 30 seconds to establish the connection and 30 seconds on waiting for data. diff --git a/samples/testsuite-xlt/config/default.properties b/samples/testsuite-xlt/config/default.properties index 0ae674170..34a629475 100644 --- a/samples/testsuite-xlt/config/default.properties +++ b/samples/testsuite-xlt/config/default.properties @@ -37,6 +37,23 @@ com.xceptance.xlt.ssl.easyMode = true ## need to be enabled there again before they can be used here. #com.xceptance.xlt.ssl.protocols = SSLv3, TLSv1, TLSv1.1, TLSv1.2 +## The path to a Java key store file with a client key/certificate. +## Specify the path relative to the root directory of the test suite. +## If the store is protected by a password, you can configure it here +## as well. Please ensure that the key in the store is always protected by the +## same password as the store itself. +#com.xceptance.xlt.tls.keyStore.file = config/keystore.p12 +#com.xceptance.xlt.tls.keyStore.password = + +## The path to a Java key store file with trusted server certificates. +## Specify the path relative to the root directory of the test suite. +## If the store is protected by a password, you can configure it here +## as well. +## If you don't configure any trust store here, the system-default trust store +## will be used. +#com.xceptance.xlt.tls.trustStore.file = config/truststore.p12 +#com.xceptance.xlt.tls.trustStore.password = + ## The connection timeout [ms] on sockets and establishing the connection. This ## value is used therefore twice. So a value of 30000 will set a timeout of ## 30 seconds to establish the connection and 30 seconds on waiting for data. diff --git a/src/main/java/com/xceptance/xlt/engine/XltWebClient.java b/src/main/java/com/xceptance/xlt/engine/XltWebClient.java index 2e36c97cf..49c0f10be 100644 --- a/src/main/java/com/xceptance/xlt/engine/XltWebClient.java +++ b/src/main/java/com/xceptance/xlt/engine/XltWebClient.java @@ -15,10 +15,12 @@ */ package com.xceptance.xlt.engine; +import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; +import java.security.KeyStore; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -33,6 +35,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringEscapeUtils; import org.htmlunit.AjaxController; @@ -81,6 +84,7 @@ import com.xceptance.xlt.api.engine.SessionShutdownListener; import com.xceptance.xlt.api.htmlunit.LightWeightPage; import com.xceptance.xlt.api.util.ResponseProcessor; +import com.xceptance.xlt.api.util.XltException; import com.xceptance.xlt.api.util.XltLogger; import com.xceptance.xlt.api.util.XltProperties; import com.xceptance.xlt.engine.htmlunit.apache.XltApacheHttpWebConnection; @@ -127,10 +131,15 @@ public class XltWebClient extends WebClient implements SessionShutdownListener, */ private static final XltCache globalCache; + /** + * The global cache for key and trust stores. The cache is shared among all {@link XltWebClient} instances. + */ + private static final ConcurrentHashMap storeCache; + /** * Holds the URLs and responses of static resources that have been loaded so far for a page. */ - private final ConcurrentHashMap pageLocalCache = new ConcurrentHashMap(); + private final ConcurrentHashMap pageLocalCache = new ConcurrentHashMap<>(); /** * Enabled flag for static content loading. @@ -166,7 +175,7 @@ public class XltWebClient extends WebClient implements SessionShutdownListener, /** * The list of response processors. */ - private final List responseProcessors = new ArrayList(); + private final List responseProcessors = new ArrayList<>(); /** * JS Beautifying response processor (separate instance field since its (omni)presence is configuration-dependent). @@ -181,7 +190,7 @@ public class XltWebClient extends WebClient implements SessionShutdownListener, /** * Mapping of base URLs to relative CSS resource URLs. */ - private final ConcurrentHashMap> cssResourceUrlCache = new ConcurrentHashMap>(); + private final ConcurrentHashMap> cssResourceUrlCache = new ConcurrentHashMap<>(); /** * The maximum number of download threads. @@ -224,6 +233,7 @@ public class XltWebClient extends WebClient implements SessionShutdownListener, cssCacheSize = Math.max(cssCacheSize, ConcurrentLRUCache.MIN_SIZE); globalCache = new XltCache(jsCacheSize, cssCacheSize); + storeCache = new ConcurrentHashMap<>(); // configure the XPath engine to use final String xpathEngine = props.getProperty("com.xceptance.xlt.xpath.engine", "jaxen"); @@ -404,7 +414,7 @@ public XltWebClient(final BrowserVersion browserVersion, final boolean javaScrip getOptions().setUseInsecureSSL(props.getProperty("com.xceptance.xlt.ssl.easyMode", false)); // the SSL protocol (family) to use when in easy mode - String easyModeProtocol = StringUtils.defaultIfBlank(props.getProperty("com.xceptance.xlt.ssl.easyModeProtocol"), "TLS"); + final String easyModeProtocol = StringUtils.defaultIfBlank(props.getProperty("com.xceptance.xlt.ssl.easyModeProtocol"), "TLS"); getOptions().setSSLInsecureProtocol(easyModeProtocol.trim()); // the SSL handshake protocols to enable at an SSL socket @@ -464,6 +474,10 @@ public XltWebClient(final BrowserVersion browserVersion, final boolean javaScrip final XltHttpWebConnection connection = new XltHttpWebConnection(this, underlyingWebConnection); setWebConnection(connection); + + // load key/trust material for client/server authentication + setUpKeyStore(props); + setUpTrustStore(props); } /** @@ -683,7 +697,7 @@ public void loadNewStaticContent(final HtmlPage htmlPage) final RequestStack requestStack = RequestStack.getCurrent(); requestStack.setTimerName(getTimerName()); - final Set urlStrings = new TreeSet(); + final Set urlStrings = new TreeSet<>(); // No need to select img elements too -> they are automatically downloaded (#1379) // Scripts can be skipped as well -> automatically downloaded as needed elements = htmlPage.getByXPath("//input[@type='image']"); @@ -743,7 +757,7 @@ public void loadNewStaticContent(final HtmlPage htmlPage) } // use of copy of the frame list to minimize the chance of ConcurrentModificationException's (#1676) - final List frames = new ArrayList(htmlPage.getFrames()); + final List frames = new ArrayList<>(htmlPage.getFrames()); for (final WebWindow frame : frames) { final Page framePage = frame.getEnclosedPage(); @@ -787,7 +801,7 @@ private void loadStaticContent(final WebResponse response) // use a sorted set to hold the links -> this way each resource will be // loaded only once and in the same order - final Set urlStrings = new TreeSet(); + final Set urlStrings = new TreeSet<>(); final boolean isCssModeAlways = CssMode.ALWAYS.equals(getCssMode()); @@ -1073,7 +1087,7 @@ private static Collection resolveUrls(final Collection urlStrings, { // use a hash map to avoid URL duplicates since given collection of URL strings may also contain absolute URL // strings - final HashMap resolvedURLs = new HashMap(urlStrings.size()); + final HashMap resolvedURLs = new HashMap<>(urlStrings.size()); for (final String urlString : urlStrings) { final URL absoluteURL = makeUrlAbsolute(baseURL, urlString); @@ -1097,7 +1111,7 @@ private static Collection resolveUrls(final Collection urlStrings, */ private List getAllowedLinkURIs(final String pageContent) { - final List links = new ArrayList(); + final List links = new ArrayList<>(); final String relAttRegex = loadStaticContent ? LINKTYPE_WHITELIST_PATTERN : "(?i)stylesheet"; for (final String attributeList : LWPageUtilities.getAllLinkAttributes(pageContent)) { @@ -1447,8 +1461,8 @@ private void evalCss(final HtmlPage page, final URL baseURL) evalCss(body, cssRulesWithUrls, browserVersion, cssText); // create absolute URLs - final Collection urls = new ArrayList(); - final Set alreadyLoadedUrls = new HashSet(pageLocalCache.keySet()); + final Collection urls = new ArrayList<>(); + final Set alreadyLoadedUrls = new HashSet<>(pageLocalCache.keySet()); final List pageBaseURLs = Arrays.asList(baseURL); for (final String urlString : CssUtils.getUrlStrings(cssText.toString())) @@ -1486,7 +1500,7 @@ private void evalCss(final HtmlPage page, final URL baseURL) */ private List getCssBaseUrls(final String urlString) { - final List urls = new ArrayList(); + final List urls = new ArrayList<>(); for (final Map.Entry> entry : cssResourceUrlCache.entrySet()) { if (entry.getValue().contains(urlString)) @@ -1579,7 +1593,7 @@ private void evalCss(final HtmlElement element, final List css */ private List getCssRulesWithUrls(final HtmlPage page) { - final List cssRules = new ArrayList(); + final List cssRules = new ArrayList<>(); if (isJavaScriptEnabled()) { @@ -1685,7 +1699,7 @@ else if (rule instanceof CSSMediaRuleImpl) { // get the embedded CSS rules and process them recursively final List rules = mediaRule.getCssRules().getRules(); - for (AbstractCSSRuleImpl r : rules) + for (final AbstractCSSRuleImpl r : rules) { addCssStyleRulesWithUrls(sheet, r, cssStyleRules); } @@ -1714,7 +1728,7 @@ private static boolean containsUrl(final String cssText) */ private List getAllWebWindows(final Page page) { - final List webWindows = new ArrayList(); + final List webWindows = new ArrayList<>(); if (page != null) { @@ -1951,20 +1965,20 @@ else if (browserType.equals("FF_ESR")) enum CssMode { - /** - * Always download images references in CSS files. - */ - ALWAYS, + /** + * Always download images references in CSS files. + */ + ALWAYS, - /** - * Resolve and download image references on demand. - */ - ONDEMAND, + /** + * Resolve and download image references on demand. + */ + ONDEMAND, - /** - * Never resolve or download any image reference. - */ - NEVER; + /** + * Never resolve or download any image reference. + */ + NEVER; /** * Returns the CSSMode instance for the given mode string. @@ -1995,25 +2009,25 @@ else if (s.equals("ondemand")) enum AjaxMode { - /** - * Perform AJAX calls always asynchronously. - */ - ASYNC, - - /** - * Perform AJAX calls always synchronously. - */ - SYNC, - - /** - * Perform AJAX calls as intended by programmer. - */ - NORMAL, - - /** - * Re-synchronize asynchronous AJAX calls calling from the main thread. - */ - RESYNC; + /** + * Perform AJAX calls always asynchronously. + */ + ASYNC, + + /** + * Perform AJAX calls always synchronously. + */ + SYNC, + + /** + * Perform AJAX calls as intended by programmer. + */ + NORMAL, + + /** + * Re-synchronize asynchronous AJAX calls calling from the main thread. + */ + RESYNC; /** * Returns the AjaxMode instance for the given mode string. @@ -2044,4 +2058,100 @@ else if (s.equals("resync")) return mode; } } + + /** + * Sets up the key store with the client key to be used by this web client. The store is read from a file specified + * in the test suite configuration. + * + * @param props + * the configuration + */ + private void setUpKeyStore(final XltProperties props) + { + final String storeFilePath = props.getProperty("com.xceptance.xlt.tls.keyStore.file"); + if (StringUtils.isNotBlank(storeFilePath)) + { + final String storePassword = props.getProperty("com.xceptance.xlt.tls.keyStore.password"); + final char[] storePasswordChars = getPasswordChars(storePassword); + + final KeyStore store = getStore(storeFilePath, storePasswordChars); + getOptions().setSSLClientCertificateKeyStore(store, storePasswordChars); + } + } + + /** + * Sets up the trust store with the server certificate to be used by this web client. The store is read from a file + * specified in the test suite configuration. + * + * @param props + * the configuration + */ + private void setUpTrustStore(final XltProperties props) + { + final String storeFilePath = props.getProperty("com.xceptance.xlt.tls.trustStore.file"); + if (StringUtils.isNotBlank(storeFilePath)) + { + final String storePassword = props.getProperty("com.xceptance.xlt.tls.trustStore.password"); + final char[] storePasswordChars = getPasswordChars(storePassword); + + final KeyStore store = getStore(storeFilePath, storePasswordChars); + getOptions().setSSLTrustStore(store); + } + } + + /** + * Returns the given password string as a char array. + * + * @param password + * the password + * @return the password as char array (will be empty if the password was null) + */ + private static char[] getPasswordChars(final String password) + { + return password == null ? ArrayUtils.EMPTY_CHAR_ARRAY : password.toCharArray(); + } + + /** + * Returns the key/trust store for the given file path from the internal store cache. If the store has not been + * loaded so far, it will be loaded and put in the cache. + * + * @param storeFilePath + * the path to the store file, relative to the test suite root directory + * @param storePassword + * the store password + * @return the store + */ + private static KeyStore getStore(final String storeFilePath, final char[] storePassword) + { + XltLogger.runTimeLogger.debug("Getting store for path '{}'", storeFilePath); + + return storeCache.computeIfAbsent(storeFilePath, path -> loadStore(path, storePassword)); + } + + /** + * Loads a key/trust store from the given path and opens it with the provided password. + * + * @param storeFilePath + * the path to the store file, relative to the test suite root directory + * @param storePassword + * the store password + * @return the loaded store + * @throws XltException + * if anything went wrong + */ + private static KeyStore loadStore(final String storeFilePath, final char[] storePassword) + { + try + { + XltLogger.runTimeLogger.debug("Loading store from '{}'", storeFilePath); + + final File storeFile = new File(storeFilePath); + + return KeyStore.getInstance(storeFile, storePassword); + } + catch (final Exception e) + { + throw new XltException("Failed to read key/trust store from: " + storeFilePath, e); + } + } } diff --git a/src/main/java/org/htmlunit/WebClientOptions.java b/src/main/java/org/htmlunit/WebClientOptions.java index 7478ef64b..5e25d3e84 100644 --- a/src/main/java/org/htmlunit/WebClientOptions.java +++ b/src/main/java/org/htmlunit/WebClientOptions.java @@ -652,7 +652,12 @@ public void setSSLTrustStore(final URL sslTrustStoreUrl, final String sslTrustSt } } + // GH#497 start + /* void setSSLTrustStore(final KeyStore keyStore) { + */ + public void setSSLTrustStore(final KeyStore keyStore) { + // GH#497 end sslTrustStore_ = keyStore; } From 3ba483fa78f0565fe35b6d117f282911148df2c6 Mon Sep 17 00:00:00 2001 From: Joerg Werner <4639399+jowerner@users.noreply.github.com> Date: Mon, 3 Jun 2024 11:23:29 +0200 Subject: [PATCH 13/13] prepared release 8.2.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0480c20c2..4725abcc6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.xceptance xlt - 8.1.0 + 8.2.0 jar XLT