diff --git a/README.md b/README.md
index aeed5cf..0955a2c 100644
--- a/README.md
+++ b/README.md
@@ -11,13 +11,16 @@ Reports can easily grow to several MB.
- `npm install --global yarn`
- `yarn start`
+## Artifact information
+* `/path/to/your/all.json` is the file produced by Hyperfoil.
+* `test-report.html` will be the output created by `report.sh`
## Generate the HTML report for testing
```bash
yarn run build
chmod +x build/report.sh
./build/report.sh /path/to/your/all.json > test-report.html
```
-Open the HTML on your browser.
+Open the `test-report.html` HTML on your browser.
## Building
diff --git a/src/pages/Phase.js b/src/pages/Phase.js
index ab63cdd..6364378 100644
--- a/src/pages/Phase.js
+++ b/src/pages/Phase.js
@@ -51,8 +51,13 @@ export default () => {
rtrn._bucketCount = index > 0 ? entry.totalCount - all[index - 1].totalCount : entry.totalCount
rtrn._total = all[all.length - 1].totalCount;
rtrn.value = entry.to / 1000000
+
if (rtrn.percentile === 1) {
- return undefined; //
+ // Give 100% a safe "inversed" value so it doesn't calculate Infinity.
+ // We place it one logarithmic step (x10) past the previous maximum point.
+ const prev = all[index - 1];
+ rtrn.inversed = prev && prev.percentile !== 1 ? (1 / (1 - prev.percentile)) * 10 : 100000;
+ return rtrn;
} else {
rtrn.inversed = (1 / (1 - rtrn.percentile))
return rtrn;
@@ -64,21 +69,32 @@ export default () => {
const tickTransform = {}
const ranges = {}
percentileHisto.forEach((entry,idx,all)=>{
-
tickTransform[entry.inversed] = entry.percentile
ranges[entry.count] = (idx>0 ? all[idx-1].count : 0)
-
})
+
+ // Reusable formatter for dynamic decimal precision
+ const formatPercentValue = (v) => {
+ if (v === 1) return "100%";
+ const percent = 100 * v;
+
+ if (v >= 0.999999) return percent.toFixed(6) + "%";
+ if (v >= 0.99999) return percent.toFixed(5) + "%";
+ if (v >= 0.9999) return percent.toFixed(4) + "%";
+ if (v >= 0.999) return percent.toFixed(3) + "%";
+
+ return percent.toFixed(2) + "%";
+ };
+
const tickFormatter = (v) => {
if (typeof tickTransform[v] !== "undefined") {
v = tickTransform[v]
}
- return Number(100 * v).toFixed(2)+"%"
+ return formatPercentValue(v);
}
+
const responsetimeTickFormatter = (v,f,g) => {
- // const entry = responsetimeHisto[v];
return v;
- //return Math.round(entry.from / 1000000).toFixed(0)+"-"+Math.round(entry.to / 1000000).toFixed(0)
}
const extra = [
@@ -86,16 +102,53 @@ export default () => {
(v) => ({ color: "grey", name: "after", value: (v._total - v.totalCount) })
]
+ const targetPercentiles = [
+ { key: 0, label: "p0" },
+ { key: 0.25, label: "p25" },
+ { key: 0.50, label: "p50" },
+ { key: 0.75, label: "p75" },
+ { key: 0.90, label: "p90" },
+ { key: 0.95, label: "p95" },
+ { key: 0.99, label: "p99" },
+ { key: 0.999, label: "p99.9" },
+ { key: 0.9999, label: "p99.99" },
+ { key: 0.99999, label: "p99.999" },
+ { key: 0.999999, label: "p99.9999" },
+ { key: 1.0, label: "p100" },
+ ];
+
+ // Map the definitions and display the exactly matched percentile
+ const percentileTableRows = targetPercentiles.map(tp => {
+ const bucket = stat.histogram.percentiles.find(e => e.percentile >= tp.key)
+ || stat.histogram.percentiles[stat.histogram.percentiles.length - 1];
+
+ const valueMs = bucket ? (bucket.to / 1000000).toFixed(2) : "N/A";
+ const exactPercentile = bucket ? formatPercentValue(bucket.percentile) : "N/A";
+
+ return (
+
+ |
+ {tp.label} ({exactPercentile})
+ |
+
+ {valueMs} ms
+ |
+
+ );
+ });
+
segments.push(
{stat.name}
-
-
-
- {`${stat.metric} response time histogram`}
-
-
-
+
+ {/* 1. Response Time Histogram */}
+
+
+
+ {`${stat.metric} response time histogram`}
+
+
+
{({ height, width }) => {
return (
{
)
}}
-
+
-
-
-
- {`${stat.metric} percentile distribution`}
-
-
-
+
+ {/* 2. Percentile Distribution Chart */}
+
+
+
+ {`${stat.metric} percentile distribution`}
+
+
+
{({ height, width }) => {
return (
{
>
@@ -179,18 +234,41 @@ export default () => {
)
}}
-
+
+
+
+ {/* 3. Percentile Data Table */}
+
+
+
+ {`${stat.metric} key performance percentiles`}
+
+
+
+
+
+
+ | Percentile |
+ Response Time |
+
+
+
+ {percentileTableRows}
+
+
+
)
})
+
return (<>
- {segments}
-
-
-
-
-
-
- >)
+ {segments}
+
+
+
+
+
+
+ >)
}
diff --git a/src/pages/Summary.js b/src/pages/Summary.js
index 8560543..c22ea3b 100644
--- a/src/pages/Summary.js
+++ b/src/pages/Summary.js
@@ -52,51 +52,31 @@ const statAccessors = [
const colors = theme.colors.chart
const colorNames = Object.keys(colors);
-const phasesTimetable = (data = [], stats = [], getStart = v => v.startTime, getEnd = v => v.endTime, getKey = v=>v._pif) => {
+const phasesTimetable = (data = [], stats = [], getStart = v => v.startTime, getEnd = v => v.endTime, getKey = v => v._pif) => {
let rtrn = {}
- // In runs with multiple agents the wall-clock timestamps often don't match exactly; while merging the stats
- // each interval is an union of the agents' intervals and therefore the per-second intervals overlap.
- // That would mess up charts, producing a sawtooth-like pattern instead of bars, so we have to artificially correct it.
- let ends = {}
- data.forEach(entry => {
- const key = getKey(entry)
- let phaseEnds = ends[key]
- if (!phaseEnds) {
- ends[key] = phaseEnds = []
- }
- phaseEnds.push(getEnd(entry))
- })
- Object.values(ends).forEach(phaseEnds => phaseEnds.sort())
- data.forEach(entry => {
- const key = getKey(entry);//phaseName
- let start = getStart(entry);
+ data.forEach(entry => {
+ const key = getKey(entry);
+ const start = getStart(entry);
const end = getEnd(entry);
- const prevEndIndex = ends[key].filter(e => e < end).length - 1
- if (prevEndIndex >= 0 && ends[key][prevEndIndex] >= start) {
- start = ends[key][prevEndIndex] + 1
- }
-
- const rtrnStart = rtrn[start] || { _areaKey: start }
- const rtrnEnd = rtrn[end] || { _areaKey: end }
+ // Plot each bucket at its startTime. With type="stepAfter" recharts
+ // draws a flat horizontal segment from this point to the next, which
+ // visually covers exactly the [startTime, endTime] window. The tooltip
+ // activates anywhere in that span because recharts assigns cursor
+ // positions to the nearest preceding data point.
+ const rtrnStart = rtrn[start] || { _areaKey: start, start: start, end: end };
stats.forEach(stat => {
const statKey = key + "_" + stat.name;
- const statValue = stat.accessor(entry)
- rtrnStart[statKey] = statValue
- rtrnEnd[statKey] = statValue
- })
- rtrnStart.start = start
- rtrnStart.end = end
- rtrnEnd.start = start
- rtrnEnd.end = end
+ rtrnStart[statKey] = stat.accessor(entry);
+ });
rtrn[start] = rtrnStart;
- rtrn[end] = rtrnEnd;
- })
- //sort by the timestamp
- rtrn = Object.values(rtrn).sort((a, b) => a._areaKey - b._areaKey)
- return rtrn
+ });
+
+ // Sort chronologically by start time
+ rtrn = Object.values(rtrn).sort((a, b) => a._areaKey - b._areaKey);
+ return rtrn;
}
const getPhaseTransitionTs = (data = [], getStart = (v) => v.startTime, getEnd = v => v.endTime) => {
const rtrn = []
@@ -198,8 +178,8 @@ function Section({ forkName, metricName }) {
stroke={color}
unit="ns"
fill={color}
- connectNulls={true} //needs to be true for cases of overlap betweeen phases
- type="monotone"
+ connectNulls={true}
+ type="stepAfter"
yAxisId={0}
isAnimationActive={false}
style={{ opacity: 0.5 }}
@@ -216,6 +196,7 @@ function Section({ forkName, metricName }) {
stroke={"#FF0000"}
fill={"#FF0000"}
connectNulls={true}
+ type="stepAfter"
dot={false}
isAnimationActive={false}
style={{ strokeWidth: 1 }}
@@ -230,6 +211,7 @@ function Section({ forkName, metricName }) {
stroke={"#00A300"}
fill={"#00A300"}
connectNulls={true}
+ type="stepAfter"
dot={false}
isAnimationActive={false}
style={{ strokeWidth: 1 }}
@@ -244,6 +226,7 @@ function Section({ forkName, metricName }) {
stroke={"#A30000"}
fill={"#A30000"}
connectNulls={true}
+ type="stepAfter"
dot={false}
isAnimationActive={false}
style={{ strokeWidth: 1 }}