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
Binary file modified favicon.ico
Binary file not shown.
334 changes: 318 additions & 16 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="manifest" href="manifest.json">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Wallpoet&display=swap" rel="stylesheet">
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
Expand All @@ -21,15 +24,33 @@
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<style>
.link {
color: rgb(45,46,103);
}

.link:hover {
color: #0d6efd;
}
</style>
<!-- End Matomo Code -->
</head>
<body>
<div class="container">
<a href="https://www.codeart.dk/"><img src="https://www.codeart.dk/contentassets/ec584942f4c84114aed03c3ec8161130/logo-2.png" style="width:100px;" alt="CodeArt"></a>
<h1 class="text-center">WebLoadBreaker</h1>
<h5 class="text-center">100% browser-based load test tool. A <a href="https://www.codeart.dk/">CodeArt</a> project.</h5>
<p class="text-center">
This is an <a href="https://github.com/CodeArtDK/WebLoadBreaker">open source</a>, browser based (runs on the client with javascript) tool for doing simple load testing on specific target urls.
<div class="container py-4">
<h1 class="text-center mb-1 d-block d-md-none" style="font-family: 'Wallpoet', cursive; margin-bottom: 2rem!important;">WebLoad
<span style="transform: rotate(15deg) translate(0px,45px); display: inline-block;">Breaker</span>
</h1>
<h1 class="text-center mb-1 d-none d-md-block" style="font-family: 'Wallpoet', cursive; font-size: 4rem;">WebLoad
<span style="transform: rotate(15deg) translate(0px,45px); display: inline-block;">Breaker</span>
</h1>
<p class="text-center" style="margin-bottom: 4rem;">by
<a href="https://www.codeart.dk/">
<img src="https://www.codeart.dk/contentassets/ec584942f4c84114aed03c3ec8161130/logo-2.png" style="width:100px;" alt="CodeArt">
</a>
</p>
<h5 class="text-center">100% browser-based load test tool</h5>
<p class="text-center col-md-9 mx-auto">
This is an <a class="link" href="https://github.com/CodeArtDK/WebLoadBreaker">open source</a>, browser based (runs on the client with javascript) tool for doing simple load testing on specific target urls.
Pick a target url to hit, set a number of concurrent users to emulate and how many requests should be run and set your browser to work.<br/>
Since this is testing using javascript in your local browser it can test on any urls you can access.
</p>
Expand All @@ -38,18 +59,18 @@ <h5 class="text-center">100% browser-based load test tool. A <a href="https://ww
<div class="alert alert-warning d-none" id="warning" role="alert">
</div>

<form class="row g-3">
<div class="col-md-12">
<label for="target" class="form-label">Url to test:</label>
<input type="url" class="form-control" id="target" aria-describedby="targetHelp" placeholder="Paste in url to test">
<form class="row g-3 mx-1 p-3 pb-4 p-md-5" style="box-shadow: 4px 4px black; border: 2px solid;">
<div class="col-md-12 mb-md-4">
<label for="target" class="form-label" style="font-weight: 700; font-size: 1.2rem;">Url to test:</label>
<input type="url" class="form-control" id="target" aria-describedby="targetHelp" placeholder="Paste in url to test" style="border: none; border-bottom: 2px solid #0d6efd; color:#0d6efd; border-radius: 0; padding-left: 0;">
<div id="targetHelp" class="form-text">The target url to test. Hint: Use {rand10-100} in the url to insert a random number between 10 and 100.</div>
</div>
<div class="col-md-6">
<div class="col-md-6 mb-md-4">
<label for="threads" class="form-label">Number of users to emulate: <span id="threadNo">8</span></label>
<input type="range" class="form-range" min="1" max="100" value="8" id="threads" aria-describedby="threadsHelp" oninput="javascript:document.getElementById('threadNo').innerText=this.value;">
<div id="threadsHelp" class="form-text">The number simultaneous requests.</div>
</div>
<div class="col-md-6">
<div class="col-md-6 mb-md-4">
<label for="reqgoal" class="form-label">Number of requests: <span id="reqNo">30</span></label>
<input type="range" class="form-range" min="1" max="500" id="reqgoal" value="30" aria-describedby="reqgoalHelp" oninput="javascript:document.getElementById('reqNo').innerText=this.value;">
<div id="reqgoalHelp" class="form-text">The number of requests to test.</div>
Expand All @@ -59,11 +80,10 @@ <h5 class="text-center">100% browser-based load test tool. A <a href="https://ww
</div>
</form>

<div class="d-none" id="resultspane">
<hr/>
<h3>Previous results</h3>
<div class="d-none py-5" id="resultspane">
<h6>Previous results</h6>

<select id="historyHeader" class="form-select form-select-lg mb-3" aria-label=".form-select-lg example" onchange="javascript:ShowHistory(this.value)">
<select id="historyHeader" class="form-select form-select-lg mb-3" aria-label=".form-select-lg example" onchange="javascript:ShowHistory(this.value)" style="font-size: 1rem;">
</select>
<table class="table table-striped" id="historyTable">
<thead>
Expand Down Expand Up @@ -148,7 +168,289 @@ <h1 class="modal-title fs-5" id="runModalLabel">Testing...</h1>
</div>
</div>
</div>

<script>
//TODO:
// - handle errors elegantly
// {rand10-100} placeholder in url
// Support query parameters


//Initialization!
var trowtemplate='<tr><th scope="row">#date#</th><td>#details#</td><th scope="row">#rps#</th><td>#min#</td><td>#avg#</td><td>#median#</td><td>#max#</td><td><div class="btn-group" role="group"><button type="button" class="btn btn-outline-primary" onclick=\'javascript:setFormValues("#target#","#users#","#requests#");showModal();\'>Repeat</button><button type="button" class="btn btn-outline-danger" onclick=\'javascript:deleteEntry("#timeid#");\'>Delete</button></div></td></tr>';

var urlhistoryStr=localStorage.getItem('urlhistory');
var urlhistory=[];
if(urlhistoryStr) urlhistory=JSON.parse(urlhistoryStr);
var resultshistoryStr=localStorage.getItem('resultshistory');
var resultshistory=[];
if(resultshistoryStr) resultshistory=JSON.parse(resultshistoryStr);




//Set form defaults
setFormValues(localStorage.getItem('target'),localStorage.getItem('threads'),localStorage.getItem('reqgoal'));

if(urlhistory.length>0){
//Show history
ShowHistory(localStorage.getItem('target'));
}

//Show warning if performance numbers are not supported in browser
if(performance==undefined){
let w=document.getElementById('warning');
w.classList.remove('d-none');
w.innerText="Your browser does not support all the features required to measure performance correctly";
}


function setFormValues(target,users,requests){
if(target){
document.getElementById('target').value=target;
}
if(users){
document.getElementById('threads').value=users;
document.getElementById('threadNo').innerText=users;
}
if(requests){
document.getElementById('reqgoal').value=requests;
document.getElementById('reqNo').innerText=requests;
}
}

function formatDate(date){

let d=new Date(date);
var datestring = d.getFullYear() + "-" + (d.getMonth()+1).toString().padStart(2, '0')+"-"+d.getDate().toString().padStart(2, '0') + " " +d.getHours().toString().padStart(2, '0') + ":" + d.getMinutes().toString().padStart(2, '0')+ ":" + d.getSeconds().toString().padStart(2, '0');
return datestring;
}

function deleteEntry(tm){
let itm=resultshistory.find(elem => elem.time==tm);
if(!itm)return;
let idx=resultshistory.indexOf(itm);
resultshistory.splice(idx,1);
localStorage.setItem('resultshistory', JSON.stringify(resultshistory));
//TODO: If there are no more items from that target in resultshistory then remove from url history
ShowHistory(document.getElementById('historyHeader').value);
}

function ShowHistory(currentUrl){
let grouped = resultshistory.reduce(function (r, a) {
r[a.url] = r[a.url] || [];
r[a.url].push(a);
return r;
}, Object.create(null));

let hheader=document.getElementById('historyHeader');
hheader.innerHTML='';
(urlhistory).forEach(u => {
hheader.appendChild(
Object.assign(
document.createElement('option'),
{ value:u, selected:(u == currentUrl)}
)
).innerText=u;
});

let hbody=document.getElementById('historyBody');
hbody.innerHTML="";
if(!grouped[currentUrl]) return;
(grouped[currentUrl].reverse()).forEach(r => {
hbody.innerHTML+=trowtemplate
.replace('#date#',formatDate(Date.parse(r.time)))
.replace('#rps#',r.rps)
.replace('#details#',r.threads+' users, '+r.reqgoal+' requests')
.replace('#min#',(r.timings) ? r.timings.min:"n/a")
.replace('#avg#',(r.timings) ? r.timings.avg:"n/a")
.replace('#median#',(r.timings) ? r.timings.med:"n/a")
.replace('#max#',(r.timings) ? r.timings.max:"n/a")
.replace('#target#',currentUrl)
.replace('#users#',r.threads)
.replace('#timeid#',r.time)
.replace('#requests#',r.reqgoal);
//.replace('#errors#',r.errors);
});

//Unhide history
document.getElementById('resultspane').classList.remove('d-none');
}


function addTestResults(url,results){
//results contain time, url, threads, req-per-minute, min, max, median
//Add to urlhistory if it doesn't already exist there (and save to local storage) and to resulthistory
if(!urlhistory.includes(url)) urlhistory.push(url);
resultshistory.push(results);
localStorage.setItem('urlhistory', JSON.stringify(urlhistory));
localStorage.setItem('resultshistory', JSON.stringify(resultshistory));
ShowHistory(url);
}

function resetModal(url){
SetProgress(0,100);
document.getElementById('spinner').style.display='block';
document.getElementById('runModalLabel').innerText='Testing '+url;
document.getElementById('stopBtn').disabled=false;
document.getElementById('closeBtn').disabled=true;
document.getElementById('results').style.display='none';
}

function calculate_load_times(skipcount) {
const median = arr => {
const mid = Math.floor(arr.length / 2),
nums = [...arr].sort((a, b) => a - b);
return arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2;
};
const arrSum = arr => arr.reduce((a,b) => a + b, 0);
// Check performance support
if (performance === undefined) {
console.log("Unable to calculate load times - not supported in browser");
return;
}

// Get a list of "resource" performance entries
const resources = performance.getEntriesByType("resource");
if (resources === undefined || resources.length <= skipcount) {
console.log("Unable to Load Times: there are NO new `resource` performance records");
return;
}
console.log("Calculating load times");
let times=[];
for(let i=skipcount;i<resources.length;i++){
//Validate that it is a proper resource call to target
if(resources[i].initiatorType=='fetch' && !isNaN(resources[i].duration)){
times.push(Math.round(resources[i].duration));
}
}
console.log(times);
let result = {
min: Math.min(...times),
max: Math.max(...times),
avg: Math.round(arrSum(times)/times.length),
med: Math.round(median(times))
};
console.log(result);
return result;
}

function get_resource_count(){
return performance.getEntriesByType("resource").length;
}


function SetProgress(val,max){
let progress=document.getElementById('pbar');
let pct=((val*100)/max);
progress.style.width=pct+"%";
progress.setAttribute('aria-valuenow',pct);
}

const runModal = document.getElementById('runModal');
runModal.addEventListener('shown.bs.modal', () => {
RunTest();
});
runModal.addEventListener('show.bs.modal', () => {
resetModal('');
});
function hideModal() {
const modal = bootstrap.Modal.getInstance(runModal);
modal.hide();
}
function showModal(){
const modal = bootstrap.Modal.getInstance(runModal);
modal.show();
}



function RunTest(){
const url=document.getElementById('target').value;
localStorage.setItem('target',url);
const threads=document.getElementById('threads').value;
localStorage.setItem('threads',threads);
const reqgoal=document.getElementById('reqgoal').value;
localStorage.setItem('reqgoal',reqgoal);

let totalreqs=0;
let errors=0;
let stopping=false;
let rescount=get_resource_count();
let controller = new AbortController();

function done(){
let s2=new Date();
let dif=s2.getTime()-s1.getTime();
let rps = Math.round((totalreqs/(dif/1000))*100)/100;
let t=calculate_load_times(rescount);
addTestResults(url,{
url: url,
rps: rps,
threads: threads,
reqgoal: reqgoal,
timings: t,
time: new Date(),
errors: errors
});
document.getElementById('results').style.display='block';
document.getElementById('rps').innerText=rps;
document.getElementById('mintime').innerText=t.min;
document.getElementById('mediantime').innerText=t.med;
document.getElementById('avgtime').innerText=t.avg;
document.getElementById('maxtime').innerText=t.max;
// document.getElementById('errors').innerText=errors;


document.getElementById('spinner').style.display='none';
document.getElementById('stopBtn').disabled=true;
document.getElementById('closeBtn').disabled=false;
}

function RecursiveFetch(){
const fetchReq = fetch(url, {mode: 'no-cors', cache: 'no-cache', signal: controller.signal})
.then((res) => {
let t=totalreqs++;
// if(!res.ok) errors++;
SetProgress(t,reqgoal);
//console.log(t+' done');
if(t>=reqgoal && !stopping){
stopping=true;
done();
} else if(!stopping) RecursiveFetch();
else console.log('Stopping '+totalreqs);
});
return fetchReq;
}

let runner=[];
for(let i=0;i<threads;i++){
runner.push(RecursiveFetch());
}

//Start logic
console.log('starter...');
resetModal(url);
document.getElementById('stopBtn').onclick=(e) => {
controller.abort();
hideModal();
};



//Track
_paq.push(['trackEvent', 'Test', 'Run', threads+'/'+reqgoal]);


let s1=new Date();
const allData = Promise.all(runner);

allData.then((res) => {
console.log('done with '+totalreqs);
});

}
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
<script src="Script.js"></script>

Expand Down