Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
152 changes: 115 additions & 37 deletions src/pages/Phase.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -64,38 +69,86 @@ 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 = [
(v) => ({ color: "grey", name: "before", value: (v.totalCount - v._bucketCount) }),
(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 (
<tr key={tp.key}>
<td style={{ padding: '12px 16px', borderBottom: '1px solid #ededed', fontWeight: 600 }}>
{tp.label} <span style={{color: '#6a6e73', fontWeight: 400, fontSize: '0.9em'}}>({exactPercentile})</span>
</td>
<td style={{ padding: '12px 16px', borderBottom: '1px solid #ededed', textAlign: 'right', fontWeight: 600, color: '#002F5D' }}>
{valueMs} ms
</td>
</tr>
);
});

segments.push(
<React.Fragment key={stat.metric}>
<Title headingLevel="h1" size="4xl">{stat.name}</Title>
<Card style={{ pagreBreakInside: 'avoid' }}>
<CardHeader>
<Toolbar className="pf-l-toolbar pf-u-justify-content-space-between pf-u-mx-xl pf-u-my-md">
<ToolbarGroup><ToolbarItem>{`${stat.metric} response time histogram`}</ToolbarItem></ToolbarGroup>
</Toolbar>
</CardHeader>
<CardBody style={{ minHeight: 410 }}>

{/* 1. Response Time Histogram */}
<Card style={{ pageBreakInside: 'avoid', marginBottom: '1rem' }}>
<CardHeader>
<Toolbar className="pf-l-toolbar pf-u-justify-content-space-between pf-u-mx-xl pf-u-my-md">
<ToolbarGroup><ToolbarItem>{`${stat.metric} response time histogram`}</ToolbarItem></ToolbarGroup>
</Toolbar>
</CardHeader>
<CardBody style={{ minHeight: 410 }}>
<AutoSizer>{({ height, width }) => {
return (
<ComposedChart
Expand All @@ -107,7 +160,7 @@ export default () => {
<XAxis
type="number"
scale="linear"
domain={['auto', 'auto']} //for use with log
domain={['auto', 'auto']}
dataKey="value"
>
<Label
Expand Down Expand Up @@ -144,15 +197,17 @@ export default () => {
</ComposedChart>
)
}}</AutoSizer>
</CardBody>
</CardBody>
</Card>
<Card style={{ pageBreakInside: 'avoid' }}>
<CardHeader>
<Toolbar className="pf-l-toolbar pf-u-justify-content-space-between pf-u-mx-xl pf-u-my-md">
<ToolbarGroup><ToolbarItem>{`${stat.metric} percentile distribution`}</ToolbarItem></ToolbarGroup>
</Toolbar>
</CardHeader>
<CardBody style={{ minHeight: 410 }}>

{/* 2. Percentile Distribution Chart */}
<Card style={{ pageBreakInside: 'avoid', marginBottom: '1rem' }}>
<CardHeader>
<Toolbar className="pf-l-toolbar pf-u-justify-content-space-between pf-u-mx-xl pf-u-my-md">
<ToolbarGroup><ToolbarItem>{`${stat.metric} percentile distribution`}</ToolbarItem></ToolbarGroup>
</Toolbar>
</CardHeader>
<CardBody style={{ minHeight: 410 }}>
<AutoSizer>{({ height, width }) => {
return (
<ComposedChart
Expand All @@ -162,12 +217,12 @@ export default () => {
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
type="number"
scale="log"
domain={['auto', 'auto']} //for use with log
dataKey="inversed"
interval={10}
tickFormatter={tickFormatter}
type="number"
scale="log"
domain={['auto', 'auto']}
dataKey="inversed"
interval={10}
tickFormatter={tickFormatter}
>
<Label value="percentile" position="insideBottom" angle={0} offset={0} textAnchor='middle' style={{ textAnchor: 'middle' }} />
</XAxis>
Expand All @@ -179,18 +234,41 @@ export default () => {
</ComposedChart>
)
}}</AutoSizer>
</CardBody>
</CardBody>
</Card>

{/* 3. Percentile Data Table */}
<Card style={{ pageBreakInside: 'avoid', marginBottom: '1rem' }}>
<CardHeader>
<Toolbar className="pf-l-toolbar pf-u-justify-content-space-between pf-u-mx-xl pf-u-my-md">
<ToolbarGroup><ToolbarItem>{`${stat.metric} key performance percentiles`}</ToolbarItem></ToolbarGroup>
</Toolbar>
</CardHeader>
<CardBody>
<table style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left', fontFamily: 'inherit' }}>
<thead>
<tr>
<th style={{ padding: '12px 16px', borderBottom: '2px solid #d2d2d2' }}>Percentile</th>
<th style={{ padding: '12px 16px', borderBottom: '2px solid #d2d2d2', textAlign: 'right' }}>Response Time</th>
</tr>
</thead>
<tbody>
{percentileTableRows}
</tbody>
</table>
</CardBody>
</Card>
</React.Fragment>
)
})

return (<>
{segments}
<br />
<Card>
<CardBody>
<StatsTable data={totals} />
</CardBody>
</Card>
</>)
{segments}
<br />
<Card>
<CardBody>
<StatsTable data={totals} />
</CardBody>
</Card>
</>)
}
61 changes: 22 additions & 39 deletions src/pages/Summary.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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 }}
Expand All @@ -216,6 +196,7 @@ function Section({ forkName, metricName }) {
stroke={"#FF0000"}
fill={"#FF0000"}
connectNulls={true}
type="stepAfter"
dot={false}
isAnimationActive={false}
style={{ strokeWidth: 1 }}
Expand All @@ -230,6 +211,7 @@ function Section({ forkName, metricName }) {
stroke={"#00A300"}
fill={"#00A300"}
connectNulls={true}
type="stepAfter"
dot={false}
isAnimationActive={false}
style={{ strokeWidth: 1 }}
Expand All @@ -244,6 +226,7 @@ function Section({ forkName, metricName }) {
stroke={"#A30000"}
fill={"#A30000"}
connectNulls={true}
type="stepAfter"
dot={false}
isAnimationActive={false}
style={{ strokeWidth: 1 }}
Expand Down